A minimal working example showing how to accept x402 crypto payments on Voi / Algorand using patterns from:
- NautilusOSS/x402 — the x402 open standard for internet-native payments
- NautilusOSS/x402-avm-facilitator — AVM payment verification & submission
x402 is an open standard that brings the HTTP 402 Payment Required status
code to life. A server responds with 402 when a resource requires payment,
the client constructs and signs a blockchain transaction, then submits payment
proof back to the server. Once verified, the server returns the protected
resource.
This demo implements the simplest possible end-to-end flow:
Client → GET /premium/joke
← 402 Payment Required (price, receiver, note)
→ builds & signs VOI payment transaction
→ POST /premium/joke/pay { signedTxns }
← server verifies, submits, and confirms on-chain
← { joke: "..." }
server/
src/
index.ts Express server entry point
routes/premium.ts GET /premium/joke & POST /premium/joke/pay
lib/paymentRequirement.ts Payment requirement definition
lib/verifyPayment.ts Transaction decode, validate, submit & confirm
client/
src/
requestPremium.ts Full client flow: request → pay → receive
buildPaymentTxn.ts Build native AVM payment transaction
npm install
cp .env.example .envEdit .env:
SERVER_RECEIVER_ADDRESS— your Voi wallet address (receives payments)CLIENT_MNEMONIC— 25-word mnemonic for the paying wallet (must hold VOI)
The defaults use Voi mainnet via the free Nodely endpoint.
Start the server:
npm run serverIn another terminal, run the client:
npm run clientClient:
→ Requesting GET /premium/joke ...
← 402 Payment Required — 0.01 VOI
→ Constructing payment transaction ...
→ Signing transaction ...
→ Submitting payment proof to POST /premium/joke/pay ...
← Payment verified! Response:
{
"joke": "Why do blockchain developers hate nature? Too many forks."
}
Server:
x402-basic-demo server listening on http://localhost:3000
Payment verified — txId: ABC123...
The client sends GET /premium/joke to the server. Instead of returning the
joke, the server responds with HTTP 402 and a JSON body describing what
payment is needed: amount, receiver address, network, and an
ARC-2 formatted note
identifying the resource.
Using algosdk, the client constructs a native VOI payment transaction with the
exact receiver, amount (10,000 microunits = 0.01 VOI), and note from the 402
response. It then signs the transaction locally with the client's private key.
The signed transaction is base64-encoded and sent back to
POST /premium/joke/pay as { signedTxns: ["<base64>"] }.
The server's verification logic (verifyPayment.ts) follows the validation
pattern from
x402-avm-facilitator:
- Decode each signed transaction from base64 via
algosdk.decodeSignedTransaction - Check the genesis hash to confirm it targets the correct network (Voi mainnet)
- Find a matching
paytransaction in the group with the right receiver, amount (\u2265 10,000 microunits), and note - Submit the raw transaction to the Voi network via algod
- Wait for on-chain confirmation (up to 5 rounds)
If the payment confirms on-chain, the server responds with the protected resource. If anything fails (wrong network, insufficient amount, submission error), it returns 400 with error details.
The server never holds the client's keys. The client signs locally, but the server is the one that actually submits the transaction to the blockchain. The premium content is only released after on-chain confirmation — no trust required.
Transaction notes use the ARC-2 structured format:
x402:j{"resource":"/premium/joke"}
The format is <dappName>:<formatChar><data> where j indicates JSON. This
makes the payment's purpose machine-readable on-chain.
| Field | Value |
|---|---|
| Amount | 0.01 VOI (10,000 microunits) |
| Asset | VOI (native) |
| Network | voi:mainnet |
| Note | x402:j{"resource":"/premium/joke"} |
- algosdk — Algorand/Voi SDK for transaction construction & signing
- express — HTTP server
- dotenv — environment configuration
- tsx — TypeScript execution