xAPGX Protocol Agent Payment GatewayTESTNET · NO REAL FUNDS

Integration guide

Integrate institutional agent payments

Integration mode / Scheme

Switch the flow, navigation and examples throughout this guide.

Verify and settle institutional payments with xAPG Facilitator.

Three APIs · one payment flow

Discover capability, verify the wallet-signed authorization, then settle only after institutional approval.

Use caseScheme
One authorization, one immediate paymentexact
High-frequency small charges accumulated before onchain settlementbatch-settlement

API base URL: https://xapg.io

The host is the same; select the environment from GET /supported and the returned CAIP-2 network, not from the hostname.

x402 v2 · exact · Base · USDC · EIP-3009
x402 v2 · batch-settlement · Base Sepolia · USDC · payment channel

Your institution controls the wallet and private key. xAPG receives only signed authorizations and commitments.

Get an institution API key first

Manage institution API keys

Get this key first; the POC cannot call /supported or /verify without it.

RequirementValue / action
Node.js24 or later
XAPG_FACILITATOR_API_KEYInstitution API key (supported + verify scopes)
XAPG_TEST_PRIVATE_KEYDisposable Testnet EOA wallet with at least 0.01 Base Sepolia USDC
XAPG_ALLOWED_PAY_TOApproved Testnet recipient; fill after Inspect
Use a disposable Testnet wallet. Never use a production private key.

Exact · 5-minute Quick Start · Testnet

This POC does one thing: sign a Base Sepolia USDC authorization locally and call /verify. It never calls /settle.

All institutional examples use xAPG Facilitator only.

Install

mkdir xapg-testnet && cd xapg-testnet
npm init -y
npm install --save-exact viem@2.55.19
mkdir scripts
POC source · scripts/institution-quick-verify.mjs
// Base Sepolia only. Use a disposable Testnet wallet.
import { randomBytes } from 'node:crypto';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { getAddress, formatUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const BASE = 'https://xapg.io';
const RESOURCE = `${BASE}/x402/resource`;
const NETWORK = 'eip155:84532';
const USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
const same = (a, b) => a?.toLowerCase() === b?.toLowerCase();
const assert = (ok, message) => { if (!ok) throw new Error(message); };

export async function runQuickVerify(env, deps = {}) {
  const fetcher = deps.fetcher ?? fetch;
  const log = deps.log ?? console.log;
  const apiKey = env.XAPG_FACILITATOR_API_KEY;
  assert(/^xapg_inst_[A-Za-z0-9_-]+$/.test(apiKey ?? ''), 'Missing institution API key');

  const api = async (path, body) => {
    const response = await fetcher(`${BASE}${path}`, {
      method: body ? 'POST' : 'GET', redirect: 'error',
      headers: { 'X-API-Key': apiKey, Accept: 'application/json',
        ...(body ? { 'Content-Type': 'application/json' } : {}) },
      ...(body ? { body } : {}), signal: AbortSignal.timeout(20_000),
    });
    assert(response.status === 200, `${path} returned HTTP ${response.status}`);
    return response.json();
  };

  const supported = await api('/supported');
  assert(supported.kinds?.some(k => k.x402Version === 2 && k.scheme === 'exact' && k.network === NETWORK), 'Unsupported payment kind');
  assert(supported.assets?.some(a => a.network === NETWORK && same(a.address, USDC)), 'Unsupported asset');

  const challenge = await fetcher(RESOURCE, { redirect: 'error', signal: AbortSignal.timeout(20_000) });
  assert(challenge.status === 402, 'Expected HTTP 402');
  const encoded = challenge.headers.get('PAYMENT-REQUIRED');
  assert(encoded && encoded.length < 65_536, 'Missing payment requirements');
  const required = JSON.parse(Buffer.from(encoded, 'base64url').toString()).accepts
    ?.find(r => r.scheme === 'exact' && r.network === NETWORK && same(r.asset, USDC));
  assert(required, 'No supported payment requirement');
  getAddress(required.payTo);
  assert(BigInt(required.amount) > 0n && BigInt(required.amount) <= 10_000n, 'Amount exceeds 0.01 test USDC');

  if (env.inspect) {
    log(`INSPECT ONLY\nNetwork: Base Sepolia\nAsset: USDC\npayTo: ${required.payTo}\nAmount: ${formatUnits(BigInt(required.amount), 6)} USDC\nNO SIGNATURE · NO PAYMENT`);
    return { status: 'inspected', payTo: required.payTo, amount: required.amount };
  }

  assert(same(required.payTo, getAddress(env.XAPG_ALLOWED_PAY_TO)), 'Recipient is not approved');
  assert(/^0x[0-9a-fA-F]{64}$/.test(env.XAPG_TEST_PRIVATE_KEY ?? ''), 'Missing Testnet private key');
  log('1. DISCOVERED · Recipient and amount approved · NOT PAID.');

  const account = privateKeyToAccount(env.XAPG_TEST_PRIVATE_KEY);
  const now = deps.now ?? Math.floor(Date.now() / 1000);
  const authorization = { from: account.address, to: required.payTo, value: required.amount,
    validAfter: String(now - 60), validBefore: String(now + 120),
    nonce: `0x${randomBytes(32).toString('hex')}` };
  const signature = await account.signTypedData({
    domain: { name: 'USDC', version: '2', chainId: 84532, verifyingContract: USDC },
    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' },
    ] }, primaryType: 'TransferWithAuthorization', message: authorization,
  });
  log('2. SIGNED · EIP-3009 authorization signed locally · NOT PAID.');

  const body = JSON.stringify({ x402Version: 2, paymentPayload: { x402Version: 2,
    resource: { url: RESOURCE }, accepted: required, payload: { signature, authorization } },
    paymentRequirements: required });
  const verified = await api('/verify', body);
  assert(verified.isValid === true && same(verified.payer, account.address), verified.invalidReason ?? 'Verification failed');
  log('3. VERIFIED · isValid: true · NOT PAID.');
  return { status: 'verified', payer: account.address };
}

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
  runQuickVerify({ ...process.env, inspect: process.argv.includes('--inspect') })
    .catch(error => { console.error(error.message); process.exitCode = 1; });
}

