A decentralized pay-per-document marketplace built with Next.js, Supabase, and Bitcoin SV (BSV) blockchain. Upload documents, set prices, and get paid in cryptocurrency.
ppd is an early-stage TypeScript project in the AI payments / x402 ecosystem, focused on blockchain, bsv-blockchain, hackathon, merge2025. It currently has 0 GitHub stars and 0 forks, and sits alongside related tools like vscode-x402, x402-payments-mcp, image-analysis-mcp, aperture, knowmint, react-native-wallet-app.
A decentralized pay-per-document marketplace built with Next.js, Supabase, and Bitcoin SV (BSV) blockchain. Upload documents, set prices, and get paid in cryptocurrency.
Live Demo: https://ppd-three.vercel.app
git clone <your-repo>
cd ppd
npm install
# Copy and run in Supabase SQL Editor:
backend/supabase/migrations/001_create_documents_table.sql
backend/supabase/migrations/002_create_purchases_table.sql
backend/supabase/migrations/003_create_tags_system.sql
backend/supabase/migrations/004_create_row_level_security.sql
Create .env.local:
# Supabase (from https://app.supabase.com/project/_/settings/api)
NEXT_PUBLIC_SUPABASE_URL=your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# Supabase Service Role Key (Required for admin operations like delete)
# IMPORTANT: Keep this secret! Never expose to client-side code
# Get from: https://app.supabase.com/project/_/settings/api (under "service_role" section)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Backend Wallet (generates from setup command)
BACKEND_PRIVATE_KEY=your-backend-private-key
STORAGE_URL=https://storage.babbage.systems
NETWORK=main
Important: The SUPABASE_SERVICE_ROLE_KEY is required for document deletion to work properly. This key bypasses Row Level Security (RLS) policies and should only be used in secure server-side contexts.
npm run setup:wallet
This creates a backend wallet and adds PRIVATE_KEY to .env.
npm run dev
Open http://localhost:3000 🎉
For testing payments locally:
npm run wallet-server
┌──────────────────────────────────────────┐
│ Next.js Frontend │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Home │ │ Upload │ │ Viewer │ │
│ │ Page │ │ Page │ │ Page │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└────────────────┬─────────────────────────┘
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────┐ ┌──────────┐
│ Supabase │ │ BSV │ │ API │
│ Database │ │ SDK │ │ Routes │
└──────────┘ └──────┘ └──────────┘
Frontend:
Backend:
ppd/
├── app/ # Next.js pages
│ ├── page.tsx # Home/marketplace
│ ├── upload/page.tsx # Upload documents
│ ├── view/[id]/page.tsx # View purchased docs
│ ├── published/page.tsx # Creator's docs
│ ├── library/page.tsx # User's purchases
│ └── creator/stats/page.tsx # Analytics
│
├── pages/api/ # API endpoints
│ ├── documents/ # CRUD + purchase
│ ├── purchases/ # Purchase history
│ ├── stats/ # Analytics data
│ └── tags/ # Tag management
│
├── components/ # React components
│ ├── ui/ # shadcn/ui
│ ├── document-card.tsx # Document display
│ ├── wallet-provider.tsx # Wallet state
│ ├── wallet-button.tsx # Connect UI
│ └── upload-document.tsx # Upload form
│
├── backend/supabase/ # Database layer
│ ├── migrations/ # SQL migrations
│ ├── documents.ts # Document queries
│ ├── purchases.ts # Purchase queries
│ ├── stats.ts # Analytics queries
│ └── tags.ts # Tag queries
│
└── lib/ # Utilities
├── wallet.ts # Frontend wallet
├── wallet-server.ts # Backend wallet
└── middleware.ts # Auth & payments
documentsStores document metadata and binary file data.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| title | VARCHAR(255) | Document title |
| hash | VARCHAR(255) | SHA-256 hash (unique) |
| cost | FLOAT | Price in satoshis |
| address_owner | VARCHAR(255) | Creator's BSV address |
| file_data | BYTEA | Binary PDF data |
| file_size | INTEGER | Size in bytes |
| mime_type | VARCHAR(100) | File type |
| created_at | TIMESTAMP | Upload time |
purchasesTracks document purchases with blockchain transactions.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| address_buyer | VARCHAR(255) | Buyer's BSV address |
| doc_id | UUID | FK to documents.id |
| transaction_id | VARCHAR(255) | BSV transaction hash |
| created_at | TIMESTAMP | Purchase time |
tags & document_tagsMany-to-many relationship for document categorization.
List/Search Documents
GET /api/documents
GET /api/documents?title=react
GET /api/documents?tags=tutorial,beginner
Get Document Details
GET /api/documents/[id]
Upload Document
POST /api/documents
Content-Type: multipart/form-data
Fields:
- file: PDF file
- title: Document title
- cost: Price in satoshis
- address_owner: Creator's BSV address
- tags: JSON array of tag names
View/Download Document
GET /api/documents/[id]/view?buyer=[address]
Purchase Document
POST /api/documents/[id]/purchase
Headers:
- x-bsv-payment: Payment transaction data
Delete Document
DELETE /api/documents/[id]
Get User's Purchases
GET /api/purchases/buyer/[address]
Creator Analytics
GET /api/stats/creator/[address]
List All Tags
GET /api/tags
Create Tag
POST /api/tags
Body: { "name": "tutorial" }
The app uses two separate wallets:
Purpose: Receives payments from buyers
Setup:
npm run setup:wallet
This generates a private key and saves it to .env as PRIVATE_KEY. Add it to Vercel as BACKEND_PRIVATE_KEY.
How it works:
Purpose: Users make payments from their wallet
For Development:
npm run wallet-server
This starts a local wallet server on localhost:3321.
For Production: Users need their own BSV wallet with JSON-API support configured for your domain.
git add .
git commit -m "Initial commit"
git push origin main
Connect to Vercel
Add Environment Variables
In Vercel dashboard → Settings → Environment Variables:
NEXT_PUBLIC_SUPABASE_URL=your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
BACKEND_PRIVATE_KEY=your-backend-private-key
STORAGE_URL=https://storage.babbage.systems
NETWORK=main
vercel --prod
Run in Supabase SQL Editor:
-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
ALTER TABLE tags ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_tags ENABLE ROW LEVEL SECURITY;
-- Public read policies
CREATE POLICY "Public read documents" ON documents FOR SELECT USING (true);
CREATE POLICY "Public read purchases" ON purchases FOR SELECT USING (true);
CREATE POLICY "Public read tags" ON tags FOR SELECT USING (true);
CREATE POLICY "Public read document_tags" ON document_tags FOR SELECT USING (true);
-- Public write policies
CREATE POLICY "Anyone can upload" ON documents FOR INSERT WITH CHECK (true);
CREATE POLICY "Anyone can purchase" ON purchases FOR INSERT WITH CHECK (true);
CREATE POLICY "Anyone can create tags" ON tags FOR INSERT WITH CHECK (true);
CREATE POLICY "Anyone can tag documents" ON document_tags FOR INSERT WITH CHECK (true);
npm run dev # Start dev server (localhost:3000)
npm run build # Build for production
npm run start # Start production server
npm run lint # Run ESLint
npm run setup:wallet # Generate backend wallet
npm run wallet-server # Start local wallet server
# Terminal 1: Main app
npm run dev
# Terminal 2: Wallet server (for testing payments)
npm run wallet-server
# Terminal 3: Watch logs
# Check console for errors
# List documents
curl http://localhost:3000/api/documents
# Upload document
curl -X POST http://localhost:3000/api/documents \
-F "[email protected]" \
-F "title=My Document" \
-F "cost=1000" \
-F "address_owner=03abc..." \
-F 'tags=["tutorial","react"]'
# Search documents
curl "http://localhost:3000/api/documents?title=react&tags=tutorial"
# Get wallet info
curl http://localhost:3000/api/wallet-info
npm run wallet-serverhttp://localhost:3000Problem: 500 error when loading documents
Solution:
NEXT_PUBLIC_SUPABASE_URL in .env.localNEXT_PUBLIC_SUPABASE_ANON_KEYProblem: "No wallet detected" error
Solution:
npm run wallet-serverProblem: Upload returns error
Solution:
file_data column exists in databaseProblem: "Wallet not connected" on view page
Solution:
npm run wallet-serverSolution:
npm run devNEXT_PUBLIC_WALLET_HOST=localhost:3001
NETWORK=test # For testnet
NETWORK=main # For mainnet
STORAGE_URL=https://your-storage-provider.com
The creator dashboard provides:
This is a personal project, but suggestions are welcome!
The app now uses react-pdf (built on PDF.js) for better PDF rendering instead of iframes. Here are the different approaches:
Pros:
Cons:
<iframe src={pdfUrl} className="w-full h-full" />
<object data={pdfUrl} type="application/pdf" />
// More control but more complex setup
import * as pdfjsLib from 'pdfjs-dist';
// Manual canvas rendering
The app uses the unpkg CDN for the PDF.js worker:
pdfjs.GlobalWorkerOptions.workerSrc =
`//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
For production, consider self-hosting the worker file for better performance and reliability.
If you prefer the iframe approach, simply replace the PDFViewer component in app/view/[id]/page.tsx:
// Instead of:
<PDFViewer url={pdfUrl} title={documentMetadata?.title} onDownload={handleDownload} />
// Use:
<div className="w-full h-full">
<iframe src={pdfUrl} className="w-full h-full border-0" title={documentMetadata?.title || 'Document'} />
</div>
If documents aren't displaying properly in the viewer page, check the following:
Open the browser console (F12) and look for:
%PDF (indicates valid PDF data)%PDF-The PDF files are stored as BYTEA in Supabase. Check:
# In Supabase SQL Editor, check a document's file_data:
SELECT id, title, file_size,
length(file_data) as stored_bytes,
substring(file_data, 1, 4) as file_header
FROM documents
LIMIT 1;
stored_bytes should match file_sizefile_header should show hex bytes starting with 25504446 (which is %PDF in hex)Test the view API endpoint:
# Check if the API returns valid PDF data
curl -v "http://localhost:3000/api/documents/[DOC_ID]/view?buyer=[WALLET_ADDRESS]" > test.pdf
# Verify it's a valid PDF
file test.pdf # Should show "PDF document"
Some browsers don't support inline PDF viewing in iframes:
Issue: "Received empty file data"
Issue: "Invalid PDF file format"
backend/supabase/documents.ts\\x prefix is correct for BYTEA hex formatIssue: "Failed to display PDF"
Check the following files for detailed console logs:
app/view/[id]/page.tsx - Frontend PDF loadingpages/api/documents/[id]/view.ts - API endpointbackend/supabase/document-files.ts - Database retrieval# 1. Upload a simple test PDF
# 2. Check the console logs during upload
# 3. Navigate to the viewer page
# 4. Check the console logs during viewing
# 5. Compare file sizes: upload vs download
MIT License - Feel free to use this project for your own purposes.
Built with:
Built with ❤️ using Next.js, Bitcoin SV, and Supabase
Pay for APIs, unlock content, and settle x402 micropayments right inside VS Code.
Give AI agents a wallet — x402 payment tools over Model Context Protocol.
Image analysis and visual understanding tools for AI agents — describe, detect, extract from any image.
Production-grade ZK compliance for AI agent payments on Solana. Real Circom Groth16 proofs gate spending limits, sanctions, and time rules without leaking payment details. Two rails: HTTP 402 (USDC/USDT/aUSDC) and Stripe MPP, both with on-chain attestation.
Knowledge marketplace where AI agents autonomously buy human expertise
💰 Build a personal finance wallet app with React Native, featuring secure authentication, real-time updates, and seamless cloud deployment.
The agent-native LLM router for autonomous agents. 55+ models (8 free), <1ms local routing, USDC payments on Base & Solana via x402.
A payments protocol for the internet. Built on HTTP.
A local-first AI agent with persistent memory, emotional intelligence, and a peer-to-peer skills economy.
The trust layer for agent-to-agent commerce — natural-language mandates, ERC-7710 delegated permissions, x402 payments, escrow, and dispute resolution as one open, catch-all Agent Skill / Claude Code plugin.
Self-healing infrastructure for AI agent payments. 90.3% auto-recovery.
The AI agent with a wallet — spends USDC autonomously to get real work done. Apache-2.0, TypeScript.