Paywall vs proxy
When you publish an endpoint, you pick a mode field. Two values are
supported. The choice affects the take rate, the network path of the
request, and the level of integration on your side.
#Comparison
| | Paywall (3 %) | Proxy (4 %) |
|--------------------------|-----------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| Take rate | 3 % of the call price | 4 % of the call price |
| Network path | Buyer ↔ your URL (direct after 402 dance) | Buyer ↔ tools402 ↔ your URL (we relay each call) |
| What tools402 returns | An Ed25519-signed JWT (60 s TTL) the buyer presents to your URL | The proxied response from your URL, augmented with X-Tools402-Tx header |
| Your verification step | Verify the JWT signature (see code sample below) | None — we authenticated the payment before relaying |
| Body size limit | None (your infra) | 50 MB body, 30 s timeout (our relay limits) |
| Visibility on us | We see only the 402 dance + the JWT-mint moment. Your responses don't pass us. | Every request + response transits our infra (in memory, not stored) |
| Best for | Sellers who want lowest take rate and don't mind a verify-JWT step | Sellers who want zero infra setup, cheap endpoints, or no JWT verification code |
#Paywall mode — flow
buyer ──POST──→ tools402 (/v1/<your-slug>/<path>)
←─402 quote─
buyer ──pays USDC on-chain──→
buyer ──POST + X-Payment──→ tools402
←─{ jwt, upstream_url, expires_in_seconds: 60 }─
buyer ──POST + Authorization: Bearer <jwt>──→ YOUR-URL (direct)
←─your 200 OK response (you never see tools402)─You verify the JWT on your side. Reference Ed25519 public key (rotates, always live at) :
https://tools402.dev/.well-known/audit-pubkey.pemVerification snippet (Node + jose) :
import { jwtVerify, importSPKI } from "jose";
const pubkeyPem = await (await fetch("https://tools402.dev/.well-known/audit-pubkey.pem")).text();
const key = await importSPKI(pubkeyPem, "EdDSA");
export async function verifyToolsToken(jwt: string) {
const { payload } = await jwtVerify(jwt, key, { algorithms: ["EdDSA"] });
// payload = { iss: "tools402", endpoint, payer, amount_atomic, iat, exp }
return payload;
}The JWT TTL is 60 seconds — short enough that a stolen JWT is unusable
after a minute. If the buyer's clock drifts, they pay again. The JWT's
endpoint field MUST match the URL they're calling on your side.
#Proxy mode — flow
buyer ──POST──→ tools402 (/v1/<your-slug>/<path>)
←─402 quote─
buyer ──pays USDC on-chain──→
buyer ──POST + X-Payment──→ tools402
tools402 ──POST + X-Tools402-Tx──→ YOUR-URL
←─your response─
←─your response (proxied)─Limits :
- Body size :
50 MB(request OR response). Larger requests fail with413 Payload Too Large. - Timeout :
30 sfrom tools402 → your URL. Longer responses fail with504 Gateway Timeout. - 5xx upstream : if your URL returns 5xx, tools402 returns
502 Bad Gatewayand the buyer's payment is logged to Sentry but not refunded. Set up your monitoring so this never happens — the marketplace flips your listing todegradedif 5xx rate exceeds 50 % over 10 minutes.
#Pick paywall if
- You already have HTTPS hosting and don't want another layer
- Your endpoint streams large responses or processes long-running jobs (no 30 s cap, no 50 MB cap on your side)
- You want the lowest take rate ($0.0003 vs $0.0004 on a $0.01 call — adds up at scale)
- You're comfortable adding a tiny JWT verifier (~5 lines of
jose)
#Pick proxy if
- You want zero seller-side code beyond the upstream URL
- Your endpoint is static / cheap / fast (well under 50 MB and 30 s)
- You don't want to deal with JWT verification logic
- You're testing the marketplace and want the simplest path
You can switch modes later by updating the endpoint (signed by your wallet).