A minimal Node.js client that pays an AEON QR-code order over the x402 protocol (V2). It demonstrates the full machine-to-machine payment loop:
- Request the payment API without a payment header → receive HTTP 402 Payment Required with the x402 V2 payment requirements.
- Verify the wallet balance, sign an EIP-712 payment authorization locally (the private key never leaves your machine).
- Retry the request with the
PAYMENT-SIGNATUREheader → the server verifies and settles on-chain through the AEON facilitator and returns the settlement result in thePAYMENT-RESPONSEheader.
┌────────┐ GET /payment (no header) ┌────────────┐
│ Client │ ───────────────────────────────────────► │ AEON API │
│ │ ◄─────────────────────────────────────── │ │
│ │ 402 { x402Version:2, resource, accepts }│ │
│ │ │ │
│ │ sign EIP-712 authorization locally │ │
│ │ │ │
│ │ GET /payment + PAYMENT-SIGNATURE │ │ ┌─────────────┐
│ │ ───────────────────────────────────────► │ │ ───► │ Facilitator │ ──► on-chain
│ │ ◄─────────────────────────────────────── │ │ ◄─── │ verify/settle│ transfer
└────────┘ 200 + PAYMENT-RESPONSE (tx hash) └────────────┘ └─────────────┘
The token is selected with the token parameter (CLI arg or TOKEN env);
USDT is the default when not specified.
| Mode | Network | USDT | USDC | USDG |
|---|---|---|---|---|
base |
Base (eip155:8453) |
facilitator scheme | EIP-3009 (gasless) | – |
x |
X Layer (eip155:196) |
– | EIP-3009 (gasless) | EIP-3009 (gasless) |
bsc |
BSC (eip155:56) |
facilitator scheme | facilitator scheme | – |
- EIP-3009 (gasless): sign
TransferWithAuthorizationdirectly; no transaction is sent by the client. - Facilitator scheme: the token does not implement EIP-3009 (USDT on all
chains, USDC on BSC). The client sends a one-time ERC-20
approveto the AEON facilitator contract (requires a small native-token balance for gas), then signstokenTransferWithAuthorization. Settlement gas is sponsored by the facilitator. The server tells the client which scheme to use viaaccepts[].extra.eip3009.
.
├── src/
│ ├── config/app.js # Configuration (env-driven)
│ ├── sign/createX402Sign.js# EIP-712 signing (both schemes) + PAYMENT-SIGNATURE builder
│ └── main.js # Payment flow: 402 → balance check → sign → settle
├── qrcode.txt # Payment QR code string (sample included)
├── .env.example # Configuration template
└── package.json
Requires Node.js ≥ 18.
npm install
cp .env.example .env # then edit .envSet at least these two values in .env:
PRIVATE_KEY=0x...your wallet private key...
EMAIL=you@example.comPut the QR code string you want to pay into qrcode.txt (or set QR_CODE in
.env), then run:
npm run pay:base # pay with USDT on Base (default token)
npm run pay:bsc # pay with USDT on BSC
npm run pay:x # pay with USDT on X LayerEquivalent direct invocation: node src/main.js [base|bsc|x] [usdt|usdc], e.g.:
node src/main.js bsc # USDT on BSC (token defaults to USDT)
node src/main.js bsc usdc # USDC on BSC
node src/main.js base usdc # USDC on Base (gasless EIP-3009)
node src/main.js x usdg # USDG on X Layer (gasless EIP-3009)Note: X Layer does not support USDT — pass usdc or usdg explicitly when
using x mode.
Running in BSC mode (x402 V2)
{ address: '0x34B7...F510' }
Fetching x402 V2 payment requirements...
Accept: { amount: '550000000000000000', asset: '0x8ac7...580d', network: 'eip155:56', payTo: '0x043b...6669', ... }
Balance check on eip155:56: 9.90 USDC available, 0.55 USDC required
Generated PAYMENT-SIGNATURE header: eyJ4NDAyVmVyc2lvbiI6MiwicGF5bG9hZCI6...
Sending payment request with PAYMENT-SIGNATURE header...
Payment request status: 200
PAYMENT-RESPONSE header: { success: true, transaction: '0x…', network: 'eip155:56', payer: '0x34B7…F510' }
All settings are read from .env (see .env.example):
| Variable | Required | Default | Description |
|---|---|---|---|
PRIVATE_KEY |
yes | – | Payer wallet private key (signs locally only) |
EMAIL |
yes | – | Email attached to the order |
APP_ID |
no | TEST000001 |
Merchant appId issued by AEON |
PAYMENT_API_URL |
no | https://ai-api.aeonpay.ai/open/ai/402/payment |
Payment API endpoint |
QR_CODE |
no | contents of qrcode.txt |
QR code string to pay |
TOKEN |
no | USDT |
Payment token (USDT / USDC / USDG); CLI arg wins |
RPC_URL_BSC / RPC_URL_BASE / RPC_URL_XLAYER |
no | public RPCs | JSON-RPC endpoints for balance checks / approve |
The PAYMENT-SIGNATURE header is the Base64 of:
- For EIP-3009 tokens the EIP-712 domain is the token contract itself
(
name/versioncome fromaccepts[].extra), typeTransferWithAuthorization. - For non-EIP-3009 tokens the domain is the AEON facilitator contract
(
{ name: "Facilitator", version: "1" }), typetokenTransferWithAuthorizationwith an extraneedApprove: truefield, and addresses are lowercased in the signed message. - The scheme is chosen by the
accepts[].extra.eip3009flag from the 402 response.
The authorization expires after maxTimeoutSeconds (from the 402 response),
and the random nonce prevents replay.
| Symptom | Cause / fix |
|---|---|
Insufficient USDC balance on eip155:56 … |
The wallet holds less than the required amount — top up the token shown in the message. Checked before signing, so no gas is wasted. |
Server is not x402 V2 (x402Version=…) |
The endpoint is running the V1 protocol — check PAYMENT_API_URL. |
Expected 402 Payment Required, got … with code 2102 |
Server-side order creation failed (e.g. the deposit-address channel for that network is not configured in this environment). |
Settlement failed: …transfer amount exceeds balance… |
Balance changed between the check and settlement. |
| First BSC payment sends a transaction | That is the one-time approve to the facilitator; it needs a small BNB balance for gas. |
- Never commit
.env— it contains your private key (.gitignorealready excludes it). - The private key is used only to sign locally and to send the optional BSC
approvetransaction; it is never transmitted. - The signed authorization is bound to the exact recipient, amount and expiry returned by the server, and single-use via the nonce.
- Use a dedicated hot wallet with a small balance for automated payments.
MIT
{ "x402Version": 2, "payload": { "authorization": { // what the wallet authorizes "from": "0xPayer", "to": "0xPayTo", "value": "550000000000000000", "validAfter": "1786506621", "validBefore": "1786506981", "nonce": "0x…32 random bytes…" }, "signature": "0x…EIP-712 signature…" }, "resource": { "url": "…", "description": "…", "mimeType": "application/json" }, "accepted": { /* the accepts[] entry being paid, echoed back */ } }