Create .env with your institution API key. Use a disposable Testnet wallet only.

XAPG_FACILITATOR_API_KEY=xapg_inst_REPLACE_ME
XAPG_TEST_PRIVATE_KEY=0xREPLACE_WITH_TEST_PRIVATE_KEY
XAPG_ALLOWED_PAY_TO=

1. Inspect

node --env-file=.env scripts/institution-quick-verify.mjs --inspect
INSPECT ONLY · NO SIGNATURE · NO PAYMENT

Approve the displayed payTo and amount, then copy payTo into XAPG_ALLOWED_PAY_TO.

2. Sign + Verify

node --env-file=.env scripts/institution-quick-verify.mjs
1. DISCOVERED · Recipient and amount approved · NOT PAID.
2. SIGNED · EIP-3009 authorization signed locally · NOT PAID.
3. VERIFIED · isValid: true · NOT PAID.
POC complete: /verify succeeded. No payment was submitted.

Continue to settlement only after institutional approval

Two different keys

CredentialPurposeSent to xAPG?
XAPG_FACILITATOR_API_KEYAuthenticate Institutional API callsYes, in X-API-Key
Wallet Private KeySign payment authorizationNever

Exact · signing with EIP-712 / EIP-3009

The payer signs an EIP-3009 TransferWithAuthorization locally using EIP-712 typed data.

const signature = await account.signTypedData({
  domain,
  types,
  primaryType: "TransferWithAuthorization",
  message: authorization
});

EIP-712 domain · Base Sepolia example

name               USDC
version            2
chainId            84532
verifyingContract  0x036CbD53842c5426634e7929541eC2318f3dCF7e

The contract above is Base Sepolia USDC. For another environment, use its advertised network and asset; do not reuse this domain.

TransferWithAuthorization · signed fields

from          address
to            address
value         uint256
validAfter    uint256
validBefore   uint256
nonce         bytes32

10000 atomic units = 0.01 USDC. Times are Unix seconds; the example uses a random 32-byte nonce and an authorization valid for at most 120 seconds.

Field mapping

SourceMust match
paymentRequirements.payToauthorization.to
paymentRequirements.amountauthorization.value
paymentRequirements.assetEIP-712 domain.verifyingContract
Payer wallet addressauthorization.from
These values must match. Do not modify them after signing.

Exact + Batch · GET /supported

Call /supported before signing to discover the currently supported network, asset, scheme and signing adapter. No body; requires supported scope. Do not assume batch-settlement is available unless its kind is returned.

network = required network
scheme  = exact OR batch-settlement
asset   = required USDC
adapter = eip-3009

ALL MATCH → continue
OTHERWISE → stop

