Quickstart
This single page covers both sides of the marketplace and the chains it runs on. Pick a section, copy the code, run it.
#Pay an endpoint (buyer)
You want your agent to call an HTTP API and pay per call.
# 1 · ask the endpoint, get a 402 quote
curl -i -X POST https://api.tools402.dev/v1/pdf-md \
-F "file=@receipt.pdf"
# → HTTP/1.1 402 Payment Required
# → { "accepts": [{"network":"base","payTo":"0xD6E8…2878","maxAmountRequired":"10000",…}], … }
# 2 · pay 0.010 USDC on Base
cast send 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
"transfer(address,uint256)" \
0xD6E8aF2F65B4C9ACC7BF14A3096056e89E312878 10000 \
--rpc-url https://mainnet.base.org
# 3 · retry with X-Payment receipt
curl -X POST https://api.tools402.dev/v1/pdf-md \
-H 'X-Payment: eyJ4NDAyVi…ifQ' \
-F "file=@receipt.pdf"
# → 200 OK · { "markdown": "# Receipt …" }import { createWalletClient, http } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });
// 1 · ask the endpoint, get a 402 quote
const body = new FormData();
body.append("file", new Blob([await Bun.file("receipt.pdf").arrayBuffer()]));
const r1 = await fetch("https://api.tools402.dev/v1/pdf-md", { method: "POST", body });
const quote = (await r1.json()).accepts.find((a: any) => a.network === "base");
// 2 · pay USDC ERC-20 transfer
const hash = await wallet.writeContract({
address: quote.asset,
abi: [{ name: "transfer", type: "function", inputs: [{ name: "to", type: "address" }, { name: "v", type: "uint256" }], outputs: [{ name: "ok", type: "bool" }] }],
functionName: "transfer",
args: [quote.payTo, BigInt(quote.maxAmountRequired)],
});
// 3 · retry with X-Payment receipt (base64url-encoded JSON with tx hash)
const xPayment = Buffer.from(JSON.stringify({
x402Version: 1, scheme: "exact", network: "base", payload: { tx_hash: hash },
})).toString("base64url");
const r2 = await fetch("https://api.tools402.dev/v1/pdf-md", {
method: "POST", body, headers: { "X-Payment": xPayment },
});
console.log(await r2.json()); // { markdown: "# Receipt …" }import os, requests, eth_account, json, base64
from web3 import Web3
acct = eth_account.Account.from_key(os.environ["PRIVATE_KEY"])
w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org"))
# 1 · ask the endpoint, get a 402 quote
with open("receipt.pdf", "rb") as f:
r1 = requests.post("https://api.tools402.dev/v1/pdf-md", files={"file": f})
quote = next(a for a in r1.json()["accepts"] if a["network"] == "base")
# 2 · pay USDC ERC-20 transfer
usdc = w3.eth.contract(address=quote["asset"], abi=[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"v","type":"uint256"}],"outputs":[{"name":"ok","type":"bool"}]}])
tx = usdc.functions.transfer(quote["payTo"], int(quote["maxAmountRequired"])).build_transaction({
"from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
})
signed = acct.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction).hex()
# 3 · retry with X-Payment receipt
x_payment = base64.urlsafe_b64encode(json.dumps({
"x402Version": 1, "scheme": "exact", "network": "base",
"payload": {"tx_hash": tx_hash},
}).encode()).rstrip(b"=").decode()
with open("receipt.pdf", "rb") as f:
r2 = requests.post("https://api.tools402.dev/v1/pdf-md", files={"file": f},
headers={"X-Payment": x_payment})
print(r2.json()) # { "markdown": "# Receipt …" }Full walkthroughs : /buy-side/curl · /buy-side/typescript · /buy-side/python · /buy-side/the-402-dance for the full wire protocol with failure modes.
#Publish an endpoint (seller)
You host an HTTPS API. You want to monetise it in USDC, with no platform fee beyond the 3 % paywall or 4 % proxy take rate.
#Save the snippet
Save the code block below as index.ts. It requires bun (or Deno).
#Edit 5 lines
Change SLUG, PATH_SUFFIX, UPSTREAM_URL, ATOMIC_PRICE, DESC to match
your endpoint.
#Run
bun add viem && bun run index.ts — your endpoint is live in ~10 seconds.
Idempotent : safe to re-run if anything fails (PK persisted, register +
add_endpoint both check /v1/_meta first).
// requires: viem 2.x (run: bun add viem)
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { keccak256, toBytes } from "viem";
// ─── TODO change me — 5 lines ─────────────────────────────────────────────
const SLUG = "your-slug"; // ^[a-z0-9-]{3,32}$ — globally unique
const PATH_SUFFIX = "your-path"; // ^[a-z0-9-]{3,64}$ — suffix after /v1/<slug>/
const UPSTREAM_URL = "https://your-api.example.com/x"; // your live HTTPS endpoint (must return 200 + JSON)
const ATOMIC_PRICE = 1000; // INT, atomic USDC units — 1000 = $0.001
const DESC = "What your endpoint does (max 200 chars).";
// ──────────────────────────────────────────────────────────────────────────
// Constants — do not edit (source: src/lib/seller-auth.ts:19,25)
const API = "https://api.tools402.dev";
const DOMAIN = { name: "tools402", version: "1", chainId: 8453 } as const;
const TYPES = {
SellerAction: [
{ name: "wallet", type: "address" },
{ name: "action", type: "string" },
{ name: "payloadHash", type: "bytes32" },
{ name: "timestamp", type: "uint256" },
],
} as const;
// ─── PK : load from .tools402-seller.pk (gitignored) or generate ─────────
const PK_FILE = ".tools402-seller.pk";
let pk: `0x${string}`;
if (existsSync(PK_FILE)) {
pk = readFileSync(PK_FILE, "utf-8").trim() as `0x${string}`;
} else {
pk = generatePrivateKey();
writeFileSync(PK_FILE, pk, { mode: 0o600 });
console.log(`[onboard] new wallet saved to ${PK_FILE} — add this file to .gitignore, never commit`);
}
const account = privateKeyToAccount(pk);
const WALLET = account.address;
console.log(`[onboard] wallet: ${WALLET}`);
// ─── Idempotence : check /v1/_meta before each mutating call ─────────────
const meta = await fetch(`${API}/v1/_meta`).then((r) => r.json()) as {
endpoints: { path: string; seller: string }[];
};
const alreadySeller = meta.endpoints.some((e) => e.seller === WALLET);
const targetPath = `/v1/${SLUG}/${PATH_SUFFIX}`;
const alreadyPublished = meta.endpoints.some((e) => e.path === targetPath);
// ─── 1 · Register slug (skipped if wallet is already a seller) ──────────
if (!alreadySeller) {
const ts1 = Math.floor(Date.now() / 1000);
const sig1 = await account.signTypedData({
domain: DOMAIN, types: TYPES, primaryType: "SellerAction",
message: { wallet: WALLET, action: "register", payloadHash: keccak256(toBytes(SLUG)), timestamp: BigInt(ts1) },
});
const r1 = await fetch(`${API}/v1/_seller/register`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ wallet: WALLET, slug: SLUG, signature: sig1, timestamp: ts1 }),
});
if (!r1.ok) {
console.error(`[onboard] register failed (${r1.status}):`, await r1.json());
console.error(`[onboard] wallet still saved in ${PK_FILE} — fix and re-run`);
process.exit(1);
}
console.log(`[onboard] register: OK — slug "${SLUG}" is yours`);
} else { console.log(`[onboard] register: skipped (wallet already a seller)`); }
// ─── 2 · Add endpoint (skipped if path already published) ───────────────
// Schema canonical — source: src/routes/_seller/endpoints.ts:37 (6 fields, atomic_price is NUMBER)
if (!alreadyPublished) {
const endpoint = {
path_suffix: PATH_SUFFIX,
upstream_url: UPSTREAM_URL,
atomic_price: ATOMIC_PRICE,
unit: "call",
desc: DESC,
mode: "paywall", // "paywall" (3 %) or "proxy" (4 %)
};
const ts2 = Math.floor(Date.now() / 1000);
const sig2 = await account.signTypedData({
domain: DOMAIN, types: TYPES, primaryType: "SellerAction",
message: { wallet: WALLET, action: "add_endpoint", payloadHash: keccak256(toBytes(JSON.stringify(endpoint))), timestamp: BigInt(ts2) },
});
const r2 = await fetch(`${API}/v1/_seller/${WALLET}/endpoints`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ ...endpoint, signature: sig2, timestamp: ts2 }),
});
if (!r2.ok) {
console.error(`[onboard] add_endpoint failed (${r2.status}):`, await r2.json());
console.error(`[onboard] slug "${SLUG}" still yours, wallet still in ${PK_FILE} — fix and re-run`);
process.exit(1);
}
console.log(`[onboard] add_endpoint: OK`);
} else { console.log(`[onboard] add_endpoint: skipped (${targetPath} already published)`); }
// ─── 3 · Done — your endpoint is live ──────────────────────────────────
console.log(`[onboard] ✅ live: ${API}${targetPath}`);
console.log(`[onboard] test: curl -X POST ${API}${targetPath} -H "content-type: application/json" -d '{}' # → 402 + x402 quote`);
console.log(`[onboard] daily settlement at 00:00 UTC → USDC to ${WALLET}`);Full walkthrough : /sell-side/onboarding · /sell-side/paywall-vs-proxy · /sell-side/settlement · /sell-side/manage.
#Same dev, two rails
If your endpoint already accepts Stripe for human customers, keep it. tools402 is not a Stripe replacement — it's the second rail that lets the same endpoint accept both customer segments. Stripe handles your human checkout (cards, your KYC, your dashboard). tools402 handles the agent rail (wallet-only, no KYC, USDC settlement). The same business logic, two payment surfaces, one developer.
#The three chains
tools402 settles on three chains. All three carry native USDC issued by Circle — no bridged USDC.e. A seller picks one wallet per chain it wants to receive on; the buyer picks one chain it wants to pay on; the marketplace bridges between the two if necessary (cross-chain settlement V1, Mayan-only).
Base
EVM L2 by Coinbase · ~$0.005 gas · 1-block finality (~1.2 s) · native USDC 0x833589…2913 · scheme exact (EIP-3009 gasless)
Polygon
EVM PoS · ~$0.001 gas (cheapest) · ~2 s finality · native USDC 0x3c499c…3359 · scheme exact
Solana
SPL token · ~$0.00025 fee (lowest) · sub-second finality · USDC mint EPjFW…Dt1v · scheme spl-transfer (partial-sign)
Per-chain wallet addresses + facilitator stacks (9 facilitators × 3 chains, independent failover) : /chains/overview · /reference/facilitators.
#What to read next
- New to HTTP 402 ? → /buy-side/the-402-dance
- Want to publish ? → /sell-side/overview
- Curious about uptime + failover ? → /reference/facilitators
- Browse the live catalog → api.tools402.dev/v1/_meta