x402 on Algorand | Algorand Developer Portal
x402 on Algorand
x402 is an open payment protocol that lets services charge for API access directly over HTTP. Clients pay per request using the 402 Payment Required status code — no sessions, API keys, or credential management required.
The protocol has three participants: a client sends a request, a resource server returns payment requirements (or the response if already paid), and a facilitator verifies and settles payments on-chain.
This tutorial walks through the protocol by building a working example on Algorand TestNet. You will build a pay-per-API /weather endpoint. Unpaid requests get 402 Payment Required; paid requests get JSON back. You will:
- Build a client that automatically pays when it receives a 402 response
- Build a resource server that charges for
GET /weather - Run both locally end-to-end
Build a pay-per-API service with x402
Prerequisites
Setup
Complete these steps in order before running code:
- Create two TestNet accounts: one for the client (the payer) and one for the resource server (the receiver).
- Fund both accounts with TestNet ALGO using the Lora faucet. See fees and minimum balance.
- Opt both accounts into TestNet USDC through Lora or your wallet. See opt in to assets.
- Get USDC on both accounts from the Circle testnet faucet. Select Algorand Testnet and fund each address.
- Save these values for later: the client’s payer mnemonic (used as
AVM_MNEMONIC) and the resource server’s public address (used asAVM_ADDRESS).
Part 1: Build the client
In this part you’ll build a client that sends a request, handles the 402 response, signs and submits payment, then retries with proof of payment. You’ll test it against a hosted resource server before building your own in Part 2.
Create the project
mkdir x402-demo-client
cd x402-demo-client
pnpm init
Add TypeScript tooling
pm add -D typescript @types/node tsx
Install dependencies
pm add @x402/core @x402/fetch @x402/avm @algorandfoundation/algokit-utils@10.0.0-alpha.42 dotenv
Client config
Create tsconfig.json in the project root:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"types": ["node"]
},
"include": ["**/*.ts"]
}
Environment variables
Add .env to .gitignore, then create .env:
AVM_MNEMONIC="your 25-word mnemonic here"
Plain fetch first (expect 402)
Before adding the x402 libraries, confirm the endpoint requires payment. A plain fetch should return HTTP 402.
Create index.ts at the project root:
const url = 'https://x402.goplausible.xyz/examples/weather';
async function main(): Promise<void> {
const response = await fetch(url, { method: 'GET' });
console.log('status:', response.status, response.statusText);
const paymentHeader = response.headers.get('payment-required');
if (paymentHeader) {
console.log('payment-required (first 80 chars):', paymentHeader.slice(0, 80) + '...');
}
}
main().catch(console.error);
Run:
pnpm tsx index.ts
Expected output (your payment-required value will differ):
status: 402 Payment Required
payment-required (first 80 chars): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY2hlbWUiOiJleGFjd...
Add x402 payment support
Now replace index.ts with the paying version. Instead of a plain fetch, wrapFetchWithPayment intercepts 402 responses, signs an Algorand payment using the mnemonic from AVM_MNEMONIC, and retries the request with the payment proof attached. This requires the Setup above: funded accounts, asset opt-in, and TestNet USDC.
import { config } from 'dotenv';
import { x402Client, wrapFetchWithPayment, x402HTTPClient } from '@x402/fetch';
import { toClientAvmSigner, ExactAvmScheme, ALGORAND_TESTNET_CAIP2 } from '@x402/avm';
import {
ed25519SigningKeyFromWrappedSecret,
type WrappedEd25519Seed,
} from '@algorandfoundation/algokit-utils/crypto';
import { seedFromMnemonic } from '@algorandfoundation/algokit-utils/algo25';
config();
const avmMnemonic = process.env.AVM_MNEMONIC as string;
const url = 'https://x402.goplausible.xyz/examples/weather';
async function main(): Promise<void> {
const secretKey = await getSecretKeyFromMnemonic(avmMnemonic);
const avmSigner = toClientAvmSigner(secretKey);
const client = new x402Client();
client.register(ALGORAND_TESTNET_CAIP2, new ExactAvmScheme(avmSigner));
console.info(`AVM signer: ${avmSigner.address}`);
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment(url, { method: 'GET' });
if (response.ok) {
const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => response.headers.get(name));
console.log('\nPayment response:');
console.log(JSON.stringify(paymentResponse, null, 2));
const data = await response.json();
console.log('\nWeather response:');
console.log(JSON.stringify(data, null, 2));
} else {
console.log(`\nNo payment settled (response status: ${response.status})`);
}
}
async function getSecretKeyFromMnemonic(avmMnemonic: string): Promise<string> {
const seed = seedFromMnemonic(avmMnemonic);
const seedCopy = new Uint8Array(seed);
const wrappedSeed: WrappedEd25519Seed = {
unwrapEd25519Seed: async () => seed,
wrapEd25519Seed: async () => {},
};
const wrappedSecret = await ed25519SigningKeyFromWrappedSecret(wrappedSeed);
return Buffer.concat([Buffer.from(seedCopy), Buffer.from(wrappedSecret.ed25519Pubkey)]).toString('base64');
}
main().catch(error => {
console.error(error?.response?.data?.error ?? error);
process.exit(1);
});
Run the paying client from the project root:
pnpm tsx index.ts
Expected output (addresses and transaction IDs will differ):
AVM signer: ABC123...
Payment response: {
"success": true,
"payer": "ABC123...",
"transaction": "TXID...",
"network": "algorand:..."
}
Part 2: Build the resource server
In this part you’ll build a resource server that hosts a /weather endpoint. It returns a 402 response when a request lacks valid payment and delegates verification to the facilitator, only serving data once payment is confirmed.
Create the server project
mkdir x402-demo-server
cd x402-demo-server
pnpm init
Add TypeScript tooling
pnpm add -D typescript @types/node tsx
Install dependencies
pnpm add @x402/core @x402/avm @x402/hono @x402-avm/extensions hono @hono/node-server dotenv
Server config
Similarly, copy the tsconfig.json from Client config.
Environment variables
Add .env to .gitignore, then create .env:
AVM_ADDRESS=
FACILITATOR_URL=https://facilitator.goplausible.xyz
Use the resource server account’s public address for AVM_ADDRESS ( Setup). FACILITATOR_URL points to the hosted facilitator, so you do not need to run facilitator code for this tutorial.
Implement GET /weather
Create index.ts at the project root. The paymentMiddleware function intercepts incoming requests and checks for a valid payment proof before forwarding to your route handler — unpaid requests get a 402 response with payment instructions.
The accepts array defines what the server will accept as payment. Each entry specifies the scheme, price, network, and recipient. The network value algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI= is the CAIP-2 identifier for Algorand TestNet — this tells clients which chain to submit payment on.
This server also includes bazaar discovery metadata, which helps clients discover paid x402 resources by describing each route’s method, price, response type, and example output in the 402 Payment Required response. If you use the hosted GoPlausible facilitator and settle a real TestNet payment, you can view the activity in the facilitator dashboard.
import { config } from 'dotenv';
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { paymentMiddleware, x402ResourceServer } from '@x402/hono';
import { HTTPFacilitatorClient } from '@x402/core/server';
import type { ResourceServerExtension } from '@x402/core/types';
import { ExactAvmScheme } from '@x402/avm/exact/server';
import { ALGORAND_TESTNET_CAIP2, USDC_TESTNET_ASA_ID } from '@x402/avm';
import { declareDiscoveryExtension, bazaarResourceServerExtension } from '@x402-avm/extensions';
config();
const avmAddress = process.env.AVM_ADDRESS;
const facilitatorUrl = process.env.FACILITATOR_URL;
if (!avmAddress || !facilitatorUrl) {
console.error('Missing environment variables: AVM_ADDRESS or FACILITATOR_URL');
process.exit(1);
}
const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
const server = new x402ResourceServer(facilitatorClient);
server.register(ALGORAND_TESTNET_CAIP2, new ExactAvmScheme());
server.registerExtension(bazaarResourceServerExtension as unknown as ResourceServerExtension);
const weatherDiscovery = declareDiscoveryExtension({
output: {
example: {
report: {
weather: 'sunny',
temperature: 70,
timestamp: new Date().toISOString(),
},
},
},
});
const app = new Hono();
app.use(
paymentMiddleware(
{
'GET /weather': {
accepts: [
{
scheme: 'exact',
price: '$0.005',
network: ALGORAND_TESTNET_CAIP2,
payTo: avmAddress,
extra: { asset: USDC_TESTNET_ASA_ID },
},
],
description: 'Weather data access',
mimeType: 'application/json',
extensions: weatherDiscovery,
},
},
server,
),
);
app.get('/weather', c => {
return c.json({
report: {
weather: 'sunny',
temperature: 70,
timestamp: new Date().toISOString(),
},
});
});
serve({ fetch: app.fetch, port: 4021 }, () => {
console.log(`x402 Resource Server listening at http://localhost:4021`);
});
Unpaid GET /weather returns HTTP 402; paid requests return the JSON body.
Part 3: Run locally
In Part 1 you hit a hosted resource server. Now you’ll point the client at your own local server so you can see the full flow end-to-end.
Start the resource server
From the x402-demo-server directory:
pnpm tsx index.ts
Leave it running on http://localhost:4021.
Expected output:
Server listening at http://localhost:4021
Point the client at your server
In the client index.ts, change the URL to your local server:
const url = 'http://localhost:4021/weather';
Run the client
From the x402-demo-client directory:
pnpm tsx index.ts
You should get the weather JSON after payment settles. The output will look similar to the Part 1 output, with your local server’s transaction details.
Next steps
You now have a working x402 payment flow on Algorand TestNet. From here you can add more paid routes, adjust pricing, deploy your resource server publicly for bazaar discovery, or explore the demo repo for a custom facilitator.
x402-demo (examples & basic demo) Examples for the client, server, bazaar integration, basic demo, and custom facilitator
x402 on Algorand Product overview and ecosystem information
Developer resources SDKs, APIs, and integration guides
x402.org Protocol specification and ecosystem directory