402tools402 docs
docs · live
Sign once → get paid forever (full code, TypeScript + viem)

One file, ~90 lines. Save as index.ts, edit the 5 lines marked // TODO change me, run bun add viem && bun run index.ts. The script generates a wallet (or reuses your saved .tools402-seller.pk), registers your slug, publishes your endpoint, prints the live URL. Idempotent — safe to re-run if anything fails.

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}`);

Both API calls fire within ~50 ms each. The endpoint flips from pending to active as soon as three regional probes return 200 OK with a valid JSON body and p95 latency < 5 s. From bun run to first billable call: typically < 30 seconds. Add .tools402-seller.pk to your .gitignore immediately — that file holds the private key that owns your slug and receives your USDC settlements.