402tools402 docs
docs · live

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.

bash
# 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 …" }

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).

typescript
// 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).

Per-chain wallet addresses + facilitator stacks (9 facilitators × 3 chains, independent failover) : /chains/overview · /reference/facilitators.