Buying and Selling
Every trade takes the same four calls. The options you add to the quote change three things: which kinds of legs come back, what each leg asks you to sign, and who pays the gas. This page maps every combination of priority, execution and payout_chain to what you get, so you can pick the right one and handle any quote it returns. The chain-by-chain rules behind each option are in Routing and Priority Mode.
The four calls
- Quote.
POST /quote/buywithamount_usdc, orPOST /quote/sellwithamount_shares, plus theticker, the wallets, an ownership proof and any of the options below. You get aquote_id, anexpires_at,warnings[]andquotes[]: one leg per chain, each carrying thesignable_payloadsto sign. - Sign. Sign every payload of the legs you will submit, each as its
typesays (below). - Submit.
POST /trade/submitwith thequote_idand, for each leg, itsquote_indexandsigned_payloads. Treasures broadcasts them. - Track. Poll
GET /quote/{quote_id}/statusat thepoll_after_msit returns until every leg is terminal.
Which legs you submit is the one rule that differs by side.
- Buy: the legs are alternatives, best price first. Submit exactly one
quote_index, usually0. Two or more is400 quote_index_mismatch. - Sell: the legs are parts of one sale, each selling a slice of your holdings. Submit every leg. A subset is
400 incomplete_submit; to sell from fewer chains, re-quote withchain.
Submit before expires_at. To price a buy with no wallet attached, POST /quote/preview takes the same options (it needs an integrator key) and returns the legs with nothing to sign.
The options
| Option | Values | What it changes |
|---|---|---|
chain, protocol | One value (a pin) or an array (a subset) | Which venues are considered. Default: every one the ticker lists |
preferred_chain | A chain inside your chain scope | Order only: that chain's leg comes first on a buy, and is filled first on a sell |
priority | "speed" | One-block swaps on Robinhood Chain and Base instead of gasless orders, and cross-chain buys |
execution | "transaction" (default) or "user_operation" | With priority: "speed" only: EVM legs come back as one call bundle your own paymaster sponsors |
payout_chain | "sol", "eth" or "base" | Sells only: the sale's USDC lands on that chain, and positions elsewhere can sell cross-chain |
integrator_fee_bps | 0 to your ceiling, in net bps | With your integrator key only: your own fee on this quote, instead of your configured default. See Integrator Fees and Payouts |
These combinations are refused up front with 400 invalid_request:
execution: "user_operation"withoutpriority: "speed".payout_chainon/quote/buyor/quote/preview.priority: "speed"withchain: "arbitrum", which has no speed route.priority: "speed"withchain: "eth"on a sell, unlesspayout_chainnames another chain.- A
preferred_chainoutside thechainyou sent.
The kinds of leg
Whatever you ask for, each leg that comes back is one of these. The rest of the page names them.
| Leg | What you sign | Who pays gas |
|---|---|---|
| Swap (Solana) | One solana_versioned_tx | Usually the wallet, in SOL; gasless on the leg says which |
| Gasless order | One evm_eip712_typed_data | Nobody on your side: a settlement network broadcasts it |
| Speed swap | One or two EVM transactions: an approve (first time only), then the swap | The wallet, in native ETH on that chain |
| Sponsored swap | One evm_calls bundle, packed into an ERC-4337 UserOperation | Your paymaster |
Cross-chain buy (origin_chain set) | The origin chain's shape: a Solana transaction, an approve and deposit pair, or one evm_calls | The wallet on the origin chain, or your paymaster when sponsored |
Cross-chain sell (payout_chain set) | On Ethereum and Base, an approve and deposit pair, or one evm_calls when sponsored; on Solana, one transaction | The wallet on the chain the stock is sold on, or your paymaster when sponsored |
Every leg carries gasless: true when nobody on your side pays (a gasless order, or your paymaster), false when the wallet does. It can be absent on a Solana leg, and absent is not false.
Buying: what each combination returns
| You add | Solana | Ethereum | Robinhood Chain | Base |
|---|---|---|---|---|
| Nothing | Swap | Gasless order | Gasless order | Gasless order |
priority: "speed" | Swap, or cross-chain buy | Cross-chain buy only | Speed swap | Speed swap, or cross-chain buy |
priority: "speed" and execution: "user_operation" | Swap, or cross-chain buy | Cross-chain buy only | Sponsored swap | Sponsored swap, or cross-chain buy |
- A cross-chain buy is offered only on a chain the wallet is not funded on, and only into
sol/xstocks,eth/ondoand, on an unpinned or multi-chain request,base/coinbase. The funds leave from another chain where the wallet does hold enough, so send bothsol_walletandeth_wallet. - Arbitrum, once its venue opens, returns a gasless order by default and is never quoted under speed.
- Choose the default for the best price with no native gas on the EVM chains. Choose speed when settlement inside one block matters, or when the user's USDC sits on a different chain from the listing. Add
user_operationwhen you run an ERC-4337 stack and would rather your paymaster paid than hold native gas.
Selling: what each combination returns
Without payout_chain, each position sells on its own chain:
| You add | Solana | Ethereum | Robinhood Chain | Base |
|---|---|---|---|---|
| Nothing | Swap | Gasless order | Gasless order | Gasless order |
priority: "speed" | Swap | Not sold | Speed swap | Speed swap |
priority: "speed" and execution: "user_operation" | Swap | Not sold | Sponsored swap | Sponsored swap |
With payout_chain, all of the USDC lands on that chain, in the wallet you sent for it (sol_wallet on sol, eth_wallet on eth and base). Each position is planned by where it sits, whatever the priority:
| Position | On the payout chain | On another chain |
|---|---|---|
sol/xstocks, base/coinbase | Sells as in the table above | Cross-chain sell |
eth/ondo | Sells as in the table above (so not under speed) | Cross-chain sell |
sol/ondo, eth/xstocks | Sells as in the table above | Left out, with a warning |
| Robinhood Chain, Arbitrum | Never a payout chain | Left out, with a warning |
A position that cannot pay out on payout_chain is never sold on its own chain instead, because that would pay out somewhere you did not ask for. It is named by a cross_chain_route_unavailable warning. Adding priority: "speed" and execution: "user_operation" turns the Ethereum and Base cross-chain sells into sponsored evm_calls bundles.
Handling a quote
A single quote can mix leg kinds. An unpinned speed buy, for example, can return a Solana swap, a Base speed swap and an Ethereum cross-chain buy side by side. So handle each leg on its own: read what it is, then sign each payload by its type.
Recognise the leg
| On the leg | It is | Read it as |
|---|---|---|
payout_chain present | A cross-chain sell | chain is where the stock is sold; the USDC lands on payout_chain |
origin_chain present | A cross-chain buy | chain is the destination; the funds leave origin_chain, and base_asset names that chain's currency |
| Neither | A same-chain leg | chain is where everything happens |
A cross-chain leg also carries cross_chain_cost_bps and cross_chain_cost_usdc, the hop's own cost, and its cost_breakdown_bps.dex_swap_fee_bps is null.
Sign by payload type
Sign every payload in signable_payloads, in the order issued:
Payload type | How to sign it | Return |
|---|---|---|
solana_versioned_tx | Deserialize tx_base64 and sign with the wallet's transaction signer, not signMessage | solana_versioned_tx with signed_tx_base64 |
evm_eip712_typed_data | eth_signTypedData_v4 over typed_data, exactly as given | evm_eip712_signature with signature |
evm_eip1559_tx, evm_legacy_tx | signTransaction over tx_hex as issued; never change the nonce, gas or fees | evm_signed_tx with signed_tx_hex, one per payload |
evm_calls | Pack calls into one ERC-4337 v0.7 UserOperation for sender on chain_id (execute for one call, executeBatch in order for two), sponsor it with your paymaster, sign it | evm_user_operation with user_operation |
Rules that hold for every leg:
- Branch on
type, never onchainor on the options you sent. - Return every payload, in order. Two EVM transactions on one leg (an
approve, then theswapordeposit) are normal. Returning only one isincomplete_submit, and reordering them on a retry is read as a new submit. - Never broadcast a trade payload yourself. You sign; Treasures sends it. The only transaction you broadcast is a bridge.
- Know what an approve grants. A speed swap's
approvegives the router an unlimited allowance on the token spent, once per wallet, token and chain, so surface that consent to your user. A cross-chain leg'sapproveis for the exact amount, and on a sell it comes every time. - The sponsored lane needs a delegated wallet. The wallet must be EIP-7702-delegated to Modular Account v2 on that chain, and the operation must carry a paymaster. The lane serves Base and Robinhood Chain; a chain it cannot serve at the moment is dropped with
speed_route_unavailableandreason: "disabled".
A signing loop that handles every combination above, whatever options were sent:
import { VersionedTransaction } from '@solana/web3.js';
import { parseTransaction } from 'viem';
// `signers` is yours: a Solana transaction signer, an EVM account that signs typed data and
// transactions without sending them, and (sponsored lane only) your ERC-4337 account SDK.
async function signLeg(leg: QuoteLeg, signers: Signers) {
const signedPayloads = [];
for (const payload of leg.signable_payloads) {
switch (payload.type) {
case 'solana_versioned_tx': {
const unsigned = VersionedTransaction.deserialize(Buffer.from(payload.tx_base64, 'base64'));
const signed = await signers.solana.signTransaction(unsigned);
signedPayloads.push({
type: 'solana_versioned_tx',
signed_tx_base64: Buffer.from(signed.serialize()).toString('base64'),
});
break;
}
case 'evm_eip712_typed_data': {
const { domain, types, primaryType, message } = payload.typed_data;
const { EIP712Domain: _domainType, ...orderTypes } = types; // viem adds the domain type itself
const signature = await signers.evm.signTypedData({ domain, types: orderTypes, primaryType, message });
signedPayloads.push({ type: 'evm_eip712_signature', signature });
break;
}
case 'evm_eip1559_tx':
case 'evm_legacy_tx': {
const signedTxHex = await signers.evm.signTransaction(parseTransaction(payload.tx_hex));
signedPayloads.push({ type: 'evm_signed_tx', signed_tx_hex: signedTxHex });
break;
}
case 'evm_calls': {
// Build one UserOperation from payload.calls for payload.sender on payload.chain_id,
// run your paymaster's sponsorship, sign it, and return the unpacked v0.7 fields.
const userOperation = await signers.userOperation.buildSponsorAndSign(payload);
signedPayloads.push({ type: 'evm_user_operation', user_operation: userOperation });
break;
}
default:
throw new Error(`Unrecognised payload type ${payload.type}; update your integration`);
}
}
return { quote_index: leg.quote_index, signed_payloads: signedPayloads };
}
Submit one signed leg for a buy (signed: [await signLeg(quote.quotes[0], signers)]), and every leg for a sell.
Recipes
| Goal | Add to the quote request | What to expect |
|---|---|---|
| Cheapest buy, no native gas on EVM | Nothing | quotes[0] is the best price |
| Buy on one chain only | "chain": "base" | One leg, or 422 no_routes |
| Prefer a chain, keep the fallbacks | "preferred_chain": "sol" | That chain's leg is quotes[0] |
| Settle in one block | "priority": "speed" | A speed swap, with an approve the first time |
| Settle in one block, no native gas | "priority": "speed", "execution": "user_operation" | One evm_calls per EVM leg |
| Buy where the user holds no USDC | "priority": "speed", both wallets | A leg with origin_chain set |
| Sell, all USDC to Solana | "payout_chain": "sol" | Cross-chain legs carry payout_chain; submit every leg |
| Sell to USDC on Base, no native gas | "payout_chain": "base", "priority": "speed", "execution": "user_operation" | Ethereum and Base legs as evm_calls |
The request for the last recipe, with the proof built as in Ownership Proof:
{
"ticker": "NVDA",
"amount_shares": "2.5",
"sol_wallet": "7xKXtg2C…",
"eth_wallet": "0xab5801a7…",
"payout_chain": "base",
"priority": "speed",
"execution": "user_operation",
"max_slippage_bps": 100,
"ownership_proof": {
"issued_at": 1750000000,
"sol_signature": "<base64 Ed25519 signature>",
"eth_signature": "0x<EIP-191 signature>"
}
}
Tracking the trade
Poll GET /quote/{quote_id}/status at the poll_after_ms it returns: 1250 while a speed swap is in flight, 3250 otherwise. Which handle identifies a leg depends on its kind:
| Leg | What to watch |
|---|---|
| Swap (Solana) | tx_hash, usually final at submit. On broadcast_unknown, do not resubmit: poll |
| Gasless order | order_hash until the order fills, then tx_hash |
| Speed swap | tx_hash, set from submit; on an approve and swap pair it lands once the approve mines |
| Sponsored swap | user_op_hash; tx_hash is filled in once the operation is included |
| Cross-chain sell | tx_hash is the sale, payout_tx_hash the payout, set on completed. The completed leg's payout_chain is where the USDC landed: it equals chain if the payout was returned there |
| Cross-chain buy | The leg's status. cross_chain_unsettled after 6 hours means wait, because it may still fill or refund; do not re-quote |
Resubmitting the same signed payloads for the same quote_index is safe and returns the original result. Only one speed trade per wallet per chain can be in flight at a time; wait for it to finish before quoting the next.
When a quote comes back short
| You see | What to do |
|---|---|
speed_route_unavailable warning | A Robinhood Chain or Base leg was dropped under speed; its reason says why. Take another leg, fix the reason, or re-quote without priority |
cross_chain_route_unavailable warning | A listing could not be reached cross-chain, or (with payout_chain) a position was left out. Choose another leg or payout chain, or re-quote later |
422 no_routes, reason: "insufficient_balance" | The wallet's own balance refused a speed request. Fund the token spent on that chain |
422 no_routes, reason: "cross_chain_wallet_missing" | The buy could only be funded from a chain whose wallet you did not send. Send both wallets |
422 no_routes, reason: "payout_route_unavailable" | You hold enough, but not enough of it can pay out on payout_chain. Pay out elsewhere, or drop payout_chain |
400 wallet_not_delegated | The sponsored lane on a pinned chain, with a wallet not delegated there. Delegate it, or drop execution |
insufficient_native_gas at submit | The wallet pays gas on this leg and has too little. Fund it and re-quote |
nonce_conflict at submit | Another speed trade from this wallet on that chain is in flight. Wait, then re-quote |
sponsorship_rejected at submit | Your paymaster declined the operation. Fix the sponsorship and re-sign |
Every error code and field is in the API Reference.