Swaps
Quoting through 0x, and the fake that deliberately cannot be signed.
How a swap happens
- The browser asks Rails for a quote.
- Rails calls 0x with the API key it holds, and returns the calldata.
- The browser asks the user's wallet to send that transaction.
- The browser reports the resulting hash back to Rails and stops.
- 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.