VLED x402 $VLED ← Pay

Accept USDG on Robinhood Chain without asking anyone for gas. The payer signs an EIP-3009 authorization; the facilitator submits the transaction and pays the ETH.

This is the standard x402 exact scheme for EVM — the same one used with USDC on Base. Nothing here is bespoke crypto: if you already speak x402, you only need the chain and token values below.

How a payment works

Four steps. The payer never sends a transaction, so they need no ETH — only USDG.

StepWhoWhat happens
Quoteclient → serverRequest the resource, get 402 with the terms
Signpayer's walletSign a TransferWithAuthorization (EIP-712). Off-chain, free
Verifyserver → facilitatorSignature, recipient, deadline and balance are checked
Settlefacilitator → chaintransferWithAuthorization() moves USDG payer → payee. Facilitator pays gas

Because EIP-3009 nonces are random 32-byte values rather than a sequential counter, two payments from the same wallet can be in flight at once without invalidating each other. That is the main reason this uses EIP-3009 and not EIP-2612 permit.

Chain and token

NetworkRobinhood Chain — chain ID 4663
RPChttps://rpc.mainnet.chain.robinhood.com
Explorerrobinhoodchain.blockscout.com
Native gasETH — needed by the facilitator, never by the payer
TokenUSDG "Global Dollar" (Paxos)
Address0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
Decimals6 — 1 USDG = 1000000, not 1e18
x402 network idrobinhood

The one thing that will break your integration

USDG has no version() function — calling it reverts. x402 falls back to that call when building the EIP-712 domain, so every payment requirement must carry the domain explicitly:

extra: { name: "Global Dollar", version: "1" }

Omit it and verification fails with invalid_network, which points nowhere near the real cause. The verifyingContract is the proxy address above, not the implementation behind it.

Endpoints

Resource server

MethodPathReturns
GET · POST/pay/1
/pay/5
/pay/25
402 with the terms for that amount, or 200 when an X-PAYMENT header settles
GET/x402
/.well-known/x402
Discovery: every tier's payment requirements in one document. This is what an agent reads, not this page
GET/llms.txtThis guide as plain text
GET/docsThis page

Facilitator — {{FACILITATOR_URL}}

MethodPathBody / returns
POST/verify{paymentPayload, paymentRequirements}{isValid, invalidReason, payer}
POST/settleSame body → {success, transaction, payer}. Non-2xx if settlement failed
GET/supportedThe schemes this facilitator serves

The facilitator accepts exact + eip3009 on robinhood only. A permit payload, or one for another network, is rejected with a 400.

Getting a quote

Three amounts are on sale: 1, 5 and 25 USDG, at /pay/1, /pay/5 and /pay/25. Each quotes only itself; /x402 lists all three at once.

# Ask what the resource costs
curl -X POST https://vled.example/pay/1

# 402 Payment Required
{
  "x402Version": 1,
  "accepts": [{
    "scheme":            "exact",
    "network":           "robinhood",
    "maxAmountRequired": "1000000",        // 1 USDG, 6 decimals
    "asset":             "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
    "payTo":             "{{PAY_TO}}",
    "maxTimeoutSeconds": 3600,
    "extra":             { "name": "Global Dollar", "version": "1" }
  }],
  "error": "Payment required"
}

Signing the authorization

Sign the EIP-712 struct below, then base64 the payload into an X-PAYMENT header and repeat the request. Take name and version from the quote's extra — don't read them from the contract, it can't tell you.

const authorization = {
  from:        payer,
  to:          req.payTo,
  value:       req.maxAmountRequired,
  validAfter:  "0",
  validBefore: String(Math.floor(Date.now() / 1000) + 3600),
  nonce:       randomBytes32(),            // random, not sequential
}

const signature = await wallet.signTypedData({
  primaryType: "TransferWithAuthorization",
  domain: {
    name:              req.extra.name,     // "Global Dollar"
    version:           req.extra.version,  // "1"
    chainId:           4663,
    verifyingContract: req.asset,          // the USDG proxy
  },
  types: { TransferWithAuthorization: [
    { name: "from",        type: "address" },
    { name: "to",          type: "address" },
    { name: "value",       type: "uint256" },
    { name: "validAfter",  type: "uint256" },
    { name: "validBefore", type: "uint256" },
    { name: "nonce",       type: "bytes32" },
  ]},
  message: authorization,
})

const header = btoa(JSON.stringify({
  x402Version: 1,
  scheme:      "exact",
  network:     "robinhood",
  payload: { authorizationType: "eip3009", signature, authorization },
}))

await fetch("/pay/1", { method: "POST", headers: { "X-PAYMENT": header } })
// 200 → { message, tier, payer, transaction }

When it fails

A rejected payment comes back as 402 with a reason. These are the ones you will actually hit:

ReasonWhat it really means
insufficient_fundsThe payer's USDG balance is below the amount. Not a gas problem — payers never need gas
invalid_exact_evm_payload_signatureThe signature doesn't recover to from. Usually a wrong EIP-712 domain
invalid_networkOften the missing extra.name/extra.version, not a wrong chain
invalid_exact_evm_payload_recipient_mismatchauthorization.to isn't the quoted payTo
invalid_exact_evm_payload_authorization_valid_beforeThe authorization expired before it could settle
settlement_errorThe transaction failed on-chain. Commonly the facilitator wallet is out of ETH

A failed settlement is never a 200. If the facilitator can't move the funds, you don't get the resource.

Reference

EIP-3009 — Transfer With Authorization · x402 protocol · Robinhood Chain explorer