First Payment
End-to-end walkthrough of making your first payment through an Axon vault.
First Payment
This guide walks through a complete payment from start to finish, covering initialization, sending, handling the three possible response paths, and confirming the result on-chain.
Prerequisites
Before starting, ensure you have:
- A deployed vault with USDC deposited (Deploy a Vault)
- A registered bot with its address whitelisted on the vault (Register a Bot)
- The bot's private key available in your environment
@axonfi/sdkinstalled (npm install @axonfi/sdk)
Step 1: Initialize the SDK
import { AxonClient, decryptKeystore, Chain, Token, encodeRef } from '@axonfi/sdk';
import fs from 'fs';
// Decrypt bot key from encrypted keystore file
const keystore = fs.readFileSync(process.env.AXON_BOT_KEYSTORE_PATH!, 'utf8');
const botPrivateKey = await decryptKeystore(keystore, process.env.AXON_BOT_PASSPHRASE!);
const axon = new AxonClient({
botPrivateKey,
vaultAddress: process.env.AXON_VAULT_ADDRESS as `0x${string}`,
chainId: Chain.Base,
});The AxonClient derives your bot's public address from the private key. The decrypted key stays in memory and is used only for EIP-712 signing — it is never sent to the relayer.
Step 2: Create and Send a Payment
const result = await axon.pay({
to: '0xRecipientAddress',
token: 'USDC',
amount: 25, // 25 USDC — SDK handles decimals
ref: encodeRef('invoice-042'), // bytes32 on-chain reference
memo: 'Payment for data processing job #42',
});Under the hood, the SDK:
- Constructs the
PaymentIntentstruct with your bot address, a deadline (default: 5 minutes from now), and the provided fields - Signs the struct as EIP-712 typed data using your bot's private key
- Generates an idempotency key (UUID) to prevent double-processing
- Submits the signed intent to the relayer at
POST /v1/payments
Step 3: Handle the Response
The response shape depends on which path the relayer takes.
Fast Path (Synchronous)
If the payment is below all policy thresholds, you get a confirmed transaction hash immediately:
if (result.status === 'approved') {
console.log('Payment confirmed on-chain!');
console.log('Transaction:', result.txHash);
// Done — no further action needed
}AI Scan Path (Short Async)
If the payment triggered AI verification (amount exceeded aiTriggerThreshold or velocity window), the relayer holds the request while the three agents vote. This typically resolves within 30 seconds.
if (result.status === 'pending_review' && result.estimatedResolutionMs) {
console.log('AI verification in progress...');
console.log('Estimated wait:', result.estimatedResolutionMs, 'ms');
// Poll until resolved
let final = await axon.poll(result.requestId);
while (final.status === 'pending_review') {
await new Promise((r) => setTimeout(r, 3_000));
final = await axon.poll(result.requestId);
}
if (final.status === 'approved') {
console.log('Payment approved by AI and confirmed:', final.txHash);
} else {
console.log('Payment rejected:', final.reason);
}
}Human Review Path (Long Async)
If AI agents did not reach consensus, or if the bot is configured with requireAiVerification: true and manual review, the payment enters the owner's review queue.
if (result.status === 'pending_review') {
console.log('Payment requires human approval.');
console.log('Request ID:', result.requestId);
console.log('Poll URL:', result.pollUrl);
// Poll periodically until resolved
let checkLater = await axon.poll(result.requestId);
while (checkLater.status === 'pending_review') {
await new Promise((r) => setTimeout(r, 30_000));
checkLater = await axon.poll(result.requestId);
}
}Step 4: Complete Response Handler
Here is a robust handler that covers all three paths:
async function handlePayment(result: PaymentResult): Promise<string | null> {
switch (result.status) {
case 'approved':
// Fast path — already confirmed
return result.txHash;
case 'pending_review':
// AI scan or human review — poll until resolved
let resolved = await axon.poll(result.requestId);
while (resolved.status === 'pending_review') {
await new Promise((r) => setTimeout(r, 5_000));
resolved = await axon.poll(result.requestId);
}
return resolved.status === 'approved' ? resolved.txHash : null;
default:
return null;
}
}
const txHash = await handlePayment(result);
if (txHash) {
console.log('Confirmed:', txHash);
}Step 5: Verify On-Chain
Once you have a txHash, you can confirm the transfer on a block explorer:
- Base:
https://basescan.org/tx/{txHash} - Arbitrum:
https://arbiscan.io/tx/{txHash} - Base Sepolia:
https://sepolia.basescan.org/tx/{txHash}
Or verify programmatically:
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const receipt = await publicClient.getTransactionReceipt({
hash: txHash as `0x${string}`,
});
console.log('Block number:', receipt.blockNumber);
console.log('Status:', receipt.status); // "success"Using the Raw API
If you are not using the TypeScript SDK, you can call the relayer API directly:
| Parameter | Type | Required | Description | |---|---|---|---| | bot | string | Yes | Bot address that signed
the intent | | to | string | Yes | Recipient address | | token | string | Yes | ERC-20 token address | |
amount | string | Yes | Amount in base units (e.g. "25000000" for 25 USDC) | | deadline | string | Yes |
Unix timestamp as decimal string | | ref | string | Yes | bytes32 hex string (0x-prefixed) | | signature |
string | Yes | EIP-712 signature of the intent | | chainId | number | Yes | Target chain ID (e.g., 8453
for Base) | | vaultAddress | string | Yes | The vault contract address | | idempotencyKey | string | Yes |
UUID to prevent double-processing | | memo | string | No | Human-readable payment purpose | | metadata |
object | No | Arbitrary key-value pairs (max 10 keys) |
curl -X POST https://relay.axonfi.xyz/v1/payments \
-H "Content-Type: application/json" \
-d '{
"bot": "0xBotAddress",
"to": "0xRecipient",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "25000000",
"deadline": "1756339200",
"ref": "0x696e766f6963652d303432000000000000000000000000000000000000000000",
"signature": "0x...",
"chainId": 8453,
"vaultAddress": "0xYourVault",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"memo": "Payment for data processing job #42"
}'Next Steps
- HTTP 402 Payments — integrate with pay-per-request APIs
- How It Works — understand what happens under the hood
- Key Concepts — reference for all terms used here