Docs/Sell-side
sell-side · seller guide

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.

Agent → tools402 [pays USDC, gets {jwt, upstream_url}] Agent → Your server [Authorization: Bearer <jwt>] └─ You verify JWT (see below) └─ You respond directly to the agent

Proxy — 4% take rate

Zero JWT code on your side. tools402 relays the request and adds tracing headers.

Agent → tools402 [pays USDC] tools402 → Your upstream [full request relay] └─ Your server responds tools402 → Agent [forwards response]

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

EIP-712 domain

json
{"name": "tools402", "version": "1", "chainId": 8453}

Typed data — action register

payloadHash = keccak256(utf8Bytes(slug)). Timestamp window: [now − 300s, now + 60s] (Unix seconds).

bash · curl
# 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";} }
python · eth_account + requests
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:

json · canonical 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"}

atomic_price is integer USDC units (6 decimals): 5000 = $0.005. Action for signature: add_endpoint.

bash · curl
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
javascript · fetch
// 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.

Monitor stats (signed URL)

GET /v1/_seller/<wallet>/stats?sig=&ts= — EIP-712 action stats, payloadHash = keccak256("").

bash · curl
# 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

Payload fields

json
{"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.

javascript · jose
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");
python · PyJWT + cryptography
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).

MethodPathAuthNotes
POST/registerEIP-712Claim slug · free
POST/:wallet/endpointsEIP-712Add endpoint · probe-gated
DELETE/:wallet/endpoints/:idEIP-712Soft-delete (enabled=0)
GET/:wallet/statssig query5 analytics categories
POST/reportnoneCommunity report (≥5/24h → suspend)
GET/countnonePublic seller count
GET/reversed-totalnonePublic USDC reversed to date

Common errors: invalid_signature, slug_taken, wallet_already_registered, probe_failed, upstream_unreachable.