Integration Summary
Client Must
- Detect `HTTP 402` from protected endpoint.
- Parse `Payment-Required` payload and supported method.
- Generate Permit2 signature matching exact requirement.
- Retry request with `Payment-Signature` header.
Store Must
- Issue x402 payment requirement with exact terms.
- Validate signature payload and accepted requirement.
- Call facilitator `/settle` for execution.
- Release resource only on successful settlement.
Facilitator Must
- Sponsor gas for supported settlement path.
- Execute Permit2 settlement transaction.
- Enforce proxy codehash allowlist.
- Expose reliable health/supported/settle APIs.
Payload Schemas (x402 v2 + Permit2)
`Payment-Required` and `Payment-Signature` are base64-encoded JSON headers in this beta branch.
// TypeScript shapes used by this stack
type AcceptRequirement = {
scheme: "exact";
network: "eip155:42793";
amount: string;
payTo: string;
asset: string;
maxTimeoutSeconds: number;
extra?: {
assetTransferMethod?: "permit2";
name?: string;
decimals?: number;
};
};
type PaymentRequired = {
x402Version: 2;
accepts: AcceptRequirement[];
resource?: {
url: string;
mimeType?: string;
description?: string;
};
error?: string;
};
type PaymentSignature = {
x402Version: 2;
scheme: "exact";
network: "eip155:42793";
accepted: AcceptRequirement;
payload: {
signature: string;
permit2Authorization: {
from: string;
permitted: { token: string; amount: string };
spender: string;
nonce: string;
deadline: string;
witness: { to: string; validAfter: string; extra: string };
};
};
};
// Permit2 typed data sent to signer.signTypedData(...)
const domain = {
name: "Permit2",
chainId: 42793,
verifyingContract: "0x000000000022D473030F116dDEE9F6B43aC78BA3"
};
const types = {
PermitWitnessTransferFrom: [
{ name: "permitted", type: "TokenPermissions" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "witness", type: "Witness" }
],
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" }
],
Witness: [
{ name: "to", type: "address" },
{ name: "validAfter", type: "uint256" },
{ name: "extra", type: "bytes" }
]
};
Role Example: Client Integrator (TypeScript + ethers v6)
import { ethers } from "ethers";
const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
const X402_PROXY = "0xB6FD384A0626BfeF85f3dBaf5223Dd964684B09E";
const CHAIN_ID = 42793;
const toBase64 = (obj: unknown) => {
const bytes = new TextEncoder().encode(JSON.stringify(obj));
return btoa(String.fromCharCode(...bytes));
};
const fromBase64 = (value: string) => {
const bin = atob(value);
const bytes = Uint8Array.from(bin, (ch) => ch.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
};
export async function payAndRetry(protectedUrl: string, signer: ethers.Signer) {
const first = await fetch(protectedUrl);
if (first.status !== 402) {
return first.json();
}
const required = fromBase64(first.headers.get("Payment-Required") || "");
const accepted = required.accepts[0];
if (!accepted || accepted.extra?.assetTransferMethod !== "permit2") {
throw new Error("No supported permit2 requirement");
}
const now = Math.floor(Date.now() / 1000);
const deadline = BigInt(now + Number(accepted.maxTimeoutSeconds || 60));
const validAfter = BigInt(now);
const nonce = ethers.toBigInt(ethers.hexlify(ethers.randomBytes(32)));
const permit2Authorization = {
from: await signer.getAddress(),
permitted: { token: accepted.asset, amount: String(accepted.amount) },
spender: X402_PROXY,
nonce: nonce.toString(),
deadline: deadline.toString(),
witness: { to: accepted.payTo, validAfter: validAfter.toString(), extra: "0x" }
};
const signature = await signer.signTypedData(
{ name: "Permit2", chainId: CHAIN_ID, verifyingContract: PERMIT2_ADDRESS },
{
PermitWitnessTransferFrom: [
{ name: "permitted", type: "TokenPermissions" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "witness", type: "Witness" }
],
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" }
],
Witness: [
{ name: "to", type: "address" },
{ name: "validAfter", type: "uint256" },
{ name: "extra", type: "bytes" }
]
},
{
permitted: { token: accepted.asset, amount: BigInt(String(accepted.amount)) },
spender: X402_PROXY,
nonce,
deadline,
witness: { to: accepted.payTo, validAfter, extra: "0x" }
}
);
const paymentPayload = {
x402Version: 2,
scheme: "exact",
network: "eip155:42793",
accepted,
payload: { signature, permit2Authorization }
};
const paid = await fetch(protectedUrl, {
headers: {
"Payment-Signature": toBase64(paymentPayload),
"X-GAS-PAYER": "facilitator"
}
});
if (!paid.ok) {
throw new Error(`Paid request failed: ${paid.status}`);
}
return paid.json();
}
Role Example: Store Operator (Python / FastAPI)
import base64
import json
import os
import requests
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
FACILITATOR_URL = os.environ["FACILITATOR_URL"]
PAY_TO = os.environ["SERVER_WALLET"]
TOKEN = os.getenv("BBT_TOKEN", "0x7EfE4bdd11237610bcFca478937658bE39F8dfd6")
PROXY = os.getenv("X402_EXACT_PERMIT2_PROXY_ADDRESS", "0xB6FD384A0626BfeF85f3dBaf5223Dd964684B09E")
AMOUNT = "10000000000000000" # 0.01 token at 18 decimals
def b64_json_encode(obj: dict) -> str:
raw = json.dumps(obj, separators=(",", ":")).encode("utf-8")
return base64.b64encode(raw).decode("utf-8")
def b64_json_decode(value: str) -> dict:
return json.loads(base64.b64decode(value).decode("utf-8"))
def payment_required(resource_url: str) -> dict:
return {
"x402Version": 2,
"accepts": [{
"scheme": "exact",
"network": "eip155:42793",
"amount": AMOUNT,
"payTo": PAY_TO,
"asset": TOKEN,
"maxTimeoutSeconds": 60,
"extra": {"assetTransferMethod": "permit2"}
}],
"resource": {
"url": resource_url,
"mimeType": "application/json",
"description": "Paid weather response"
}
}
@app.get("/api/weather")
def weather(
payment_signature: str | None = Header(default=None, alias="Payment-Signature"),
x_facilitator_url: str | None = Header(default=None, alias="X-Facilitator-Url")
):
req = payment_required("https://your-store.example/api/weather")
effective_facilitator = x_facilitator_url or FACILITATOR_URL
if not payment_signature:
raise HTTPException(
status_code=402,
detail="Payment required",
headers={
"Payment-Required": b64_json_encode(req),
"X-Facilitator-Url": effective_facilitator
}
)
payload = b64_json_decode(payment_signature)
accepted = payload.get("accepted", {})
permit2_auth = payload.get("payload", {}).get("permit2Authorization", {})
if accepted.get("payTo", "").lower() != PAY_TO.lower():
raise HTTPException(status_code=402, detail="Recipient mismatch")
if accepted.get("asset", "").lower() != TOKEN.lower():
raise HTTPException(status_code=402, detail="Asset mismatch")
if str(accepted.get("amount")) != AMOUNT:
raise HTTPException(status_code=402, detail="Amount mismatch")
if permit2_auth.get("spender", "").lower() != PROXY.lower():
raise HTTPException(status_code=402, detail="Invalid spender")
settle_body = {
"x402Version": 2,
"paymentPayload": payload,
"paymentRequirements": accepted
}
settle = requests.post(f"{effective_facilitator}/settle", json=settle_body, timeout=20)
if settle.status_code >= 400:
raise HTTPException(status_code=402, detail=f"Facilitator reject: {settle.text}")
return {"ok": True, "settlement": settle.json()}
Role Example: Facilitator Host Ops Check (Python)
import json
import requests
FACILITATOR_URL = "https://exp-faci.bubbletez.com"
def require_ok(resp: requests.Response) -> dict:
resp.raise_for_status()
if not resp.text:
return {}
return resp.json()
def smoke_check():
health = require_ok(requests.get(f"{FACILITATOR_URL}/health", timeout=10))
supported = require_ok(requests.get(f"{FACILITATOR_URL}/supported", timeout=10))
print("health:", json.dumps(health, indent=2))
print("supported:", json.dumps(supported, indent=2))
# For verify/settle testing, send:
# {
# "x402Version": 2,
# "paymentPayload": {...decoded Payment-Signature...},
# "paymentRequirements": {...accepted requirement...}
# }
if __name__ == "__main__":
smoke_check()