Skip to content

Exact-amount swaps

An instant swap composes whole resting orders, so it can fill at most your sellAmount — the book's slice sizes decide the actual figure, and the remainder stays in your wallet. When your integration needs to move an exact amount, use the other side of the same book: a limit order fills whole or not at all, so a filled order pays you exactly the amounts it named.

The catch is that a limit order only fills if someone profits by taking it. Price it at the market mid and it rests forever — market-maker bots fill orders at their bid or ask, after the reactor fee and a profit margin, never at the mid. This guide is the recipe for pricing it so it fills, and knowing when it did.

The recipe

1. Quote the maker price.

GET /v1/limit-orders/quote?chainId=8453&sellToken=0xCNGN&buyToken=0xUSDT&sellAmount=1550000000

fastFill.buyAmount is priced through the best market-maker bid that can afford your whole order, with enough headroom that filling you is profitable for their bot — fills typically land within seconds. queue.buyAmount rests at the top of the book and waits for organic flow — usually a better price, but compare the two buyAmounts rather than assuming it: stacked queue orders can push it past fastFill (how it's computed). Check fastFill.depth.maxMakerCapacity: one taker fills one order from one balance, so an order bigger than the deepest single maker can't fast-fill — split it into a few smaller orders or use the queue price.

2. Prepare and sign. Pass pricing so the server computes buyAmount with the same math, and verify the echoed assumedRateRay before signing:

json
POST /v1/limit-orders/prepare
{
  "chainId": 8453,
  "maker": "0xYourWallet",
  "sellToken": "0xCNGN",
  "sellAmount": "1550000000",
  "buyToken": "0xUSDT",
  "pricing": { "mode": "fastFill" },
  "deadline": 1900000000
}

Use a TTL of 10–15 minutes and a fresh random nonce per order — see choosing a deadline for where those numbers come from. First order on a sell token? Approve Permit2 once from the maker wallet first, or submission fails the funded check — see the one-time setup.

3. Submit and wait for the webhook. Subscribe to the limit-order webhooks instead of polling:

  • limit_order.filled — done. The event carries the exact named amounts plus the settlement txHash.
  • limit_order.expired — the market moved away before anyone took it. Re-quote (step 1) and repost at the current price. Expiry is free — the signature just lapses — which makes short-TTL-plus-repost the gasless way to chase the market.
  • limit_order.cancelled — the order's Permit2 nonce was burned on-chain.

One timing caveat: the terminal events (expired/cancelled) are deliberately lagged ~10–15 minutes past the deadline, so a fill that settled seconds before expiry reconciles first and you get filled instead of a false expiry. Don't drive repost latency off the expired webhook — see choosing a deadline for the fast repost pattern.

GET /v1/limit-orders/{id} gives the same lifecycle on demand — and unlike the lagged events, its status is computed live (deadline and on-chain nonce checked at read time).

Choosing a deadline

The deadline (unix seconds) is your order's TTL. Set it as "how long my system tolerates before re-quoting", not "how long I expect to wait": a well-priced fastFill order is normally taken within a couple of bot ticks — seconds. The deadline only matters on the unhappy path.

Default to now + 600 (10 minutes). The floor isn't taste, it's the taker bots' timing constants: they won't serve an order within 30 seconds of expiry, and after a failed fill attempt (a race — the batch reverted, another order won) they back the order off for 3 minutes. One failure cycle therefore burns ~3.5 minutes; a 5-minute TTL survives exactly one, and anything shorter quietly excludes the bots after a single hiccup. Ten minutes gives two full retry cycles plus slack.

Don't set an hour "to be safe" either. A resting order is an option you've written: if the market moves through your price during the TTL, you fill at your named price on the wrong side of the move. Long TTLs buy no speed — the order fills in seconds or something is wrong — they only widen that exposure.

For an automated integration (an offramp desk, a treasury sweep), the shape that falls out:

  1. Prepare with deadline = now + 600, submit, and treat limit_order.filled as your settlement confirmation — typically seconds later.
  2. Run your own deadline timer for the repost — you set the deadline, so you know when it passes. Don't wait for the limit_order.expired webhook: that event is deliberately lagged ~10 minutes past the deadline (plus sweep cadence) so a fill that settled just before expiry reconciles first — waiting for it turns a 10-minute TTL into a ~20–25-minute repost loop.
  3. At deadline + ~1 minute, call GET /v1/limit-orders/{id}. Its status is computed live, deadline and on-chain nonce both checked at read time:
    • EXPIRED with statusVerified: true — the order verifiably never filled. Re-quote and repost with a fresh nonce immediately. Expiry is free, so this loop costs only the price refresh.
    • statusVerified: false — the on-chain nonce couldn't be checked (chain unreachable), so a derived EXPIRED might hide an unreconciled last-second fill. Don't act on it; poll again.
    • FILLED — a last-second fill reconciled; you're done.
    • CANCELLED — the nonce is spent but the fill may still be reconciling. Do not repost yet — if that spend was a fill, reposting executes your intent twice. Wait for the webhook to resolve it (filled supersedes).
  4. Alert a human after 2–3 consecutive expiries. Ten straight minutes with no fill doesn't mean a stale price — it usually means the corridor's operator bot is down or its wallet can't cover your order.

With the timer-plus-status pattern your worst-case repost latency is one TTL plus a minute; a webhook-driven loop would be one TTL plus the ~10–15-minute terminal-event lag. Typical latency stays in seconds either way, and the webhooks remain your settlement confirmation layer. If the product needs a hard synchronous answer instead, use the instant path with requireFullFill: true — it executes the full amount immediately at ladder prices or reports partial_fill right away, with no resting window at all.

Combining with an instant swap

When speed matters more than a single atomic execution, split the flow: instant-swap what the book can compose right now, then rest a limit order for the remainder.

  1. POST /v1/swaps for the full amount. The response's fillableAmount executes immediately; note remainingAmount.
  2. Run the recipe above for remainingAmount.

The two legs settle at slightly different prices, seconds to minutes apart — that's inherent, not a bug. If you'd rather not execute at all than execute part, pass requireFullFill: true to POST /v1/swaps and it refuses to build a partial (fill-or-kill).

What to watch

  • Depth is advisory. The book re-quotes every few seconds and other orders compete for the same maker balances. A fastFill price is a strong signal, not a guarantee — the webhook is the confirmation.
  • A resting order is an option you've written. If the market moves through your price during the TTL, you fill at your named price (which is now the worse side). The short TTL bounds this exposure; marginBps doesn't need to be generous for the order to fill.
  • Tiny orders may need more margin. Bots enforce a private per-order minimum profit as a gas guard. If a small order at the default 10 bps margin keeps expiring, raise marginBps — and if you run the corridor's own bot, you can pass your true floor instead (see tuning marginBps).
  • Cancel early only if you must. The only instant cancel is on-chain (invalidateUnorderedNonces burns the nonce) and costs gas. Letting a short TTL lapse is free and usually good enough.