Payments Architecture
Every x402Flex settlement must route Client → X402FlexRouter → X402FlexRegistry. Direct contract calls are blocked so a single PaymentSettledV2 schema powers analytics, compliance, and MCP agents.
Router Flow
- Client (wallet, POS, agent, or MCP server) creates a
PaymentIntent+ optionalFlexWitnessusing@pepaylabs/bnbpay. X402FlexRoutervalidates theschemeId(Permit2, EIP-2612, EIP-3009, push, AA4337, etc.) and moves funds into custody. Pull schemes enforce exact balance deltas; fee-on-transfer tokens are unsupported.- Router calls
X402FlexRegistry.settleFromRouter(intent, schemeId, referenceData). - Registry transfers funds to the merchant/collector, applies fees/allowlists, and emits
PaymentSettledV2.
Canonical Event
event PaymentSettledV2(
bytes32 indexed paymentId,
address indexed payer,
address indexed merchant,
address token,
uint256 amount,
uint256 feeAmount,
bytes32 schemeId,
string referenceData,
bytes32 referenceHash,
bytes32 resourceId,
uint256 timestamp
);
| Field | Description |
|---|---|
paymentId | Deterministic keccak derived alongside resourceId via createFlexIntent. |
payer | Wallet or contract that ultimately funded the router transfer. |
merchant | Settlement recipient (can be router collector). |
schemeId | keccak256("exact:evm:permit2"), keccak256("push:evm:direct"), etc. |
referenceData | Human-readable reference string (invoice/session tags appended). |
resourceId | Invoice fingerprint; index this when correlating HTTP 402 challenges. |
referenceHash | keccak256(referenceData) after tags are applied. |
timestamp | Block timestamp logged by registry for off-chain SLAs. |
PaymentId binding
paymentId is bound on-chain to the intent terms:
paymentId = keccak256(abi.encode(
PAYMENT_ID_TYPEHASH,
token,
amount,
deadline,
resourceId,
referenceHash,
nonce
))
Always generate a unique per-invoice nonce and persist it alongside paymentId.
SDK Helpers
import { createFlexIntent, buildPaymentTransaction, decodePaymentSettledEvent, X402FlexRouter__factory } from '@pepaylabs/bnbpay';
import { ethers } from 'ethers';
const intent = createFlexIntent({
merchant: '0xMerchant',
token: ethers.ZeroAddress,
amount: ethers.parseEther('0.25'),
chainId: 56,
referenceId: 'order-402-045',
});
const tx = await buildPaymentTransaction(provider, {
recipient: intent.intent.merchant,
chainId: 56,
token: intent.intent.token,
referenceId: intent.referenceId,
}, intent.intent.amount, 'contract', ROUTER_ADDRESS, {
paymentId: intent.paymentId,
resourceId: intent.resourceId,
schemeId: intent.schemeId,
});
const receipt = await signer.sendTransaction(tx);
const settlement = await decodePaymentSettledEvent(receipt.logs[0]);
const router = X402FlexRouter__factory.connect(ROUTER_ADDRESS, signer);
- Push / AA4337
- Permit2
- EIP-2612
- EIP-3009
const router = X402FlexRouter__factory.connect(ROUTER_ADDRESS, signer);
await router.depositAndSettleNative(
intent.intent,
intent.witness,
ethers.ZeroBytes,
intent.referenceId,
{ value: intent.intent.amount }
);
const router = X402FlexRouter__factory.connect(ROUTER_ADDRESS, signer);
await router.payWithPermit2(
intent.intent,
intent.witness,
witnessSignature,
permit2Permit,
transferDetails,
permit2Signature,
intent.referenceId,
);
const router = X402FlexRouter__factory.connect(ROUTER_ADDRESS, signer);
const permitSig = { deadline, v, r, s };
await router.payWithEIP2612(
intent.intent,
intent.witness,
witnessSignature,
permitSig.deadline,
permitSig.v,
permitSig.r,
permitSig.s,
intent.referenceId,
);
import { deriveEip3009Nonce, hashPaymentIntent } from '@pepaylabs/bnbpay';
const router = X402FlexRouter__factory.connect(ROUTER_ADDRESS, signer);
const intentHash = hashPaymentIntent(intent.intent);
const authNonce = deriveEip3009Nonce({
intentHash,
router: ROUTER_ADDRESS,
chainId: 56,
});
await router.payWithEIP3009(
intent.intent,
intent.witness,
witnessSignature,
validAfter,
validBefore,
authNonce,
v,
r,
s,
intent.referenceId,
);
Event Indexing
import { decodePaymentSettledEvent } from '@pepaylabs/bnbpay';
provider.on({ address: PAYMENT_REGISTRY_ADDRESS }, (log) => {
const payment = decodePaymentSettledEvent(log);
console.log('invoice paid', payment.paymentId, payment.resourceId, payment.schemeId);
});
- Store both
paymentIdandresourceIdso HTTP 402 flows, MCP agents, and merchant dashboards can correlate states. schemeIdandreferenceDatalet you differentiate Permit2 pulls, AA pushes, MCP-supplied metadata, and agent-to-agent transfers.- Use
bnbpay-apior your own indexer to fan events into analytics and CRM systems.
Best Practices
- Derive references deterministically; never reuse
paymentIdorresourceId. - Keep the
noncethat produced the invoice’spaymentId; reuse will revert. - For EIP-3009, derive
authNoncefrom the intent hash and router address to prevent replay across intents. - Fee-on-transfer / deflationary tokens are rejected by the router’s balance-delta checks for Permit2/EIP-2612/EIP-3009.
- Apply token allowlists + fee caps from
X402FlexRegistryeven when building off-chain orchestrators. - For AA/push flows, ensure the router address is granted minimal permissions (collector role only).
- Capture gas data +
txHashin your telemetry to debug agent-to-agent payments.