integrating p402
p402 turns any HTTP resource into a machine-payable one. this page documents exactly what runs in this deployment — not a future spec. every endpoint, field name, error string and environment variable below is taken from the code that serves this site.
overview
a protected resource answers an unpaid request with HTTP 402 and x402 v2 requirements. the client signs a payment authorization and retries with an X-PAYMENT header holding the base64 of a json payload. the p402 facilitator verifies the authorization against chain state, settles it, and only then does the resource release its body along with an X-PAYMENT-RESPONSE receipt.
- — amounts are token base units as decimal strings, handled as bigint server side.
- — networks are CAIP-2 identifiers, e.g. solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp.
- — the protocol version is 2 in every request and response.
- — the 402 body is public: an agent can read requirements before holding any funds.
what changed in v2
v2 is an in-place upgrade. every route, endpoint, contract address, chain id, session and record from v1 keeps working exactly as before — no api was renamed or removed.
live request console
the homepage runs one real unpaid request against a published product and prints the true status, headers and 402 requirements. it stops before the wallet signature, because p402 never signs on your behalf.
live system strip
network, chain id, rpc, facilitator, allowlisted assets and the latest verified settlement are read from live checks, not written into the page.
integrations directory
every integration is labelled live, testing or coming soon based on working code and configuration in this app. a name listed there is not a partnership.
command center
the creator dashboard opens with real counts: products, live endpoints, settlements, settled base units, keys, webhooks and recent audit events for your wallet only.
/api/v2 endpoints
read-only system and catalogue descriptors, documented below. v1 routes are untouched.
quickstart
three steps: read the requirements, produce a payment payload, retry with the header. the playground runs step one live against the demo endpoints and prints copyable curl, javascript and python for whichever one you select.
# 1. read the requirements from any published product
curl -i https://p402.app/api/x402/<slug>
# -> HTTP/1.1 402 Payment Required
# x-p402-version: 2
# body: { "x402Version": 2, "accepts": [ … ], "resource": { … } }
# 2. build a payment payload for one entry of "accepts" (see payment payloads)
# 3. base64-encode it and retry the same request
curl -i https://p402.app/api/x402/<slug> \
-H "X-PAYMENT: $(printf '%s' "$PAYLOAD_JSON" | base64)"
# -> 200 with the product body and an x-payment-response receipt header
# the free demo endpoint needs no payment at all
curl -s https://p402.app/api/demo/publicnetworks
mainnet
solana mainnet · solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
api.mainnet-beta.solana.com · solscan.io
devnet
solana devnet · solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
api.devnet.solana.com · no default payment asset — set the allowlist
the mainnet payment asset is USDC at EPjFWJ5MjYRGTV6dyDuTQXM8BVvKCHp6KfmhKb7HDDBf with 6 decimals, so "1000" means 0.001 USDC. p402 never converts prices through floating point.
launching links an existing pump.fun coin: p402 checks the mint onchain and its pump.fun bonding curve (program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P) before attaching it to a product.
the 402 handshake
an unpaid request returns 402 with cache-control: no-store and x-p402-version: 2. the body carries every requirement the resource accepts; pick one and pay it exactly.
{
"x402Version": 2,
"accepts": [
{
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"asset": "0xTokenAddress",
"amount": "1000",
"payTo": "0xRecipient",
"maxTimeoutSeconds": 60
}
],
"resource": {
"url": "https://<your-app>/api/demo/weather",
"description": "Live p402 weather demo",
"mimeType": "application/json"
},
"error": "payment_required"
}after settlement the resource responds normally and adds a receipt header you can decode and pin:
x-payment-response: <base64 of>
{
"success": true,
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"transaction": "0x…",
"payer": "0x…"
}payment payloads
the X-PAYMENT header is the base64 of one of these two json shapes. anything else fails schema validation with invalid_payload.
{
"x402Version": 2,
"payload": {
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"asset": "0x5fc5360d0400a0fd4f2af552add042d716f1d168",
"authorization": {
"from": "0xPayer",
"to": "0xRecipient",
"value": "1000", // base units, decimal string
"validAfter": "0", // unix seconds
"validBefore": "1893456000",
"nonce": "0x…32 bytes"
},
"signature": "0x…"
}
}{
"x402Version": 2,
"payload": {
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"kind": "direct-transfer",
"transactionHash": "0x…32 bytes",
"from": "0xPayer",
"asset": "0x5fc5360d0400a0fd4f2af552add042d716f1d168"
}
}facilitator api
- GET /api/facilitator/health — live rpc, relayer and configuration state (503 when unavailable)
- GET /api/facilitator/supported — schemes, networks, adapters and allowlisted assets
- POST /api/facilitator/verify — validate a payment payload without settling
- POST /api/facilitator/settle — settle, record the payment and return the receipt
both POST endpoints require content-type: application/json and answer 415 otherwise. errors are sanitized: no stack traces, no internal identifiers.
POST /api/facilitator/verify
content-type: application/json
{
"paymentPayload": { "x402Version": 2, "payload": { … } },
"paymentRequirements": { "scheme": "exact", "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "asset": "0x…", "amount": "1000", "payTo": "0x…" }
}
200 { "isValid": true, "payer": "0x…" }
200 { "isValid": false, "invalidReason": "amount_too_low" }
400 { "error": "invalid_request_body" }
415 { "error": "unsupported_media_type" }
429 { "error": "rate_limited" }POST /api/facilitator/settle
{
"paymentPayload": { … },
"paymentRequirements": { … },
"projectId": "optional uuid",
"idempotencyKey": "optional string"
}
200 { "success": true, "transactionHash": "0x…", "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "payer": "0x…" }
402 { "success": false, "errorReason": "settlement_failed" }error codes
these are the exact strings returned in invalidReason (verify) and errorReason (settle).
invalid_payload
the payload or requirements failed schema validation — wrong x402Version, malformed address, or a non-decimal amount.
network_mismatch
the payload network is not the network in the requirements, or not the chain this deployment runs on.
asset_not_allowed
the token address is not in the server asset allowlist (ALLOWED_PAYMENT_ASSETS).
amount_too_low
the authorized or transferred value is below the required amount in base units.
recipient_mismatch
the payment does not pay the payTo address in the requirements.
payment_expired
the authorization window has passed, or the proof is older than PAYMENT_TIMEOUT_SECONDS.
proof_already_consumed
that nonce, authorization hash or transaction hash was already used — replay rejected.
facilitator_not_configured
the deployment has no rpc url, chain id, allowlisted asset or relayer key; nothing is faked.
settlement_failed
the transaction could not be submitted or reverted onchain.
receipt_invalid
the onchain receipt does not match the claimed sender, receiver, asset or amount.
protecting a resource
the middleware is a plain Request → Response handler, so it runs on workers, deno, bun and tanstack server routes; express and next.js wrappers ship with it. it calls verify then settle, and only runs your handler once settlement is confirmed.
import { createP402Handler } from "@/packages/p402-middleware";
export const handler = createP402Handler(
{
accepts: [
{
scheme: "exact",
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
asset: process.env.P402_ASSET,
amount: "1000", // base units, never floats
payTo: process.env.P402_RECIPIENT,
maxTimeoutSeconds: 60,
},
],
resource: { url: "https://api.example.com/report", mimeType: "application/json" },
facilitatorUrl: "https://p402.app/api/facilitator",
},
async (request, { settlement }) =>
Response.json({ report: "…", txHash: settlement.transactionHash }),
);
// express: app.use(expressP402(options))
// next.js / hono / workers: export const GET = withP402(options, handler)settlement adapters
eip-3009
transferWithAuthorization. the signed authorization is checked for expiry and nonce state before submission, then relayed by the facilitator wallet, which pays the gas.
available when the asset implements SPL transfer
permit2
signature-transfer path for assets without SPL transfer.
returns a configuration error until a verified Permit2 deployment and asset certification exist
direct transfer
the client submits its own transfer and supplies the transaction hash; p402 re-derives sender, receiver, asset and amount from the receipt logs and requires the configured minimum confirmations.
test and demo use only
product types
every launched product is served at /api/x402/<slug> over GET and POST. what gets delivered after settlement depends on the type you chose at launch.
hosted-json
p402 stores and serves the json body itself after settlement. no origin server needed.
protected-url
settlement returns the private url you configured — a signed link, a doc, a repo.
api-proxy
the request is forwarded to your origin and the response is streamed back to the payer.
agent-endpoint
same as api-proxy but forwarded as POST, for agents that take a json instruction body.
file-download
the origin file is proxied back with application/octet-stream and a download filename.
webhook-action
settlement fires a POST to your origin and returns its response — for paid actions.
a paused product answers 503 resource_paused; an unpublished or unknown slug answers 404 not_found; a product missing an asset, price or recipient answers 402 with resource_not_configured rather than pretending to be payable.
rwa attestations
a product can carry one real-world asset record. the registry entry is free to read at GET /api/public/rwa/<slug> — no payment, no personal data. when an internal signing secret is configured, the response includes an HMAC-SHA256 signature over the canonical json so a consumer can pin the attestation.
a record stays unverified until a document is fetched and its SHA-256 digest matches the digest on file. p402 never marks an asset verified on the issuer's word alone.
/api/v2 endpoints
- GET /api/v2/system — network, chain id, facilitator status and urls, allowlisted assets, explorer, min confirmations, payment timeout
- GET /api/v2/products — every published, unpaused product with its resource url, network, asset, amount in base units and payTo address
both are read-only, public, uncached and expose no private columns. they exist so an agent can discover what is payable here before it spends anything.
rate limits
limits are per client per 60 second window and answer 429 rate_limited when exceeded.
- /api/x402/<slug> — 60 requests
- /api/facilitator/verify — 60 requests
- /api/facilitator/settle — 20 requests
- /api/public/rwa/<slug> — 120 requests
security model
- — a client-supplied transaction hash is never trusted; the receipt is re-read from the chain.
- — nonces, authorization hashes and transaction hashes are stored uniquely, so a proof cannot be replayed.
- — proofs are reserved before settlement, making settle idempotent under concurrency.
- — creator origins and webhook targets are SSRF-checked: no loopback, private ranges, link-local, or metadata hosts; redirects blocked; timeouts, size caps and content-type allowlists enforced.
- — facilitator endpoints are rate limited per client and return sanitized errors.
- — wallet sign-in is a server-verified signature over a single-use nonce; a connected address alone never authenticates.
- — every settlement, launch, key and webhook change is written to an append-only audit log scoped to the owning wallet.
- — the relayer key lives only on the server. in production it should be a dedicated low-balance hot wallet behind a KMS-backed signer, and the settlement path deserves an independent review before large volume.
webhooks
each webhook endpoint gets a secret shown once. after a settlement p402 posts a payment.settled event carrying the project id, slug, transaction hash, payer, network, amount and asset. deliveries are signed with HMAC-SHA256 over the raw body and carry a timestamp; verify with a timing-safe comparison and reject stale timestamps.
X-P402-Signature: t=<unix-seconds>,v1=<hex hmac_sha256(secret, "t.<raw body>")>configuration
SOLANA_RPC_URL= # solana json-rpc (defaults to the public mainnet endpoint)
SOLANA_CLUSTER= # mainnet (default) or devnet
SOLANA_COMMITMENT= # confirmed (default) or finalized
SOLANA_RECIPIENT_ADDRESS= # payout wallet for first-party products
ALLOWED_PAYMENT_ASSETS= # comma separated SPL mints (default: USDC)
PAYMENT_TIMEOUT_SECONDS= # max age of a payment transaction, default 900
P402_INTERNAL_SIGNING_SECRET= # signs rwa attestationswhen any of these are missing, p402 shows a "not configured" state instead of a fake success — check /status for the live state of this deployment.