A production-minded Base Mainnet USDC payment receipt app using an injected EVM wallet.
Live app: https://base-receipt-six.vercel.app/
Base Receipt creates a short-lived signed USDC payment request, opens MetaMask (or another injected wallet), independently verifies the resulting settlement on the server, and only then issues a receipt.
The full production flow and ERC-8021 attribution have been exercised with a real 0.01 USDC Base Mainnet payment.
- Direct transaction: https://basescan.org/tx/0xa7c0d15e190b7ab099c03013b14f07c6a58a7c15c8c1e7a8132bb7512c9e881d
- Result: successful Base Mainnet USDC transfer
- ERC-8021 result: top-level transaction calldata ends with Builder Code
bc_87fjmj1l - Application result:
Verified on Base. Receipt issued. - Durable receipt claim: persisted in PostgreSQL
A wallet popup returning success is not enough evidence to fulfill an order. The backend should independently verify what actually settled onchain.
Base Receipt checks:
- payment status is
completed - settled USDC amount matches the signed request
- settled recipient matches the signed request
- the payment transaction has not already been claimed by a different order
- The browser submits an amount and recipient to
/api/orders. - The server validates them and returns a 15-minute HMAC-signed payment request.
- The browser sends a directly attributed USDC call through MetaMask on Base Mainnet.
- The browser sends only the transaction hash and signed request to
/api/verify. - The server calls
getPaymentStatus()and checks the verified amount and recipient. - The payment ID is atomically claimed and persisted.
- A verified receipt is returned with a BaseScan transaction link.
For local development, claims are kept in process memory.
For production, set DATABASE_URL to a PostgreSQL database. Base Receipt creates a base_receipt_payments table with the payment transaction hash as its primary key, so concurrent or repeated claims cannot reuse one payment for multiple orders.
Copy .env.example to .env.local and provide:
PAYMENT_REQUEST_SECRET=use-a-long-random-secret
DATABASE_URL=postgres://user:password@host:5432/database?sslmode=requireNever commit either value.
npm install
npm run devQuality gates:
npm run lint
npm run typecheck
npm test
npm run buildThe app intentionally uses testnet: false. Payments are real Base Mainnet USDC transfers. The UI defaults to a small amount and clearly labels the network before payment.
Base Receipt is registered and domain-verified in Base Dashboard with Builder Code bc_87fjmj1l. The external-web payment call appends the ERC-8021 suffix directly to the USDC transfer calldata, while receipt verification remains independent of attribution.
- Direct-wallet Mainnet flow: verified in production
- Signed short-lived payment requests: implemented
- Server-side settlement verification: verified in production
- Atomic PostgreSQL replay protection: verified with durable persistence
- Mainnet receipt + BaseScan explorer link: verified in production
- Live deployment: online
- Base Dashboard registration and domain verification: completed
- Builder Code:
bc_87fjmj1l - Base Weekly Leaderboards visibility: enabled
- External-web ERC-8021 attribution: verified on Base Mainnet