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.