X402 v2 payments

ICPay supports X402 v2, a “Payment Required” pattern for card/onramp-friendly flows. Instead of sending tokens directly from the user’s wallet to a contract, the wallet signs an authorization payload which is sent to ICPay’s facilitator and settlement endpoints. ICPay verifies and settles the payment, then returns the intent/payment status.

Summary of X402 v2 in ICPay

  • Server produces Accepts: The API returns one or more payment “acceptance” records describing what to sign (scheme/network/amount/recipient). EVM acceptances carry EIP‑3009 metadata; Solana acceptances carry message/transaction hints.
  • Client signs authorization: The SDK builds the correct authorization for the network:
    • EVM: EIP‑712 typed data (EIP‑3009 Permit or TransferWithAuthorization).
    • Solana: Signs an X402 v2 message or a prebuilt transaction (as required).
  • Settlement: The SDK sends a settle request with the X402 header to ICPay’s facilitator; ICPay validates and processes payment, then your app waits until a terminal status.
  • Facilitator: ICPay operates its own X402 facilitator endpoints for EVM and Solana so you don’t need to run custom infra.

High-level flow

  1. App requests an X402 intent using the SDK.
  2. API may return:
    • accepts[] → proceed with X402 flow (sign and settle).
    • A fallback recommendation → SDK falls back to the regular flow automatically.
  3. SDK builds EIP-712 typed data, requests signature from the user’s EVM wallet (eth_signTypedData_v4).
  4. SDK constructs an X402 header (base64 JSON) and calls the settle endpoint.
  5. SDK emits events and awaits a terminal status (completed/failed/mismatched/etc.).

Acceptance object (server response)

An X402 acceptance describes the exact authorization you must sign:

type X402Acceptance = {
  scheme: string            // usually 'exact'
  network: string           // EVM: chain ID (decimal string: '84532'); Solana: 'solana:mainnet' | 'solana:devnet'
  maxAmountRequired: string // amount in smallest unit (wei-like)
  resource?: string | null  // optional
  description?: string | null
  mimeType?: string | null
  payTo: string             // EVM: recipient/verifying contract; Solana: program address or destination
  maxTimeoutSeconds?: number
  asset?: string            // token address (may match payTo)
  extra?: {
    intentId?: string
    provider?: string
    ledgerId?: string
    facilitatorUrl?: string | null // ICPay facilitator URL used by the SDK
    name?: string                // token name for domain
    eip3009Version?: string      // e.g. '1'
    primaryType?: 'Permit' | 'TransferWithAuthorization' // optional hint
  }
}

Signing (EVM EIP-712 and Solana)

EVM:

The SDK constructs an EIP-712 domain and message from the acceptance, then asks the wallet to sign:

  • Domain:
    • name: extra.name
    • version: extra.eip3009Version
    • chainId: decimal network from the acceptance
    • verifyingContract: asset or payTo
  • Primary type:
    • If extra.primaryType === 'Permit', the SDK builds a Permit message.
    • Otherwise it builds a TransferWithAuthorization message.
  • Message fields:
    • TransferWithAuthorization: { from, to, value, validAfter, validBefore, nonce }
    • Permit: { owner, spender, value, nonce, deadline }
  • Nonce and validity:
    • nonce: 32-byte random value (hex).
    • validAfter: now - 86400s; validBefore/deadline: now + maxTimeoutSeconds (default ~300s).

The signature is collected using eth_signTypedData_v4.

Solana:

  • The SDK includes the payer’s publicKey when initiating the X402 intent so the server can build a signable payload.
  • Depending on the acceptance, the SDK either:
    • Requests a signMessage signature over the X402 v2 message with domain prefix ICPAY_X402_V2:; or
    • Requests a transaction signature over a prebuilt base64 transaction that includes the necessary program instructions.
  • The SDK then settles via ICPay’s Solana facilitator, which can relay the transaction on your behalf when required.

