From zero to a paid Algorand API through the GoPlausible x402 facilitator — and into the Global Challenge — in minutes.
Develop and test your endpoints and merchant account on Algorand TestNet. When everything works, switch the same code to MainNet. The facilitator verifies and settles every payment for you — gasless for your buyers, USDC in, no payment infrastructure to run.
npm install express @x402/express @x402/avm @x402/core @x402/extensions
npm install hono @hono/node-server @x402/hono @x402/avm @x402/core @x402/extensions
pip install "x402-avm[fastapi,avm]" uvicorn # installs the x402 package with Algorand support
pip install "x402-avm[flask,avm,httpx]" # httpx: used by the sync facilitator client
One middleware, one route config. Buyers without payment get a standards-compliant
HTTP 402; buyers with an x402 client pay USDC and get your response — verified and
settled on-chain by this facilitator in a single round trip. Per the x402 exact scheme
spec for Algorand, your price: "$0.01" resolves on the wire to the ASA id and base
units — the 402 advertises asset: "10458941" and amount: "10000"
(USDC, 6 decimals). The $ sign is understood natively — the middleware reads the
dollar-money string and resolves it to the network's USDC for you. Optionally, pin the
spec fields explicitly instead:
// OPTIONAL spec-exact alternative — pin the ASA id and base units instead of "$0.01":
price: { asset: "10458941", amount: "10000", extra: { name: "USDC", decimals: 6 } },# OPTIONAL spec-exact alternative — pin the ASA id and base units instead of "$0.01":
# (from x402 import AssetAmount)
price=AssetAmount(asset="10458941", amount="10000",
extra={"name": "USDC", "decimals": 6}),feePayer anywhere — gasless is
automatic. The middleware learns the facilitator's fee-payer address from
GET /supported and advertises it in your 402; the buyer's client builds it into the
atomic group; the facilitator co-signs and pays the per-payment network fees. Buyers
spend USDC only per payment — note their account still needs Algorand's standard minimum
balance (0.1 ALGO base + 0.1 ALGO for the USDC opt-in, locked in the account, never spent on
payments)..env with dotenv, node --env-file=.env or
python-dotenv). Recommended names, same in TypeScript and Python —
server: AVM_ADDRESS (your merchant receiving address),
FACILITATOR_URL; client: AVM_PRIVATE_KEY (base64 64-byte key),
RESOURCE_SERVER_URL (the paid endpoint's full URL).import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactAvmScheme } from "@x402/avm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
const ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=";
const facilitator = new HTTPFacilitatorClient({
url: process.env.FACILITATOR_URL || "https://facilitator.goplausible.xyz",
});
const server = new x402ResourceServer(facilitator)
.register(ALGORAND_TESTNET, new ExactAvmScheme());
const app = express();
app.use(paymentMiddleware({
"GET /my-api": {
accepts: [{
scheme: "exact",
network: ALGORAND_TESTNET,
price: "$0.01", // USDC — resolved per network
payTo: process.env.AVM_ADDRESS!, // your merchant account (.env)
}],
description: "My paid API",
// `bazaar` — REQUIRED for discovery: this is what gets you listed. Your
// first settled payment then auto-catalogs the resource. Without it you
// still get paid, but you never appear in /discovery.
// `x402-merchant` — OPTIONAL identity: declare it to CONTROL your name,
// website, logo and categories. Leave it out and they're read from your
// endpoint's domain instead (OpenGraph tags, llms.txt, agent-card.json).
extensions: {
...declareDiscoveryExtension({
output: { example: { ok: true, premium: "data" } },
}),
"x402-merchant": {
info: {
name: "EXAMPLE API", // your public identity
website: "https://my-api.example.com",
logo: "https://my-api.example.com/logo.png",
categories: ["api", "algorand", "x402"],
},
// x402 v2 spec: extensions carry BOTH info and a JSON Schema for it.
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
website: { type: "string" },
logo: { type: "string" },
categories: { type: "array", items: { type: "string" } },
},
},
},
},
},
}, server));
app.get("/my-api", (_req, res) => res.json({ ok: true, premium: "data" }));
app.listen(4021, () => console.log("x402-paid API on :4021"));import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { paymentMiddleware, x402ResourceServer } from "@x402/hono";
import { ExactAvmScheme } from "@x402/avm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
const ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=";
const facilitator = new HTTPFacilitatorClient({
url: process.env.FACILITATOR_URL || "https://facilitator.goplausible.xyz",
});
const server = new x402ResourceServer(facilitator)
.register(ALGORAND_TESTNET, new ExactAvmScheme());
const app = new Hono();
app.use(paymentMiddleware({
"GET /my-api": {
accepts: [{
scheme: "exact",
network: ALGORAND_TESTNET,
price: "$0.01", // USDC — resolved per network
payTo: process.env.AVM_ADDRESS!, // your merchant account (.env)
}],
description: "My paid API",
// `bazaar` — REQUIRED for discovery (first settled payment catalogs the
// resource). `x402-merchant` — OPTIONAL: declare it to control your
// name/website/logo/categories; omit it and they're read from your
// endpoint's domain metadata (OpenGraph, llms.txt, agent-card.json).
extensions: {
...declareDiscoveryExtension({
output: { example: { ok: true, premium: "data" } },
}),
"x402-merchant": {
info: {
name: "EXAMPLE API",
website: "https://my-api.example.com",
logo: "https://my-api.example.com/logo.png",
categories: ["api", "algorand", "x402"],
},
// x402 v2 spec: extensions carry BOTH info and a JSON Schema for it.
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
website: { type: "string" },
logo: { type: "string" },
categories: { type: "array", items: { type: "string" } },
},
},
},
},
},
}, server));
app.get("/my-api", (c) => c.json({ ok: true, premium: "data" }));
serve({ fetch: app.fetch, port: 4021 });
console.log("x402-paid API on :4021");import os
from fastapi import FastAPI
from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
from x402.http.types import RouteConfig
from x402.mechanisms.avm.exact import ExactAvmServerScheme
from x402.server import x402ResourceServer
ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
facilitator = HTTPFacilitatorClient(FacilitatorConfig(
url=os.getenv("FACILITATOR_URL", "https://facilitator.goplausible.xyz")))
server = x402ResourceServer(facilitator)
server.register(ALGORAND_TESTNET, ExactAvmServerScheme())
# BAZAAR_EXT is REQUIRED for discovery: it's what gets you listed (your first
# settled payment auto-catalogs the resource). Without it you still get paid,
# but never appear in /discovery.
# MERCHANT_EXT is OPTIONAL: declare it to CONTROL your name/website/logo/
# categories. Omit it and they're read from your endpoint's domain metadata
# (OpenGraph tags, llms.txt, agent-card.json) instead.
BAZAAR_EXT = {
"info": {"input": {"type": "http", "queryParams": {}},
"output": {"type": "json", "example": {"ok": True, "premium": "data"}}},
"schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object", "required": ["input"],
"properties": {
"input": {"type": "object", "additionalProperties": False,
"required": ["type", "method"],
"properties": {"type": {"type": "string", "const": "http"},
"method": {"type": "string", "enum": ["GET", "HEAD", "DELETE"]},
"queryParams": {"type": "object", "properties": {}}}},
"output": {"type": "object", "required": ["type"],
"properties": {"type": {"type": "string"}, "example": {"type": "object"}}}}},
}
MERCHANT_EXT = {
"info": {"name": "EXAMPLE API", # ← your public identity
"website": "https://my-api.example.com",
"logo": "https://my-api.example.com/logo.png",
"categories": ["api", "algorand", "x402"]},
# x402 v2 spec: extensions carry BOTH info and a JSON Schema for it.
"schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object", "required": ["name"],
"properties": {"name": {"type": "string"}, "website": {"type": "string"},
"logo": {"type": "string"},
"categories": {"type": "array", "items": {"type": "string"}}}},
}
routes = {
"GET /my-api": RouteConfig(
accepts=[PaymentOption(
scheme="exact",
network=ALGORAND_TESTNET,
price="$0.01", # USDC — resolved per network
pay_to=os.environ["AVM_ADDRESS"], # your merchant account (.env)
)],
extensions={"bazaar": BAZAAR_EXT, "x402-merchant": MERCHANT_EXT},
),
}
app = FastAPI()
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)
@app.get("/my-api")
def my_api():
return {"ok": True, "premium": "data"}import os
from flask import Flask, jsonify
from x402.http import FacilitatorConfig, HTTPFacilitatorClientSync, PaymentOption
from x402.http.middleware.flask import payment_middleware
from x402.http.types import RouteConfig
from x402.mechanisms.avm.exact import ExactAvmServerScheme
from x402.server import x402ResourceServerSync
ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
facilitator = HTTPFacilitatorClientSync(FacilitatorConfig(
url=os.getenv("FACILITATOR_URL", "https://facilitator.goplausible.xyz")))
server = x402ResourceServerSync(facilitator)
server.register(ALGORAND_TESTNET, ExactAvmServerScheme())
# Same blocks as the FastAPI example: BAZAAR_EXT is REQUIRED for discovery
# (without it you get paid but stay unlisted); MERCHANT_EXT is OPTIONAL —
# omit it and your name/logo/website come from your domain's metadata.
BAZAAR_EXT = {
"info": {"input": {"type": "http", "queryParams": {}},
"output": {"type": "json", "example": {"ok": True, "premium": "data"}}},
"schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object", "required": ["input"],
"properties": {
"input": {"type": "object", "additionalProperties": False,
"required": ["type", "method"],
"properties": {"type": {"type": "string", "const": "http"},
"method": {"type": "string", "enum": ["GET", "HEAD", "DELETE"]},
"queryParams": {"type": "object", "properties": {}}}},
"output": {"type": "object", "required": ["type"],
"properties": {"type": {"type": "string"}, "example": {"type": "object"}}}}},
}
MERCHANT_EXT = {
"info": {"name": "EXAMPLE API", "website": "https://my-api.example.com",
"logo": "https://my-api.example.com/logo.png",
"categories": ["api", "algorand", "x402"]},
# x402 v2 spec: extensions carry BOTH info and a JSON Schema for it.
"schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object", "required": ["name"],
"properties": {"name": {"type": "string"}, "website": {"type": "string"},
"logo": {"type": "string"},
"categories": {"type": "array", "items": {"type": "string"}}}},
}
routes = {
"GET /my-api": RouteConfig(
accepts=[PaymentOption(
scheme="exact",
network=ALGORAND_TESTNET,
price="$0.01", # USDC — resolved per network
pay_to=os.environ["AVM_ADDRESS"], # your merchant account (.env)
)],
extensions={"bazaar": BAZAAR_EXT, "x402-merchant": MERCHANT_EXT},
),
}
app = Flask(__name__)
payment_middleware(app, routes=routes, server=server)
@app.route("/my-api")
def my_api():
return jsonify({"ok": True, "premium": "data"})Point an x402 client at your endpoint with a funded TestNet account:
// npm install @x402/fetch @x402/avm
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { toClientAvmSigner } from "@x402/avm";
import { ExactAvmScheme } from "@x402/avm/exact/client";
const ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=";
const client = new x402Client().register(ALGORAND_TESTNET, new ExactAvmScheme(
// AVM_PRIVATE_KEY: base64 64-byte key of a TestNet account with USDC (.env)
toClientAvmSigner(process.env.AVM_PRIVATE_KEY!),
));
const paidFetch = wrapFetchWithPayment(fetch, client);
const res = await paidFetch(
process.env.RESOURCE_SERVER_URL || "http://localhost:4021/my-api");
console.log(await res.json()); // paid, verified, settled — one call// npm install axios @x402/axios @x402/avm
import axios from "axios";
import { x402Client, wrapAxiosWithPayment } from "@x402/axios";
import { toClientAvmSigner } from "@x402/avm";
import { ExactAvmScheme } from "@x402/avm/exact/client";
const ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=";
const client = new x402Client().register(ALGORAND_TESTNET, new ExactAvmScheme(
// AVM_PRIVATE_KEY: base64 64-byte key of a TestNet account with USDC (.env)
toClientAvmSigner(process.env.AVM_PRIVATE_KEY!),
));
const api = wrapAxiosWithPayment(axios.create(), client);
const res = await api.get(
process.env.RESOURCE_SERVER_URL || "http://localhost:4021/my-api");
console.log(res.data); // paid, verified, settled — one call# pip install "x402-avm[httpx,avm]"
import asyncio, os
from x402 import x402Client
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.avm.exact.register import register_exact_avm_client
client = x402Client()
# your Algorand TestNet signer, built from AVM_PRIVATE_KEY (.env)
register_exact_avm_client(client, signer=...)
async def main():
async with x402HttpxClient(client) as http:
res = await http.get(
os.getenv("RESOURCE_SERVER_URL", "http://localhost:4021/my-api"))
print(await res.aread()) # paid, verified, settled — one call
asyncio.run(main())# pip install "x402-avm[requests,avm]"
import os
from x402 import x402ClientSync
from x402.http.clients.requests import x402_requests
from x402.mechanisms.avm.exact.register import register_exact_avm_client
client = x402ClientSync()
# your Algorand TestNet signer, built from AVM_PRIVATE_KEY (.env)
register_exact_avm_client(client, signer=...)
with x402_requests(client) as session:
res = session.get(
os.getenv("RESOURCE_SERVER_URL", "http://localhost:4021/my-api"))
print(res.text) # paid, verified, settled — one callShipping a dApp where the buyer pays from their own wallet? Don't touch private
keys at all — the ClientAvmSigner interface (address +
signTransactions) is designed to match wallet libraries like
@txnlab/use-wallet, so the connected wallet drops straight in:
// Building a browser dApp? Skip raw keys — the user's wallet plays the signer.
// ClientAvmSigner is compatible with @txnlab/use-wallet out of the box:
import { useWallet } from "@txnlab/use-wallet-react";
// Browser builds (e.g. Vite) need Node's Buffer shimmed ONCE at app entry,
// or payment creation fails with "Buffer is not defined":
// npm install buffer — then, first thing in your entry file:
// import { Buffer } from "buffer";
// if (!globalThis.Buffer) globalThis.Buffer = Buffer;
const { activeAddress, signTransactions } = useWallet();
const client = new x402Client().register(ALGORAND_TESTNET, new ExactAvmScheme({
address: activeAddress!, // the connected wallet (Pera, Lute, ...)
signTransactions, // the wallet signs — keys never leave it
}));
// ...then wrapFetchWithPayment(fetch, client) exactly as aboveThe client-web example wires this end-to-end — the
WalletManager setup, a Lute connect UI, and the browser Buffer shim a
Vite build needs.
Algorand wallets and tools hand you a 25-word mnemonic; x402 signers take
AVM_PRIVATE_KEY — the same key base64-encoded as 64 bytes (32-byte seed +
32-byte public key). Convert once with this one-liner (needs only node + npm, installs nothing
into your project) and put the output in your env — a valid key is exactly 88 characters:
AVM_MNEMONIC="your 25 words here" npx -y -p algosdk node -e 'const p=process.env.PATH.split(require("path").delimiter)[0].replace(/[\/\\]\.bin$/,"");const a=require(p+"/algosdk");console.log(Buffer.from(a.mnemonicToSecretKey(process.env.AVM_MNEMONIC).sk).toString("base64"))'And the reverse — turn an AVM_PRIVATE_KEY back into its 25-word
mnemonic (to import the account into Pera/Lute, or to round-trip-check your conversion —
feeding the words back through the converter above returns the identical key):
AVM_PRIVATE_KEY="your-88-char-base64-key" npx -y -p algosdk node -e 'const p=process.env.PATH.split(require("path").delimiter)[0].replace(/[\/\\]\.bin$/,"");const a=require(p+"/algosdk");console.log(a.secretKeyToMnemonic(new Uint8Array(Buffer.from(process.env.AVM_PRIVATE_KEY,"base64"))))'That first successful payment auto-catalogs your endpoint into the Bazaar
— no registration step, as long as your route config includes the bazaar
extension from step 2 (that declaration is what the catalog reads; without it you still
get paid but stay unlisted). Within a minute you'll appear in
/discovery/resources, and the dashboard starts tracking your settles, success rate
and volume.
x402-merchant extension is not required — declare it only if you want to
control how you appear: name, website, logo and
categories are then read straight from it. Leave it out and those are derived from
your endpoint's domain metadata instead — OpenGraph tags, llms.txt or an
agent-card.json on your site — which is how most listings get their name, logo and
description today. Either way you're listed; the extension just takes the guesswork out (and is
the only way to set your categories).
Paste your endpoint and the Doctor probes its live 402 from the
facilitator's side — 402-first behavior, v2 challenge shape, CORS headers, network and payTo
sanity, plus the bazaar and x402-merchant extensions validated with the
same gate the catalog uses. If a BAZAAR or HACKATHON badge isn't showing for your
merchant, this tells you exactly why. Free, no payment involved, 20 checks per day.
Switch the network id and point AVM_ADDRESS at your MainNet
merchant account — everything else stays identical:
Add one field to your MainNet route config — the facilitator labels every settle with the Challenge tag (labeling only; never affects the payment):
accepts: [{
scheme: "exact",
network: ALGORAND_MAINNET,
price: "$0.01",
payTo: process.env.AVM_ADDRESS!, // now your MainNet merchant account (.env)
extra: { tag: "x402-global-challenge" }, // ← the Challenge tag
}],accepts=[PaymentOption(
scheme="exact",
network=ALGORAND_MAINNET,
# on Python the tag travels in the price extra (spec form):
price=AssetAmount(asset="31566704", amount="10000", # $0.01 MainNet USDC
extra={"decimals": 6, "tag": "x402-global-challenge"}),
pay_to=os.environ["AVM_ADDRESS"], # now your MainNet merchant account (.env)
)],Tagged traffic shows up under SOURCE → X402-GLOBAL-CHALLENGE on the live dashboard — your settles, volume and success rate, visible to everyone including the judges.
x402-global-challenge — visible on the dashboard./api/receipt/<txId>.Every sample on this page as a standalone, runnable project — two servers and
two clients per platform, wired to this facilitator on TestNet, config via .env:
Stuck on an integration, found a rough edge, or have an idea — for the facilitator, the Bazaar or the Challenge? Drop it in the GoPlausible OpenBox — every submission is read.