Match kinds, assets and extensions[].adapters by network.

Response structure
{
  "kinds": [
    { "x402Version": 2, "scheme": "exact", "network": "eip155:CHAIN_ID" },
    { "x402Version": 2, "scheme": "batch-settlement", "network": "eip155:CHAIN_ID" }
  ],
  "extensions": [{
    "name": "xapg-payment-adapters",
    "version": "1",
    "adapters": [{
      "id": "eip-3009",
      "network": "eip155:CHAIN_ID",
      "asset": "0x…",
      "settlementSpenders": ["0x…"]
    }]
  }],
  "signers": { "eip155:*": ["0x…"] },
  "assets": [{
    "network": "eip155:CHAIN_ID",
    "address": "0x…",
    "symbol": "USDC",
    "decimals": 6,
    "protocols": ["x402"]
  }]
}

Exact · shared signed payment body

For Exact, POST /verify and POST /settle use the same signed payment body.

For Exact, never modify the signed amount, recipient, nonce or authorization window. Do not regenerate authorization between /verify and /settle. paymentPayload.accepted must equal paymentRequirements.

Request structure (placeholders, not executable data)
{
  "x402Version": 2,
  "paymentPayload": {
    "x402Version": 2,
    "resource": {
      "url": "https://merchant.example/resource",
      "description": "Order description",
      "mimeType": "application/json"
    },
    "accepted": {
      "scheme": "exact",
      "network": "eip155:CHAIN_ID",
      "amount": "10000",
      "asset": "0x…USDC from /supported…",
      "payTo": "0x…beneficiary…",
      "maxTimeoutSeconds": 120,
      "extra": { "name": "USDC", "version": "2", "protocol": "eip-3009" }
    },
    "payload": {
      "signature": "0x…EIP-712 signature…",
      "authorization": {
        "from": "0x…payer…",
        "to": "0x…same beneficiary…",
        "value": "10000",
        "validAfter": "…unix seconds…",
        "validBefore": "…unix seconds…",
        "nonce": "0x…32 bytes…"
      }
    }
  },
  "paymentRequirements": {
    "scheme": "exact",
    "network": "eip155:CHAIN_ID",
    "amount": "10000",
    "asset": "0x…USDC from /supported…",
    "payTo": "0x…beneficiary…",
    "maxTimeoutSeconds": 120,
    "extra": { "name": "USDC", "version": "2", "protocol": "eip-3009" }
  }
}

Exact + Batch · POST /verify

Validates an Exact authorization or a Batch deposit, voucher or refund payload without submitting an onchain transaction. Requires verify scope. Exact checks signatures, limits, replay protection and settlement simulation. Batch deposit checks include authorization and simulation; voucher/refund verification checks the commitment and current channel state, not merchant service deduplication.

{
  "isValid": true,
  "payer": "0x..."
}

Success requires HTTP 200, isValid: true and the expected payer. /verify alone does not move funds and does not replace institutional approval. For Batch vouchers, persist channelId and the cumulative amount before granting service.

Exact + Batch · POST /settle

Submit the same payment body that passed /verify.

Requires settle scope and institutional approval.

{
  "success": true,
  "network": "eip155:CHAIN_ID",
  "payer": "0x...",
  "transaction": "0x..."
}

Confirm success: true and the expected network. Persist the returned transaction hash for onchain Batch operations, then reconcile the successful receipt, channel and matching USDC movement before recording completion.

Retry policy

A timeout is an unknown outcome, not proof of failure.
Exact does not use Idempotency-Key.

Do not resubmit and do not sign a second authorization. Reconcile the original nonce and transaction first.

Exact + Batch · errors

Both /verify and /settle keep their protocol fields and also return the same safe failure object. Use failure.code for handling, failure.message for debugging, requestId when contacting xAPG, and settlementStatus to decide whether reconciliation is required.

{
  "failure": {
    "code": "invalid_signature",
    "stage": "authorization_validation",
    "message": "The wallet signature could not be recovered or validated.",
    "retryable": false,
    "settlementStatus": "not_submitted",
    "requestId": "..."
  }
}

Public failure code matrix

