Implements USDC x402 payments via PayAI (EIP-3009) and DHM x402 payments via EVVM native (signed pay). Use when adding x402 payment flows, PayAI Echo integration, EVVM pay() for DHM, agent-to-agent payments with Privy, or when the user asks how to do USDC/DHM x402 in the ClawHub/NHS EVVM app.
This skill documents the two x402 payment flows in the NHS EVVM / ClawHub app: USDC via PayAI Echo and DHM via EVVM native. Reference implementation lives in this repo.
| Flow | Client UI | Server / config |
|---|---|---|
| USDC (PayAI) | frontend/src/components/sections/USDCX402TestSection.tsx | Config: frontend/src/config/contracts.ts (X402_USDC_ECHO_URL, USDC_BASE_SEPOLIA) |
| DHM (EVVM) | frontend/src/components/sections/X402TestSection.tsx | server/src/index.ts (GET 402, POST /payments/evvm/dhm) |
| EVVM sign | frontend/src/lib/evvmSign.ts | — |
Chain: Base Sepolia (chainId 84532).
PayAI returns 402 with an accepts array (not options). Client picks a USDC option, builds EIP-3009 TransferWithAuthorization, signs EIP-712, sends signature in PAYMENT-SIGNATURE header, retries the same URL; server returns 200 and may set PAYMENT-RESPONSE header with result (e.g. transaction hash).
Request resource
GET <Echo URL> (e.g. https://x402.payai.network/api/base-sepolia/paid-content).
Parse 402
PAYMENT-REQUIRED response header (base64-encoded JSON).accepts array.{ x402Version?, error?, resource?, accepts: Array<{ scheme, network, amount, asset, payTo, maxTimeoutSeconds?, extra? }> }.Pick USDC option
accepts, choose entry where asset matches USDC on Base Sepolia or extra.name === "USDC".amount, asset, payTo, extra.name / extra.version for EIP-712.Build EIP-3009 authorization
name = extra?.name ?? "USDC", version = extra?.version ?? "2", chainId = 84532, verifyingContract = asset.TransferWithAuthorization: from, to, value, validAfter (0), validBefore (e.g. now + 300s), nonce (32 random bytes as hex).signTypedData (EIP-712).Send payment and retry
{ x402Version: 2, scheme, network, accepted: { scheme, network, amount, asset, payTo, maxTimeoutSeconds, extra? }, payload: { signature, authorization: message }, extensions: {} }.PAYMENT-SIGNATURE = base64(JSON.stringify(payload)).GET with header PAYMENT-SIGNATURE: <base64>.Read result
PAYMENT-RESPONSE or X-PAYMENT-RESPONSE header (base64 JSON) may contain transaction (tx hash) etc.VITE_X402_USDC_ECHO_URL: PayAI Echo endpoint (default: https://x402.payai.network/api/base-sepolia/paid-content).0x036CbD53842c5426634e7929541eC2318f3dCF7e.Server returns 402 with PAYMENT-REQUIRED: 1 and a JSON body containing options (EVVM pay options with to, suggestedNonce, etc.). Client signs an EVVM pay message (personal_sign), POSTs to server’s payment endpoint; server executes pay() on EVVM Core and returns content + txHash.
Protected resource
GET /clinical/mri-slot (or similar): if not paid, respond with 402, PAYMENT-REQUIRED: 1, and body:
resource, description, to (recipient address), suggestedNonceoptions: array with at least one option: id, type: "evvm_pay", chainId, evvmId, coreAddress, token (DHM), to, suggestedNonce, amount, priorityFee, executor (or null), isAsyncExec.Payment execution
POST /payments/evvm/dhm body: from, to, toIdentity, token, amount, priorityFee, executor, nonce, isAsyncExec, signature.
Server calls EVVM Core pay(...) with executor key, waits for receipt, returns { status, txHash, content }.
Request resource
GET <X402_SERVER_URL>/clinical/mri-slot.
Detect 402
res.status === 402 or res.headers.get("PAYMENT-REQUIRED") === "1". Parse body as JSON: { resource, description?, to, suggestedNonce?, options }.
Pick option
options.find(o => o.type === "evvm_pay" || o.id === "dhm-evvm") ?? options[0]. Ensure to and suggestedNonce are present.
Build EVVM pay message
keccak256(encodeAbiParameters("string, address, string, address, uint256, uint256", ["pay", to, toIdentity, token, amount, priorityFee])).evvmId, coreAddress, hashPayload, executor, nonce, isAsyncExec (comma-separated).buildEvvmPayMessageCoreDoc from frontend/src/lib/evvmSign.ts with: evvmId, coreAddress, to, "", token, amount, priorityFee, executor, nonce, isAsyncExec.Sign and submit
signMessage (personal_sign) the message string.POST <X402_SERVER_URL>/payments/evvm/dhm with JSON body: from, to, toIdentity: "", token, amount, priorityFee, executor, nonce, isAsyncExec, signature.content (unlocked resource), txHash.VITE_X402_SERVER_URL: DHM x402 server (e.g. https://evvm-x402-dhm.fly.dev or localhost).EXECUTOR_PRIVATE_KEY, RPC_URL, RECIPIENT_ADDRESS, EVVM_ID, EVVM_CORE_ADDRESS, DHM_TOKEN_ADDRESS (see server/.env.example).USDC (PayAI)
accepts used (not options).TransferWithAuthorization match USDC contract (name/version from extra or "USDC"/"2").PAYMENT-SIGNATURE is base64 JSON; same URL retried with GET + header.PAYMENT-RESPONSE decoded when present for tx hash / receipt.DHM (EVVM)
options[].to and suggestedNonce; client uses them in the signed message.hashDataForPayCore + buildEvvmMessageV3 (see evvmSign.ts).EXECUTOR_PRIVATE_KEY and RPC to submit pay().PayAI 402 (accepts):
type PaymentRequirement = {
scheme: string;
network: string;
amount: string;
asset: string;
payTo: string;
maxTimeoutSeconds?: number;
extra?: { name?: string; version?: string; [k: string]: unknown };
};
// 402 body: { x402Version?, error?, resource?, accepts: PaymentRequirement[] }
EVVM 402 (options):
type PaymentOption = {
id: string;
type: string;
chainId: number;
evvmId: string;
coreAddress: string;
token: string;
to?: string;
suggestedNonce?: string;
amount: string;
priorityFee: string;
executor: string | null;
isAsyncExec: boolean;
};
// 402 body: { resource, description?, to?, suggestedNonce?, options: PaymentOption[] }
For full code, see the reference paths at the top of this skill.
The flows above use a browser wallet (human-in-the-loop). Participants can extend the app so an agent can pay autonomously using the Privy Agentic Wallets skill.
git clone https://github.com/privy-io/privy-agentic-wallets-skill.git .cursor/skills/privy~/.openclaw/workspace/skills/privy for OpenClaw). Add PRIVY_APP_ID and PRIVY_APP_SECRET from dashboard.privy.io.Same protocol, different signer
Keep the x402 protocol (402 → build payload → sign → POST) unchanged. The only change is who signs: instead of signMessageAsync / signTypedDataAsync in the browser, the agent path uses the Privy API to sign with a Privy server wallet (same message / typed data).
DHM agent payer
/clinical/mri-slot → build EVVM pay message (reuse buildEvvmPayMessageCoreDoc) → sign the message via Privy’s sign API (see Privy skill references) → POST to /payments/evvm/dhm with the same body.USDC agent payer (optional)
TransferWithAuthorization → sign via Privy’s sign typed data API (EIP-712) → send PAYMENT-SIGNATURE and retry.Dual mode (stretch)