Wrap, register, get paid daily.
Publish any REST endpoint at /v1/<slug>/<path>. Buyers pay in USDC via x402 on Base, Polygon, or Solana. Choose your payout chain (Base, Polygon, Arbitrum, Optimism, Avalanche, or Solana) — paid in USDC there; cross-chain handled via CCTP. Settlement runs daily at 00:00 UTC, in USDC. A balance ≥ 1 USDC is paid at the next run (within 24h). A balance below 1 USDC carries over and accumulates across days until it reaches 1 USDC, then it's paid. Identity = EIP-712 wallet signature. No package install required — raw HTTP + standard crypto libs only.
Quick intro: /docs/sell-side/intro · Product overview: /publish
Wrap your endpoint
Choose paywall or proxy per endpoint. Mix modes across your portfolio.
Paywall — 3% take rate
tools402 is out of the data path after payment. The agent calls your server directly with a 60-second JWT.
Proxy — 4% take rate
Zero JWT code on your side. tools402 relays the request and adds tracing headers.
Proxy responses include X-Tools402-Tx (on-chain tx hash) and X-Tools402-Caller (buyer wallet).
Sign the EIP-712 register
Registration links your wallet to a globally unique slug (EIP-712 on Base for the register signature). Set payout_chain to choose where you receive USDC — Base, Polygon, Arbitrum, Optimism, Avalanche, or Solana. Every endpoint you publish lives at /v1/<slug>/<path_suffix>. Free — no x402 payment.
payout_chain (required)
One of base, polygon, arbitrum, optimism, avalanche, or solana. Choose where you receive USDC; cross-chain from buyer payments is handled via CCTP at daily settlement.
Slug rules
- Pattern:
^[a-z0-9-]{3,32}$ - Globally unique; immutable once registered
EIP-712 domain
{"name": "tools402", "version": "1", "chainId": 8453}
Typed data — action register
payloadHash = keccak256(utf8Bytes(slug)). Timestamp window: [now − 300s, now + 60s] (Unix seconds).
# Sign SellerAction off-line (cast wallet sign-typed-data, eth_account, etc.) # then POST: curl -X POST https://api.tools402.dev/v1/_seller/register \ -H 'Content-Type: application/json' \ -d '{"wallet": "0xYOUR_WALLET", "slug": "your-slug", "payout_chain": "base", "signature": "0xEIP712_SIG_65_BYTES", "timestamp": 1747310400}' # → 200 { "ok": true, "seller": {"wallet", "slug", "status": "active", "created_at";} }
import os, time, requests from eth_account import Account from eth_account.messages import encode_typed_data from web3 import Web3 account = Account.from_key(os.environ["PRIVATE_KEY"]) slug = "your-slug" ts = int(time.time()) payload_hash = Web3.keccak(text=slug) msg = encode_typed_data( domain_data={"name": "tools402", "version": "1", "chainId": 8453;}, message_types={"SellerAction": [ {"name": "wallet", "type": "address";}, {"name": "action", "type": "string";}, {"name": "payloadHash", "type": "bytes32";}, {"name": "timestamp", "type": "uint256";}, ]}, message_data={"wallet": account.address, "action": "register", "payloadHash": payload_hash, "timestamp": ts,;}, ) sig = account.sign_message(msg).signature.hex() r = requests.post("https://api.tools402.dev/v1/_seller/register", json={"wallet": account.address, "slug": slug, "payout_chain": "base", "signature": sig, "timestamp": ts,;}) print(r.status_code, r.json())
Set a price · add endpoint
After registration, publish an upstream URL and atomic USDC price. Listed in /v1/_meta within ~10 seconds if regional probes pass (HTTP 200, valid JSON, p95 < 5s).
payloadHash = keccak256(canonicalJSON) where canonical JSON is exact key order:
{"path_suffix": "translate",
"upstream_url": "https://your-api.example.com/v1/translate",
"atomic_price": 5000,
"unit": "call",
"desc": "Translate text to any language.",
"mode": "paywall"}
atomic_price is integer USDC units (6 decimals): 5000 = $0.005. Action for signature: add_endpoint.
curl -X POST https://api.tools402.dev/v1/_seller/0xYOUR_WALLET/endpoints \ -H 'Content-Type: application/json' \ -d '{"path_suffix": "translate", "upstream_url": "https://your-api.example.com/v1/translate", "atomic_price": 5000, "unit": "call", "desc": "Translate text to any language.", "mode": "paywall", "signature": "0xEIP712_SIG", "timestamp": 1747310400;}' # Live path: /v1/your-slug/translate
// Sign add_endpoint off-line (same SellerAction types, action = "add_endpoint") const WALLET = "0xYOUR_WALLET"; const body = {path_suffix: "translate", upstream_url: "https://your-api.example.com/v1/translate", atomic_price: 5000, unit: "call", desc: "Translate text to any language.", mode: "paywall", signature: SIG, timestamp: Math.floor(Date.now() / 1000),;}; const res = await fetch( `https://api.tools402.dev/v1/_seller/${WALLET;}/endpoints`, { method: "POST", headers: {"Content-Type": "application/json";}, body: JSON.stringify(body) }, ); console.log(res.status, await res.json());
Multi-chain buyers: optional accepted_chains column on seller endpoints narrows which chains appear in the x402 quote. See multi-chain docs.
Settlement · USDC on your payout chain
Choose your payout chain (Base, Polygon, Arbitrum, Optimism, Avalanche, or Solana) — paid in USDC there; cross-chain handled via CCTP. Settlement runs daily at 00:00 UTC, in USDC. A balance ≥ 1 USDC is paid at the next run (within 24h). A balance below 1 USDC carries over and accumulates across days until it reaches 1 USDC, then it's paid. USDC collected since the last run is sent to your wallet on your declared payout_chain minus the take rate (3% paywall / 4% proxy). Gas on payout is absorbed by tools402.
- Dust threshold: balances < 1 USDC (1,000,000 atomic) roll to the next day until cumulative balance ≥ 1 USDC
- Asset: native USDC on your
payout_chain(Circle-issued contracts per chain — see multi-chain docs) - Every payout is on-chain — verify on Basescan ↗
Monitor stats (signed URL)
GET /v1/_seller/<wallet>/stats?sig=&ts= — EIP-712 action stats, payloadHash = keccak256("").
# Sign stats action off-line, then: curl "https://api.tools402.dev/v1/_seller/0xYOUR_WALLET/stats?sig=0xSIG&ts=1747310400" # Public aggregates (no signature): curl https://api.tools402.dev/v1/_seller/count curl https://api.tools402.dev/v1/_seller/reversed-total
Verify the paywall JWT
Paywall mode only. When a caller pays, tools402 returns a signed JWT plus your upstream_url. The agent calls you with Authorization: Bearer <jwt>.
JWT spec
- Algorithm: EdDSA (Ed25519)
- Valid for 60 seconds — strict, clockTolerance 0
- Public key:
GET https://api.tools402.dev/v1/_audit/pubkey
Payload fields
{"tx_hash": "0x…",
"endpoint_path": "/v1/your-slug/translate",
"expires_at": 1747310460,
"caller_wallet": "0x…",
"iat": 1747310400,
"exp": 1747310460}
Always verify endpoint_path matches the route you serve — prevents replay across paths.
import * as jose from "jose"; const pubkeyRes = await fetch("https://api.tools402.dev/v1/_audit/pubkey"); const {pubkey_pem;} = await pubkeyRes.json(); const key = await jose.importSPKI(pubkey_pem, "EdDSA"); const token = req.headers.authorization?.replace("Bearer ", ""); const {payload;} = await jose.jwtVerify(token, key, {clockTolerance: 0;}); if (payload.endpoint_path !== "/v1/your-slug/translate") throw new Error("path mismatch");
import jwt, requests from cryptography.hazmat.primitives.serialization import load_pem_public_key pem = requests.get("https://api.tools402.dev/v1/_audit/pubkey").json()["pubkey_pem"] key = load_pem_public_key(pem.encode()) payload = jwt.decode( token, key, algorithms=["EdDSA"], options={"require": ["exp", "iat", "endpoint_path"];}, ) assert payload["endpoint_path"] == "/v1/your-slug/translate"
Seller API reference
All routes under https://api.tools402.dev/v1/_seller/. Authenticated routes use EIP-712 SellerAction (window ±300s/+60s).
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /register | EIP-712 | Claim slug · free |
| POST | /:wallet/endpoints | EIP-712 | Add endpoint · probe-gated |
| DELETE | /:wallet/endpoints/:id | EIP-712 | Soft-delete (enabled=0) |
| GET | /:wallet/stats | sig query | 5 analytics categories |
| POST | /report | none | Community report (≥5/24h → suspend) |
| GET | /count | none | Public seller count |
| GET | /reversed-total | none | Public USDC reversed to date |
Common errors: invalid_signature, slug_taken, wallet_already_registered, probe_failed, upstream_unreachable.