Install
openclaw skills install digitalhealthImplements USDC x402 payments via PayAI (EIP-3009) and DHM x402 payments via EVVM native (signed pay). Use when adding x402 payment flows, PayAI Echo integra...
openclaw skills install digitalhealthThis 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)