Docs · Crypto

Swaps

Quoting through 0x, and the fake that deliberately cannot be signed.

How a swap happens

  1. The browser asks Rails for a quote.
  2. Rails calls 0x with the API key it holds, and returns the calldata.
  3. The browser asks the user's wallet to send that transaction.
  4. The browser reports the resulting hash back to Rails and stops.
  5. A Solid Queue job watches for the receipt and streams status into a turbo-frame.

The browser never polls a chain. That is what keeps connect-src at 'self', and it means a user can close the tab without losing the outcome.

The fake cannot be signed, and that is the point

Without a ZEROX_API_KEY, Swap.for(chain_id) returns Swap::Local, whose quotes carry transaction: nil.

This is the sharpest safety property in the module. If the fake returned plausible-looking calldata, a buyer who deployed without a key would get a swap screen whose button asks a real wallet to sign real calldata routing nowhere, and somebody would lose money to a demo. With nil, the quote is structurally unexecutable: Quote#executable? is false, the UI can only render "Simulated quote", and the button is disabled.

A spec asserts it, because the temptation to make a fake more realistic is exactly how this would regress.

Slippage is clamped twice

In the controller and again in the adapter, to 10–500 basis points. Two places on purpose: the controller guards the public endpoint against a client asking to be sandwiched and then blaming the app, and the adapter guards every other caller, including a console session and whatever gets written next.

No aggregator runs on a testnet

There is no real liquidity to route against, so 0x supports mainnets only. The kit's default chain is Base Sepolia, so this refusal is the first thing you will meet rather than an edge case. It names the chain and says what to do:

Swaps are not available on Base Sepolia. Aggregators only route on mainnets, because that is where the liquidity is. Switch to Base to try one.

Swap::SUPPORTED_CHAIN_IDS is deliberately the same set as the mainnets in Chain::REGISTRY, and a spec asserts they cannot drift apart.

Swapping the aggregator

The interface is deliberately small and aggregator-shaped:

#price(sell_token:, buy_token:, sell_amount:, taker:, slippage_bps:)  # indicative
#quote(sell_token:, buy_token:, sell_amount:, taker:, slippage_bps:)  # firm, with the transaction

Swap::Lifi, 1inch, or a direct Uniswap router can replace Swap::Zerox without touching the controller or the island. Cross-chain bridging is deliberately out of scope: it is asynchronous, multi-step and refundable, which is a lot of failure surface for a starter kit.

What is not covered by tests

Swap::Zerox is spec'd for its interface, its clamping and its error handling, but never against live 0x responses, which need a real key. That is in the manual matrix in docs/ONCHAIN.md.