Submit Payment
POST /v1/payments — submit a signed payment intent to the Axon relayer.
Submit Payment
Submit a signed EIP-712 payment intent for execution. The relayer validates the signature, runs policy checks, and either executes the payment on-chain or routes it through AI verification or human review.
Request Body
Signed Intent Fields
These fields are part of the EIP-712 signed struct and are verified on-chain by the vault contract.
| Parameter | Type | Required | Description |
|---|---|---|---|
| bot | address | Required | The bot address that signed this intent. Must be whitelisted on the vault. |
| to | address | Required | Recipient address (EOA or contract). |
| token | address | Required | ERC-20 token contract address (e.g. USDC). |
| amount | string | Required | Payment amount in the token's base units as a decimal string (e.g. "10000000" for 10 USDC). |
| deadline | string | Required | Unix timestamp as a decimal string. The intent is invalid after this time. Recommended: 5 minutes from now. |
| ref | string | Required | bytes32 hex string (0x-prefixed, 66 characters). Links the on-chain transaction to off-chain records. |
| signature | string | Required | EIP-712 signature over the PaymentIntent struct, signed by the bot's private key. |
Routing Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| chainId | number | Required | The EVM chain ID where the vault is deployed (e.g. 8453 for Base, 42161 for Arbitrum). |
| vaultAddress | address | Required | The AxonVault contract address to execute the payment from. |
Off-Chain Metadata
These fields are stored in the relayer's database and surfaced in the owner's dashboard. They are not signed and not sent on-chain.
| Parameter | Type | Required | Description |
|---|---|---|---|
| idempotencyKey | string | Required | UUID (v4 recommended). Ensures at-most-once execution. Re-submitting the same key returns the cached response. |
| memo | string | Optional | Human-readable description of the payment purpose. Max 1000 characters. Analyzed by the Reasoning AI agent if verification is triggered. |
| resourceUrl | string (URL) | Optional | The URL or resource being unlocked by this payment. Used in HTTP 402 flows. Stored in audit trail. |
| invoiceId | string | Optional | External invoice reference for reconciliation. Max 255 characters. |
| orderId | string | Optional | External order or job ID. Max 255 characters. |
| metadata | object | Optional | Arbitrary key-value pairs (string keys and string values). Up to 10 keys, max 500 characters per value. |
| simulate | boolean | Optional | If true, the relayer simulates the payment via eth_call but does not execute it on-chain. Returns a simulationResult field. |
Response
{
requestId: string;
status: "approved" | "pending_review" | "rejected";
txHash?: string;
pollUrl?: string;
estimatedResolutionMs?: number;
reason?: string;
simulationResult?: {
success: boolean;
gasEstimate?: string;
error?: string;
};
}| Parameter | Type | Required | Description |
|---|---|---|---|
| requestId | string | Required | Unique identifier for this payment request. Use with GET /v1/payments/:requestId to poll status. |
| status | string | Required | Current status: "approved" (on-chain), "pending_review" (awaiting AI or human decision), or "rejected" (denied). |
| txHash | string | Optional | On-chain transaction hash. Present when status is "approved". |
| pollUrl | string | Optional | Relative URL for polling. Present when the payment is pending async resolution. |
| estimatedResolutionMs | number | Optional | Estimated time to resolution in milliseconds. Typically ~30000 for AI scan path. Absent for human review. |
| reason | string | Optional | Human-readable explanation for rejection. Present when status is "rejected". |
| simulationResult | object | Optional | Present when simulate: true was set. Contains success boolean, gas estimate, and error details. |
Response Paths
The relayer evaluates every payment against the vault's policy configuration and routes through one of three paths:
Fast Path
The payment is below all thresholds and passes all policy checks. The relayer submits the transaction on-chain and returns synchronously.
Conditions: amount below AI trigger threshold, no velocity triggers, destination allowed.
{
"requestId": "req_7f2a9b",
"status": "approved",
"txHash": "0x1234567890abcdef..."
}AI Scan Path
The payment exceeds the AI trigger threshold or velocity window. The relayer runs 3-agent verification (Safety, Behavioral, Reasoning). If 2/3 agents approve, the payment executes. If there is no consensus, it routes to human review.
Conditions: amount above aiTriggerThreshold, or rolling velocity exceeded, or requireAiVerification is true for this bot.
{
"requestId": "req_7f2a9b",
"status": "approved",
"txHash": "0x1234567890abcdef...",
"estimatedResolutionMs": 28000
}Or, if no consensus:
{
"requestId": "req_7f2a9b",
"status": "pending_review",
"pollUrl": "/v1/payments/req_7f2a9b"
}Human Review Path
The payment requires manual approval by the owner via the dashboard. This occurs when AI agents do not reach consensus, or when the owner has configured mandatory human review for the bot.
{
"requestId": "req_7f2a9b",
"status": "pending_review",
"pollUrl": "/v1/payments/req_7f2a9b"
}Poll GET /v1/payments/:requestId to check when the owner approves or rejects the payment.
Error Responses
| Status | Code | Description |
|---|---|---|
400 | INVALID_REQUEST | Malformed request body or missing required field. |
400 | INVALID_SIGNATURE | EIP-712 signature verification failed. |
400 | DEADLINE_EXPIRED | The intent's deadline has already passed. |
403 | BOT_NOT_ACTIVE | The bot address is not whitelisted on the vault. |
403 | EXCEEDS_PER_TX_LIMIT | Amount exceeds the bot's maxPerTxAmount. |
409 | IDEMPOTENCY_CONFLICT | Same idempotency key submitted with different intent data. |
422 | INSUFFICIENT_BALANCE | Vault does not have enough token balance. |
422 | SIMULATION_FAILED | Pre-flight eth_call simulation reverted. |
429 | RATE_LIMITED | Too many requests. Check the Retry-After header. |
See Error Codes for the complete reference.
Example
curl
curl -X POST https://relay.axonfi.xyz/v1/payments \
-H "Content-Type: application/json" \
-d '{
"bot": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000000",
"deadline": "1740200000",
"ref": "0x696e762d30303100000000000000000000000000000000000000000000000000",
"signature": "0xabcdef...",
"chainId": 8453,
"vaultAddress": "0xYourVaultAddress",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"memo": "Payment for 100 API calls",
"metadata": {
"service": "image-generation",
"requestCount": "100"
}
}'TypeScript (without SDK)
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
const account = privateKeyToAccount('0xYourBotPrivateKey');
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const intent = {
bot: account.address,
to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
amount: 10_000_000n,
deadline: BigInt(Math.floor(Date.now() / 1000) + 300),
ref: '0x696e762d30303100000000000000000000000000000000000000000000000000',
};
const signature = await walletClient.signTypedData({
domain: {
name: 'AxonVault',
version: '1',
chainId: 8453,
verifyingContract: '0xYourVaultAddress',
},
types: {
PaymentIntent: [
{ name: 'bot', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
{ name: 'ref', type: 'bytes32' },
],
},
primaryType: 'PaymentIntent',
message: {
bot: intent.bot,
to: intent.to,
token: intent.token,
amount: intent.amount,
deadline: intent.deadline,
ref: intent.ref,
},
});
const response = await fetch('https://relay.axonfi.xyz/v1/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...Object.fromEntries(Object.entries(intent).map(([k, v]) => [k, String(v)])),
signature,
chainId: 8453,
vaultAddress: '0xYourVaultAddress',
idempotencyKey: crypto.randomUUID(),
memo: 'Payment for 100 API calls',
}),
});
const result = await response.json();
console.log(result);Notes
- The relayer always simulates the transaction via
eth_callbefore submitting on-chain. If simulation fails, no gas is spent and you receive a422 SIMULATION_FAILEDerror. - The
deadlinefield should be set to a short window (default 5 minutes). Long deadlines increase the risk of stale intents being executed at unfavorable times. - The
reffield is the only piece of off-chain context that is recorded on-chain. Use it as the anchor linking on-chain transactions to your off-chain records.