X402 header format

After signing, the SDK builds a base64-encoded JSON header that looks like:

{
  "x402Version": 1,
  "scheme": "exact",
  "network": "84532",
  "payload": {
    "authorization": {
      "from": "0xUserAddr",
      "to": "0xPayTo",
      "value": "1000000",
      "validAfter": "1690000000",
      "validBefore": "1690000300",
      "nonce": "0x...32-bytes..."
    },
    "signature": "0x...signature..."
  }
}

The SDK then base64-encodes this JSON and sends it as paymentHeader to the settle endpoint.

Settlement request

The SDK calls ICPay’s settle endpoint:

await publicApiClient.post('/sdk/public/payments/x402/settle', {
  paymentIntentId,         // provided by the API during X402 initiation
  paymentHeader,           // base64 string described above
  paymentRequirements: acceptance, // echo of the acceptance object
})

ICPay verifies the signature and authorizes the payment. The response contains status info (e.g., succeeded / failed) and may include a txHash. The SDK then proceeds to await a terminal status via notify/long-polling if needed.

Facilitator endpoints (ICPay)

Merchants integrate via the ICPay SDK, which calls ICPay’s facilitator behind the scenes. For completeness, the relevant HTTPS endpoints are:

  • Verify (performed within settle; no separate public endpoint required)
    • Verification runs server-side as part of settlement. The request is rejected with a failure status if verification fails (e.g., signature invalid, deadlines invalid, recipient/asset mismatch, insufficient funds).
  • Settle (public; merchant-authenticated via publishable key)
    • POST https://api.icpay.org/sdk/public/payments/x402/settle
    • Body:
      {
        "paymentIntentId": "pi_123",
        "paymentHeader": "<base64-encoded x402 header JSON>",
        "paymentRequirements": { /* echo of the acceptance object used for signing */ }
      }
      
    • Returns (simplified):
      {
        "status": "succeeded | failed | processing | completed | mismatched",
        "paymentIntent": { /* intent DTO */ },
        "payment": { /* latest payment DTO or null */ },
        "transactionId": "0x... | <baseSignature> | null",
        "canisterTxId": 1234,
        "externalCostAmount": "0"
      }
      
  • Optional helper endpoints (used by the SDK on certain tokens)
    • POST https://api.icpay.org/sdk/public/payments/x402/prepare (prebuild Solana transactions)
    • POST https://api.icpay.org/sdk/public/payments/x402/signable (prebuild Solana signable message)
    • POST https://api.icpay.org/sdk/public/payments/x402/relay (relay signed Solana transactions)

How merchants use it:

  • Your backend issues an intent via the SDK.
  • If the API responds with HTTP 402 and accepts[], the SDK guides the wallet to sign and then calls ICPay settle. No additional merchant infra is required.

HTTP 402 response shape (ICPay)

When X402 is available for the selected token/chain, ICPay responds with HTTP 402 and a JSON body like:

{
  "x402Version": 2,
  "paymentIntentId": "pi_123",
  "rpcChainId": "8453",
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "maxAmountRequired": "1000000",
      "resource": "/sdk/public/payments/intents/pi_123",
      "description": "ICPay payment",
      "mimeType": "application/json",
      "payTo": "0x6c6551d0CC6315D0Be562c5b855B667eE32a5298",
      "maxTimeoutSeconds": 300,
      "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "extra": {
        "intentId": "pi_123",
        "provider": "icpay",
        "ledgerId": "token_abc",
        "facilitatorUrl": "https://api.icpay.org",
        "name": "USD Coin",
        "eip3009Version": "2",
        "rpcChainId": "8453",
        "rpcUrlPublic": "https://mainnet.base.org"
      }
    }
  ],
  "error": ""
}

Notes:

  • accepts[] entries map one-to-one to a specific token/chain configuration. The SDK uses this to build the EIP‑712 message (EVM) or signable bytes/transaction (Solana).
  • maxAmountRequired is in the token’s smallest unit.

