Overview
Send a cross-rail payment using the Payment Gateway CLI. The CLI wraps the same prepare → sign → submit flow you would use with the SDK.This guide walks through one example corridor — USDC on Arbitrum Sepolia → USDC on Celo Sepolia. Atum supports many source and destination combinations across Arbitrum, Base, Solana, Tron, and more. See Supported networks for the full list.
1
Approve token spend
One-time setup for your wallet on the source chain — not before every payment.
2
Run send-payment
The CLI builds, signs, and submits your payment in one command.
3
Settlement operators quote your payment
Settlement operators compete to fulfill your payment — no action needed from you.
4
Funds move across both rails
Settlement completes. Poll
/status to confirm.New to tokenized currency payments? Read Atum infrastructure first — about five minutes.
Prerequisites
The following is required to use Atum to send a payment:Node.js 20+ and npm
Node.js 20+ and npm
Access to Atum's Payment Gateway
Access to Atum's Payment Gateway
Confirm you can reach the hosted testnet API from your machine:A successful check returns HTTP 200 and one entry per chain the gateway serves, keyed by CAIP-2 id:This tells you two things at once: the gateway is up, and which chains are enabled — not which source→destination pairs you can use. A chain that isn’t fully configured is left out of this listing, so what comes back is what you can actually pay on. Fetch these addresses rather than hardcoding them; they change when a chain’s contracts are redeployed. A non-200 is a gateway-side answer, not a network problem: 404 means it resolved no chains and 503 means a dependency is down — in both cases the gateway is up and you have nothing to fix locally.The Payment Gateway is an HTTP API, not a blockchain network.
A funded testnet wallet
A funded testnet wallet
This example uses an EVM wallet funded on Arbitrum Sepolia. Any EVM-compatible wallet works (MetaMask, Coinbase Wallet, Rainbow, etc.).You will need:
- Testnet ETH for the one-time token approval on the source chain (settlement gas is the settlement operator’s)
- A small amount of testnet USDC on the source chain
0x address for sender and receiver. See Testnet wallet setup if you need help getting funded.1. Install the CLI
send-payment, payment-status, request-quote, quote-status and approve-permit2. Skip the -g and use npx send-payment … if you would rather not install globally.
2. Configure addresses and key
Create.env in the directory you’ll run the commands from.
You will need your wallet’s private key and address (in this example, we use the same address for sender and receiver):
Example (replace with your own values):
Using a MetaMask wallet?
Using a MetaMask wallet?
Your MetaMask address and private key belong to the account, not to a network.The network dropdown (Ethereum, Arbitrum Sepolia, etc.) only changes which chain you are viewing — not your credentials.Balances change by network; your
0x address and key do not.Copy your address
Click the copy icon next to your default Metamask address.Paste that value intoSENDER and RECEIVER in your .envExport your private key
- In MetaMask, click the three dots (⋮) next to your account name.
- Click Account details.
- Copy your account private key (it’s a long hex string starting with
0x). - Enter your MetaMask password and confirm.
PRIVATE_KEY in your .env.3. Approve USDC spend
This step is required for EVM and Tron source chains.
Technical name: Permit2
Technical name: Permit2
Atum uses the standard Permit2 contract on EVM and Tron chains — the same approval pattern many DeFi apps use. Your USDC moves into the settlement vault only after you sign a payment.Details: Token approvals.
send-payment. The private key comes from PRIVATE_KEY or an interactive prompt and is never accepted as a flag, which would put it in shell history and in the process list. Add --check to report whether an approval is needed without sending anything or needing a key at all.
An approval is unlimited by default, so later payments need no further transaction. --amount bounds it instead, at the cost of renewing it as Permit2 consumes it.
This is the one command that spends gas. Everything else the CLI does builds and signs offline.If the transaction is broadcast but not confirmed before the timeout, the command exits non-zero and prints
{"unconfirmed": true, "txHash": …}. That is not a revert — the approval may still land. Look that transaction up before re-running, rather than sending a second approval.Approving by hand
You don’t have to use Atum’s command. The approval is an ordinary ERC-20approve of the Permit2 contract — nothing Atum-specific — so any tool that can call it works.
From a block explorer or Remix
From a block explorer or Remix
Use spender
0x000000000022D473030F116dDEE9F6B43aC78BA3 (the standard Permit2 contract) and an amount in 6-decimal units (for example 1000000000 = 1000 USDC). Circle testnet USDC is a proxy — on Arbiscan use Write as Proxy, not Write Contract.From your own script
From your own script
Save as Three things are going on: connect to the source chain, call
accept-permit2.ts and run it once with npx tsx accept-permit2.ts:approve on the USDC contract, and name Permit2 as the spender allowed to pull USDC when a payment settles. No money moves — it is a permission slip, and you still sign each payment separately. 1_000_000_000n is a 1000 USDC ceiling for test runs.4. Send your first payment
With USDC spend approved, you are ready to submit a payment. Run the following to load your .env file first:.env is loaded, run:
send-payment
What's in this command?
What's in this command?
send-payment takes positional arguments in this order: sender, amount, source asset, receiver, destination asset.What are the eip155:… strings?
Those are asset IDs — one string that identifies an exact token on an exact chain. The CLI does not accept symbols like USDC; it needs the full ID. Different corridors use different asset IDs — copy them from Supported assets.Technical name: CAIP-19
Technical name: CAIP-19
Asset IDs follow the CAIP-19 standard. Chain prefixes use CAIP-2.Full catalog: Supported assets. Concept page: Corridors and assets.
Why are the asset IDs already filled in?
This quickstart pins one route so you can focus on the payment flow — not hunting token addresses at runtime.GET /v1/defaultslists which chains are enabled and returns their on-chain contract addresses — not which tokens or asset IDs to use, and not “USDC on Arbitrum Sepolia.”- Token contract addresses come from Supported assets.
Amount (atomic units)
Amount (atomic units)
The second argument to
send-payment is not dollars — it is USDC in 6-decimal atomic units (smallest on-chain increment):Use a small test amount (for example
10000) to conserve testnet USDC. If settlement fails with NO_QUOTES_RECEIVED, read decline_summary before retrying (see Why nobody quoted).- stderr (PaymentRequest)
- stdout (PaymentResponse)
The client logs the signed request with The CLI fills
console.error before submitting:escrow_contract_address and fulfillment_verifier from /v1/defaults for the source chain. The gateway requires both on every payment request.Followed by the recovery line. Copy the request_id from here if you need to retry:payment_id from stdout.
By default, the CLI does not poll after submit — send-payment waits as long as the gateway holds the submit call (see Track a payment), then prints the PaymentResponse above and exits.
Pass --wait if you’d rather have the CLI poll for you: send-payment then blocks on /status itself and prints the payment JSON once the payout is mined (finalizing), the payment fails or is refunded, the refund is ready (refund_available), or --wait-seconds expires. Add --wait-seconds <n> to change the default 60-second cap — giving up does not affect the payment itself, it’s a convenience bound only.
To wait until the payout is final (completed) instead, pass --wait-for completed and raise --wait-seconds: on some destination chains that takes tens of minutes. See Finality.
The exit code tells the endings apart, with or without --wait, so don’t branch on “zero or not” — a wait that runs out its budget is not a failure, and 5 means paid:
Review before you send
send-payment in one shot builds, signs and submits. You can also split it in two and read the signed request first:
--prepare-only prints is byte-for-byte what that run would have POSTed, so it can be reviewed or approved before any money moves.
--prepare-only still calls the gateway — that is where the corridor addresses come from — so it is not an offline signing mode. The exit codes above describe a submitted payment, so they don’t apply to a --prepare-only run, where 0 means the request was built and signed and no payment exists. Don’t chain a payment-conditional step off one.
The other commands
Every command takes
--gateway, and both send-payment and request-quote take --request-id (or --request-id-prefix to change the generated one’s prefix). Run any of them with --help for the full flag list.
5. Check payment status
This CLI flow talks to the Payment Gateway directly, so you poll/status after submit. That is the right pattern here.
For x402 / MPP HTTP payments, prefer re-attempting the same purchase when settlement is still running; keep /status for dashboards and out-of-band reconciliation.
Copy payment_id from stdout in step 4, then poll every few seconds:
/status response tells you how far settlement got. Once status is finalizing, the payout is mined and your recipient has the USDC: its transaction is in transactions.fulfillment. The payment becomes completed once the payout is final on the destination chain. On this quickstart’s route (Celo Sepolia) that takes tens of minutes; see Finality. To follow it to the end, poll every few seconds until status is completed, failed or refunded.
What each status means: Payment status.
completed, failed and refunded are the only statuses that end a payment. Treat anything else as still in flight and keep polling — in particular, do not stop on a status you do not recognize, because giving up on a live payment is how a second charge happens.Any payment can pass through finalizing, and it is not terminal. If you write an exhaustive switch on the SDK’s PaymentStatus type, you need a branch for it.payment_id from step 4 only proves submit succeeded. This step confirms settlement.
Paid, then completed
While the payment isfinalizing, look up the payout from transactions.fulfillment in a Celo Sepolia block explorer: USDC has arrived. Once the payout is final the status is completed. transactions lists the deposit on the source chain and the payout on the destination chain:
Still settling
Delivery usually takes seconds on testnet. Whilestatus is pending, keep polling — do not resubmit the same payment under a new request_id. A finalizing payment is already paid; it is only waiting for the destination chain to make the payout final.
Failed (no route available)
If no settlement operator offers a price in time, you may seeNO_QUOTES_RECEIVED. Your integration still worked — submit and signing succeeded. Read decline_summary in the status response to see why (see Why nobody quoted). An asset the gateway does not serve is refused at submit, with UNSUPPORTED_SOURCE_ASSET or UNSUPPORTED_DESTINATION_ASSET. An asset it serves but you did not mean, such as the right token on the wrong chain, passes submit and fails later, so also check your asset IDs against Supported assets. A payment that no one quoted looks like this:
Troubleshooting
Success criteria
You completed this quickstart when:-
GET /v1/defaultsreturns chain defaults for the testnet gateway - Approve USDC spend confirmed on Arbitrum Sepolia
-
send-paymentreturns apayment_idon stdout -
GET /v1/payments/{payment_id}/statusreturns"status": "finalizing"or"completed", with the payout intransactions.fulfillment
NO_QUOTES_RECEIVED on testnet does not invalidate the first three.