Skip to main content

Relay Endpoints

Relay purpose and trust model

Relay endpoints submit transactions on behalf of clients.

Use relay when:

  • payer signs authorization but does not broadcast transactions
  • your backend handles gas strategy and retries

Shared request envelope

What this does: defines fields common to all relay payment schemes.

{
network: 'bnbTestnet',
scheme: 'permit2' | 'eip2612' | 'eip3009' | 'push_signed',
intent: RelayIntent,
witness: RelayWitness,
witnessSignature: '0x...',
reference: 'order-1001',
session?: { sessionId, agent? },
sessionAuth?: { sessionId, intentHash, schemeId, spendNonce, expiresAt, epoch },
sessionAuthSignature?: '0x...'
}

Expected result: relay can build scheme-correct router calldata.

Shared setup for examples

What this does: prepares reusable relay input values.

import {
createApiClient,
createFlexIntent,
deriveEip3009Nonce,
hashPaymentIntent,
} from '@pepay/x402flex';
import { ethers } from 'ethers';

const ROUTER_ADDRESS = '0xf14f56A54E0540768b7bC9877BDa7a3FB9e66E91';
const TOKEN_ADDRESS = '0x55d398326f99059fF775485246999027B3197955';
const REFERENCE_ID = 'order-1001';

const api = createApiClient({
baseUrl: 'https://api.bnbpay.org',
apiKey: process.env.BNBPAY_RELAY_KEY,
});

const bundle = createFlexIntent({
merchant: '0x1111111111111111111111111111111111111111',
token: TOKEN_ADDRESS,
amount: ethers.parseUnits('25', 18),
chainId: 97,
referenceId: REFERENCE_ID,
scheme: 'exact:evm:permit2',
});

const intent = {
...bundle.intent,
amount: bundle.intent.amount.toString(),
};

const witness = bundle.witness;
const witnessSignature = '0xWITNESS_SIGNATURE';

const eip3009AuthNonce = deriveEip3009Nonce({
intentHash: hashPaymentIntent(bundle.intent),
router: ROUTER_ADDRESS,
chainId: 97,
});

Expected result: all examples below can be executed by replacing signatures and keys.

Permit2 payload

What this does: sends permit and transfer details for payWithPermit2.

await api.relay.payment({
network: 'bnbTestnet',
scheme: 'permit2',
intent,
witness,
witnessSignature,
permit2: {
permit: {
permitted: {
token: TOKEN_ADDRESS,
amount: intent.amount,
},
nonce: 1,
deadline: Math.floor(Date.now() / 1000) + 3600,
},
transferDetails: {
to: ROUTER_ADDRESS,
requestedAmount: intent.amount,
},
signature: '0xPERMIT2_SIGNATURE',
},
reference: REFERENCE_ID,
});

EIP-2612 payload

What this does: sends permit tuple for payWithEIP2612.

await api.relay.payment({
network: 'bnbTestnet',
scheme: 'eip2612',
intent,
witness,
witnessSignature,
eip2612: {
deadline: Math.floor(Date.now() / 1000) + 3600,
v: 27,
r: '0x' + '11'.repeat(32),
s: '0x' + '22'.repeat(32),
},
reference: REFERENCE_ID,
});

EIP-3009 payload

What this does: sends ReceiveWithAuthorization fields for payWithEIP3009.

await api.relay.payment({
network: 'bnbTestnet',
scheme: 'eip3009',
intent,
witness,
witnessSignature,
eip3009: {
validAfter: 0,
validBefore: Math.floor(Date.now() / 1000) + 3600,
authNonce: eip3009AuthNonce,
v: 27,
r: '0x' + '33'.repeat(32),
s: '0x' + '44'.repeat(32),
},
reference: REFERENCE_ID,
});

Important:

  • authNonce must be intent-derived (intent hash + router + chain ID).

Session relay payloads

Use these endpoints for session lifecycle:

  • POST /relay/session/open
  • POST /relay/session/open-claimable
  • POST /relay/session/claim
  • POST /relay/session/revoke

For session spends through /relay/payment, include session, signed sessionAuth, and sessionAuthSignature.

Error codes and operator response

  • 400 validation failure:
    • malformed payload or missing scheme fields
  • 401/403 auth failure:
    • invalid/missing API key
  • 422 semantic failure:
    • expired auth, session mismatch, nonce mismatch
  • 5xx transient failure:
    • retry with status check and idempotent operation key