failure.coderetryablesettlementStatusMeaning / action
invalid_payment_payload
invalid_permit2_payload
invalid_signature
signer_mismatch
falsenot_submittedCorrect the payload, domain or signer; do not submit settlement.
insufficient_fundsfalsenot_submittedFund the payer, then create and verify a new authorization.
authorization_expired
authorization_not_yet_valid
authorization_too_close_to_expiry
falsenot_submittedCorrect the authorization window before settlement.
nonce_already_usedfalsenot_submitted / failedExact only: reconcile the original nonce and transaction; never sign a replacement until resolved.
simulation_failed
simulation_call_failed
execution_reverted
falsenot_submitted / failedCorrect the request or onchain state; do not loop settlement.
settlement_provider_unavailable
transaction_reverted
settlement_failed
falsefailedStop and reconcile with the requestId and any known transaction hash.
verification_failed
unexpected_provider_error
falsenot_submitted / failedDo not guess the outcome; inspect stage and settlementStatus, then contact xAPG with requestId.
rpc_rate_limited
rpc_unavailable
truenot_submitted / failedRetry verification after backoff. For settlement, follow the scheme-specific retry policy above.
rpc_timeouttruenot_submitted / unknownFor settlement, follow the scheme-specific retry policy above; retryable does not authorize a new payment.
invalid_batch_settlement_evm_*falsenot_submitted / failedBatch only: the suffix identifies the channel, voucher, claim, settlement or refund validation failure. Correct that operation; do not switch keys to bypass it.

HTTP 401/403 means the institution key, environment or scope is wrong. HTTP 429 means rate limiting; respect Retry-After when present. HTTP status alone never determines whether settlement occurred—use failure.settlementStatus.

Production & Security

Production signer

Testnet
.env private key → viem signer

Production
Institution Backend
       ↓
HSM / MPC / KMS / Wallet Service
       ↓
EIP-712 Signature
       ↓
xAPG
Production private keys must remain inside the institution's HSM/MPC/KMS or institutional wallet service.

Replace the local test signer with your institutional adapter, not by exporting a production key. The quick start is pinned to Base Sepolia; changing constants is not a production rollout. Validate the live capability, policy and signer together before launch.

Keep .env backend-only, set chmod 600 .env, and exclude .env and payment-attempt*/ from version control. Use an editor or secret manager rather than putting keys in shell history. Never expose API keys or live signatures in logs, chat or screenshots.

Contact xAPG for the matching integration package and institution access. For the supplied package, install with npm ci --legacy-peer-deps.

Exact · settlement runner

Settlement is intentionally separate from the POC. After institutional approval, use the supplied scripts/institution-quickstart.mjs with a key that has settle scope.

node --env-file=.env scripts/institution-quickstart.mjs --settle --confirm-settlement BASE_SEPOLIA --out-dir payment-attempt-001

The runner submits the verified body once, waits for the matching USDC receipt and writes reconciliation records. It is not a retry command.

Exact + Batch · settlement reconciliation

The filenames below are Exact runner artifacts. Batch integrations must implement equivalent durable records themselves; the facilitator does not write these files for Batch clients.

ArtifactRecorded when / contents
attempt.jsonBefore submission: payer, recipient, amount, network, request hash, and Exact nonce or Batch channelId + Idempotency-Key
submission.jsonAfter a valid settlement response: transaction hash and attempt context
receipt.jsonAfter successful receipt and matching transfer: confirmed block and settlement

An existing output directory blocks submission; never bypass it after an uncertain outcome. Missing submission.json or receipt.json is not proof of failure. For timeout handling, use the single Retry policy above.

Implement durable per-order locking and budget reservation before signing, including across concurrent workers. The Exact sample has directory collision protection, not business-order deduplication. No public order-status API is currently provided.

Advanced Signing & Limitations

This path uses EOA recovery with a 65-byte hex r || s || v EIP-712 signature; personal_sign is not interchangeable. EIP-1271 and contract-wallet signatures are not supported by this recovery path. Confirm your institutional service can produce the required signature.

Order IDs and resource URLs are not EIP-3009 signed fields. Bind them to the approved requirements and nonce in your backend. The example proves verification/settlement only; it does not verify merchant or resource delivery.

Responsibilities

InstitutionxAPG
Wallet custodyAPI verification
Private-key managementSettlement submission
Pre-sign approvalSettlement execution
Spending policiesProtocol validation
BudgetsFacilitator infrastructure
Order deduplicationSettlement response
Audit records—
Merchant/resource delivery verification—

Acceptance: verify-only makes no transfer; one approved settlement produces the correct USDC transfer and receipt; bad input is rejected; an unknown outcome blocks another payment for the same order.

Offline example tests do not replace joint acceptance with your real institutional signer.