Facilitator

ICPay operates its own X402 facilitator for EVM and Solana:

  • EVM: Verifies EIP‑712 authorization and performs settlement against the configured contracts.
  • Solana: Verifies the X402 v2 message or relays the signed transaction to the network using a platform relayer when necessary.

These endpoints are used automatically by the SDK; no additional configuration is required beyond having an EVM/Solana wallet provider available.

SDK usage example

import { Icpay } from '@ic-pay/icpay-sdk'

// Ensure window.ethereum is available for EIP-712 signing
const icpay = new Icpay({
  publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!,
  enableEvents: true,
  debug: true,
  // Optionally pass your own provider; otherwise SDK uses window.ethereum
  // evmProvider: window.ethereum,
})

// Prefer tokenShortcode; server derives token/chain from shortcode
const result = await icpay.createPaymentX402Usd({
  usdAmount: 10,
  tokenShortcode: 'ic_icp',
  metadata: { orderId: 'A-1001' },
})

// Handle terminal response (completed / failed / pending -> continued waiting)
if (result.status === 'completed') {
  // success
}

What the SDK does for you:

  • Initiates the X402 intent (/sdk/public/payments/intents/x402).
  • If the server returns accepts[], it:
    • Ensures wallet is on the desired chain (best-effort).
    • Builds domain + message for EIP-712 (Permit or TransferWithAuthorization).
    • Requests signature (eth_signTypedData_v4) from the wallet.
    • Builds a base64 X402 header and calls ICPay settle.
    • Emits lifecycle events and awaits terminal status.
  • If the server suggests fallback or does not return accepts, it transparently falls back to createPaymentUsd.

Deprecated inputs (still supported)

You can still pass symbol, ledgerCanisterId, or chainId, but tokenShortcode is now the primary input for selecting a token. The server resolves token ledger and chain from the shortcode automatically.

Scope and networks

  • EVM and Solana are supported in X402 v2 flows (EIP‑712 on EVM; message/transaction signing on Solana).
  • For the Internet Computer (IC) network, the SDK uses native ICRC‑1 transfers (see createPayment / createPaymentUsd) and then notifies/awaits terminal status. X402 is not used on IC at this time.

USDC example (Base)

Use USD Coin on Base mainnet via tokenShortcode: 'base_usdc':

import { Icpay } from '@ic-pay/icpay-sdk'

const icpay = new Icpay({
  publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!
})

const res = await icpay.createPaymentX402Usd({
  usdAmount: 12.5,
  tokenShortcode: 'base_usdc',
  metadata: { orderId: 'ORDER-1002' },
})

if (res.status === 'completed' || res.status === 'succeeded') {
  // payment completed
} else if (res.status === 'failed') {
  // handle failure or fallback
}

Events and statuses

Enable with enableEvents: true. X402 flows emit the same lifecycle events as regular payments:

  • icpay-sdk-transaction-created
  • icpay-sdk-transaction-updated
  • icpay-sdk-transaction-completed
  • icpay-sdk-transaction-failed
  • icpay-sdk-transaction-mismatched

The settle response can be terminal or transitional. When non-terminal, the SDK continues notifying the API until a terminal state is observed.

Troubleshooting

  • No EVM provider: Ensure window.ethereum is present or pass evmProvider in Icpay config.
  • Wrong chain: The SDK will try to wallet_switchEthereumChain. Provide rpcUrlPublic and chainName in the intent (server side) if add-and-switch is needed.
  • Signature rejected: Users can cancel the signature request. Catch IcpayError and handle isUserCancelled().
  • Minimum limits: If settlement fails due to minimums (e.g., x402_minimum_amount_not_met), the SDK emits a failure and then falls back to the normal flow.
  • Server returns HTTP 402: The SDK extracts accepts/intentId from the response and begins settlement/wait accordingly.

Was this page helpful?