xAPGX Protocol Agent Payment GatewayTESTNET · NO REAL FUNDS

Integration guide

Integrate institutional agent payments

Integration mode / Scheme
Environment configuration unavailable. Copyable environment examples appear only after configuration is confirmed.

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: Testnet https://xapg.io · Mainnet https://mainnet.xapg.io

Testnet and Mainnet use separate hosts. Before signing, confirm the environment and CAIP-2 network returned by GET /supported.

x402 v2 · exact · Base · USDC · EIP-3009
x402 v2 · batch-settlement · Base · 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

Use an institution API key for the selected environment. Grant supported for discovery, verify for validation, and settle only to the service authorized to submit settlement.

RequirementValue / action
XAPG_FACILITATOR_API_KEYInstitution API key with supported, verify and, when authorized, settle scopes.
Node.js24 or later for the supplied Testnet runners
XAPG_TEST_PRIVATE_KEYDisposable Testnet EOA wallet: Exact requires 0.01 Base Sepolia USDC; Batch lifecycle requires 0.03, plus Base Sepolia ETH for gas
XAPG_ALLOWED_PAY_TOApproved Testnet recipient; fill after Inspect
Use a disposable Testnet wallet. Never use a production private key.

Exact · Quick Start · Testnet

Sign a Base Sepolia USDC authorization and call /verify. This example does not submit settlement.

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_(?:test|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_test_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.

Exact EIP-3009: /verify validates authorization locally; /settle checks current chain state before submission. Successful verification does not guarantee payment.

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

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

Amounts are decimal strings in USDC atomic units: 10000 = 0.01 USDC. Times are Unix seconds; the example expires 120 seconds after signing.

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

Shared API contract · Testnet and Mainnet

Both environments use the same methods, paths, headers and scopes. Capability discovery determines the available schemes and network; it does not authorize Mainnet access.

APIAuthentication / scopePurpose
GET /supportedX-API-Key / supported
Browser session not accepted
Discover enabled schemes, networks, assets and signers
POST /verifyX-API-Key / verify
Or browser session + X-CSRF-Token
Validate a signed payment without transferring funds
POST /settleX-API-Key / settle
Or browser session + X-CSRF-Token
Submit an approved payment operation and return its result
GET /settlements/pendingX-API-Key / settle
Or browser session
Owning account only
Find one unresolved operation across devices; reconcile each request before retrying
GET /settlements/:requestIdX-API-Key / settle
Or browser session
Owning account only
Read owned settlement evidence by request ID; no resubmission
GET /payment-environment-configPublic / noneRead deployment environment, network, asset and limits
GET /demo/payment-configBrowser session / noneRead payment configuration for the authenticated wallet demo
GET /versionPublic / noneIdentify the deployed release
HeaderUsage
X-CSRF-TokenRequired for browser-session POST requests; institution API keys do not use browser CSRF
X-API-KeyInstitution authentication; key must authorize the selected environment and endpoint scope
Content-Type: application/jsonRequired for JSON verify and settle request bodies
Idempotency-KeyBatch onchain operations only; reuse with the identical body. Exact does not use this header
X-Payment-Operation-IDOptional UUID for /settle; reuse for the same business payment across retries and devices, even if its signature changes. Omitting it defaults to X-Request-ID and cannot identify a later duplicate business payment
X-Payment-RecoveryOptional /settle header: same-operation. Requires the original X-Request-ID, X-Payment-Operation-ID and unchanged payment body. Claims only an expired execution lease; replays an existing signed transaction, never creates a replacement payment. Legacy operations without recovery metadata fail closed
X-Request-IDOptional correlation ID; the response returns a request ID

OpenAPI 3.1 JSON

not_submitted means no submission was attempted; failed means a known failure; unknown means the outcome must be reconciled by request ID before any resubmission. Retryability alone does not authorize resubmission.

Extension fields are accepted. Payload authorization and network compatibility are validated by the selected scheme adapter. Public responses omit internal diagnostics. API errors retain the string error field; additive errorDetail.code is present on standardized errors and optional for legacy authentication/CSRF errors. Local Mainnet execution refusal preserves its nested error.code variant. Successful settlements require network; legacy rejected operations may omit network.

X-Request-ID is optional on all endpoints. Supply a UUID or the server generates one; responses return it in X-Request-ID. Persist a client-generated UUID before settlement to recover a lost response through /settlements/:requestId.

Returns supported kinds, assets and adapters. Requires supported scope; no request body.

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

Exact extra.protocol selects an adapter: omitted or eip-3009 (case-insensitive). Batch extra.assetTransferMethod selects token funding: eip3009 only, required by this implementation. These fields have different purposes and are not interchangeable. Permit2 is only exposed for configured Polygon Amoy adapters; this Base Batch profile does not accept it.

FieldRequired / meaning
x402VersionRequired; 2
paymentPayloadRequired; x402Version, accepted and signed payload
paymentRequirementsRequired; approved scheme, network, asset, amount, payTo, maxTimeoutSeconds

paymentPayload.resource is optional on /verify and /settle. Its URL and metadata are not verified by these facilitator routes or signed by EIP-3009. Agent approval separately validates resourceUrl against its URL policy; institutions must bind the resource to the approved order.

paymentPayload.resourceMeaning
urlResource URL for institution-managed order binding; not verified by the facilitator
description / mimeTypeOptional descriptive metadata
For Exact, POST /verify and POST /settle use the same signed payment body.

paymentPayload.accepted must equal paymentRequirements. Keep the signed amount, recipient, nonce and validity window unchanged between /verify and /settle.

Request structure (placeholders, not executable data)

Exact + Batch · POST /verify

Validates Exact authorizations or Batch deposit, voucher and refund payloads without submitting a transaction. Requires verify scope.

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

Require HTTP 200 and isValid:true. Check payer when returned; verification does not authorize business approval or reserve funds.

Exact + Batch · POST /settle

/settle does not require a prior /verify call or approval token. Your backend must enforce verification, per-order locking, budget and approval before submission.

Submit the same payment body that passed /verify.

Requires settle scope and institutional approval.

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

Require success:true and the expected network. Save transaction and reconcile the successful receipt and USDC movement.

FieldMeaning
isValidVerification result; false means validation failed
successSettlement result; HTTP 200 alone is insufficient
networkSettlement network; must match the request
transactionOptional onchain transaction hash
payerOptional payer; validate when returned
invalidReason / errorReasonProtocol error code
extraOperation-specific channel state
failureOptional handler error details

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.

API change log and migration policy

Configured limits and release policy

The server container retains npm ci --legacy-peer-deps because Circle SDK 10.8.0 has an optional @solana/codecs-strings ^2 peer while the locked Solana stack uses 5.5.1. This is a server dependency workaround, not required by the standalone Base signing example.

GET /payment-environment-config reports the selected deployment configuration. Testnet source defaults are 1000000 atomic units for MAX_AMOUNT and BATCH_MAX_DEPOSIT_ATOMIC (1 USDC). Mainnet requires explicit MAINNET_MAX_AMOUNT and MAINNET_AGENT_TOTAL_LIMIT; its deposit ceiling defaults to that configured total limit. Defaults do not prove deployed limits.

maxAmountAtomicUnavailable
general API · IPUnavailable
verify / settle · credentialUnavailable
batchMaxDepositAtomicUnavailable

API 0.3 uses x402 v2, @x402/core and @x402/evm 2.25.0, and viem 2.55.19. Pin the integration package and lockfile, install with npm ci, and record the package version and deployment revision with each release. Review upstream changelogs and rerun signing and settlement checks before upgrades; package version alone does not identify the deployed server.

Production readiness also requires institution-controlled signing, durable operation storage, order locking, budget approval, monitoring, reconciliation and an incident owner. This guide describes technical integration; it does not certify regulatory or AML compliance.

Exact + Batch · errors

Payment failures may include failure. Use code for handling, settlementStatus for reconciliation and requestId for support. Authentication and rate-limit errors use a separate error response.

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

For /verify, handler failures use not_submitted. /settle may return failed before transaction submission; use the transaction hash and chain receipt to establish the outcome. Batch wrapper errors use verification_failed or settlement_failed; invalid_batch_settlement_evm_* identifies SDK-returned validation errors.

Authentication errors retain error as a string; errorDetail.code and requestId may be provided. HTTP 429 includes Retry-After. These are transport/authentication responses, not payment failure envelopes. Do not infer settlement submission from an authentication error or retryable flag.

Authentication and rate limiting

Institution limits for /verify and /settle are configured by the administrator. Settlement queries (/settlements/:requestId and /settlements/pending) follow the institution’s /settle limit with a separate shared query counter; polling does not consume the submission quota. When that limit is disabled, neither settlement submissions nor queries have an application request limit. RPC provider limits still apply.

HTTP 401
{ "error": "environment_mismatch",
  "errorDetail": { "code": "environment_mismatch" },
  "requestId": "<request UUID>" }

HTTP 403
{ "error": "facilitator_scope_not_authorized",
  "errorDetail": { "code": "facilitator_scope_not_authorized" },
  "requestId": "<request UUID>", "requiredScope": "settle" }

HTTP 429
Retry-After: <seconds>
{ "error": "rate_limit_exceeded",
  "errorDetail": { "code": "rate_limit_exceeded" },
  "requestId": "<request UUID>" }

Institution keys must be bound to the selected environment. environment_mismatch rejects a key for another environment; institution_key_environment_unbound rejects an unbound legacy key. Reissue legacy keys with an explicit environment before migration; existing unbound keys will stop authenticating. Retry-After is provided for HTTP 429; do not assume this header on other errors.

settlement_provider_unavailable is missing deployment capability and needs operator configuration. rpc_unavailable is a transient RPC failure. Retry reads with backoff; for settlement, reconcile the original operation before any retry.

Public failure code matrix

failure.coderetryablesettlementStatusMeaning / action
invalid_payment_payload
invalid_permit2_payloadPolygon Amoy adapter only
invalid_signature
signer_mismatch
falsenot_submitted / failedCorrect the payload, domain or signer; do not submit settlement.
insufficient_fundsfalsenot_submitted / failedFund the payer, then create and verify a new authorization.
authorization_expired
authorization_not_yet_valid
authorization_too_close_to_expiry
falsenot_submitted / failedCorrect 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
falsenot_submitted / failed / unknownStop and reconcile with the requestId and any known transaction hash.
verification_failed
unexpected_provider_error
falsenot_submitted / failed / unknownDo not guess the outcome; inspect stage and settlementStatus, then contact xAPG with requestId.
cloud_journal_lane_blocked
cloud_journal_lane_untracked
falsenot_submittedSelf-hosted settlement: a prior expired unsigned nonce or an untracked legacy gap blocks new payments for this sender. Ask the operator to reconcile the lane; do not create new operation IDs or retry automatically. Existing signed operations retain same-operation recovery.
rpc_rate_limited
rpc_unavailable
truenot_submitted / failed / unknownRetry 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: check credentials, scope and browser CSRF. HTTP 429: respect Retry-After when present. HTTP 200 may contain isValid:false or success:false.

Production & Security

Agent payment approval API

Agent payment approvals use Agent API key, OAuth or browser authentication; institution API keys are for Facilitator routes. Browser signing requires a session and CSRF. See the standalone reference for both environments, request fields and continuation recovery.

Agent API reference

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.

Use an institutional signer for Mainnet. The supplied command-line runners support Base Sepolia only.

Keep API keys and signing material in backend secret storage; exclude them and payment-attempt*/ from version control.

Contact xAPG for the matching integration package and institution access. Install the supplied lockfile with npm ci.

Exact · settlement runner

Testnet settlement runner: scripts/institution-quickstart.mjs. Requires settle scope and business approval.

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

The runner submits once, checks the USDC receipt and saves records. Do not use it to retry an uncertain submission.

Exact + Batch · settlement reconciliation

The Exact runner writes these files. Batch clients must store equivalent operation and receipt records.

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

Existing output directories block submission. Missing submission.json or receipt.json does not prove failure; follow Retry policy.

Implement durable per-order locking and budget reservation across workers. Runner directory protection is not order deduplication; settlement submission lookup is available, but no merchant order-status API is provided.

Recover a saved settlement submission

Persist a UUID X-Request-ID with the operation before POST /settle. Query GET /settlements/:requestId with a settle-scoped institution API key or the owning browser session. The response is scoped to the owner and environment.

status is processing, completed or unknown; httpStatus and response are optional saved handler results. This journal is not chain confirmation. processing may indicate an interrupted handler; HTTP 404 only means no matching stored record and never proves that funds did not move. Reconcile transaction receipts, authorization nonce or channel state before advancing.

Recover from the saved operation, not by calling lifecycle again. A permitted Batch retry uses the identical body and original Idempotency-Key, but a new persisted UUID X-Request-ID to journal the retry. Only deposits support automatic pending-transaction recovery; reconcile unknown claim, settle and refund outcomes first. Exact has no automatic resubmission.

Payer exit without the facilitator

SDK 2.25.0 exposes initiateWithdraw(config, amount), pendingWithdrawals(channelId) returning amount and initiatedAt, and finalizeWithdraw(config) in the Batch contract ABI. These are direct payer-wallet contract calls, not xAPG API endpoints. Keep the original ChannelConfig and channelId, connect to the correct network and contract, and fund the wallet with native ETH for gas.

The upstream EVM binding permits payer or payerAuthorizer to initiate a timed withdrawal. The requested amount must fit unclaimed escrow; the merchant may claim outstanding vouchers during the delay. Final withdrawal is capped by the remaining unclaimed balance. Verify that the deployed contract implements this binding.

Upstream EVM withdrawal specification

payer wallet → initiateWithdraw(originalChannelConfig, amount)
read pendingWithdrawals(channelId)
wait configured withdrawDelay from initiatedAt
payer wallet → finalizeWithdraw(originalChannelConfig)
confirm receipt and token balance

Before using exit, verify the deployed contract address, ABI, caller permissions, available balance and current withdrawal rules. The delay alone does not guarantee a successful withdrawal; competing claims and contract state may affect the result. Caller permissions have not been independently verified by this guide.

Download unsigned Batch typed-data and hash vectors

Download Voucher, ClaimBatch and Refund signature verification vectors

Vectors include typed data, digests, signatures and recovered signer addresses for both networks. Signatures come from discarded, unfunded test signers; no signing material is retained. Use them only for offline verification.

Resume a saved Batch deposit

async function resumeDeposit(saved, { post, persist, reconcile }) {
  if (saved.stage !== "deposit") throw Error("Reconcile this operation first");
  const attempt = { ...saved, requestId: crypto.randomUUID() };
  await persist(attempt); // Keep the original request and Idempotency-Key.
  const result = await post("/settle", saved.request, {
    "Idempotency-Key": saved.idempotencyKey, "X-Request-ID": attempt.requestId
  });
  await persist({ ...attempt, result });
  if (!result.success) return { halted: true, result };
  await reconcile(saved.stage, saved.request, result);
  return result;
}

Advanced Signing & Limitations

Exact requires a 65-byte EOA EIP-712 signature (r || s || v). personal_sign, EIP-1271 and contract-wallet signatures are not supported by this recovery path.

Order IDs and resource URLs are not EIP-3009 signed fields. Bind them to the approved requirements and nonce; verify delivery separately.

Compliance controls are separate from payment signature validation. This API does not perform payer/payTo sanctions screening, exchange Travel Rule data or file suspicious transaction reports. The institution controls pre-sign screening and required counterparty data; the institution and xAPG operator must each assess and document their own applicable reporting and service obligations before production.

Responsibilities

InstitutionxAPG
Wallet custodyAPI verification
Private-key managementSettlement submission
Pre-sign approvalSettlement execution
Spending policiesProtocol validation
BudgetsFacilitator infrastructure
Order deduplicationSettlement response
Legal and regulatory assessmentProvide technical integration details; institutions assess applicable custody, AML, sanctions and reporting obligations with their advisers.
Audit records—
Merchant/resource delivery verification—