Swapping via Catalyst
Catalyst is an entirely componentized and permissionless system with no inherent trust elements. It is up to integrators to mix and match components as needed.
Catalyst offers two ways to swap:
- Off-chain relay, via the order server (recommended)
- On-chain deposits.
Catalyst recommends that you use off-chain relay for a cheaper and more seamless experience. Though, for a fully decentralised system the on-chain deposit may provide advantages for you.
Getting a quote
Section titled “Getting a quote”You can implement your own quoting logic but the order server allows you to query Catalyst solvers’ inventory. This provides you with a better execution guarantee.
const getQuote = async () => { const response = await fetch("order-server-uri/quotes/request", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ // Chain identifiers as chain IDs fromChain: "11155111", // Sepolia testnet toChain: "84532", // Base Sepolia testnet
// Token addresses on respective chains fromAsset: "0xf08A50178dfcDe18524640EA6618a1f965821715", // Token on Sepolia toAsset: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // Token on Base Sepolia
// Amount in the smallest unit of the token amount: "3000000", // Amount with appropriate decimals. For USDC it is 6. }), });
/** * { "fromChainId": 11155111, "toChainId": 84532, "fromAsset": "0xf08a50178dfcde18524640ea6618a1f965821715", "toAsset": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238", "inputAmount": 3000000, "outputAmount": 2996000, "quote": 0.9986666666666667 } */ return await response.json();};
Order structure
Section titled “Order structure”A Catalyst order generally follows the structure of the CatalystCompactOrder
:
struct CatalystCompactOrder { /** @dev Pays for the inputs of the order. If the order fails, will be recipient of the refund. */ address user; /** @dev Allocator nonce for the order. Should be unique otherwise it won't be signed by the allocator. */ uint256 nonce; /** @dev The chainId (canonical) of the input chain. */ uint256 originChainId; /** @dev The expiry of the lock. Enough time for the fill and validation needs to be provided. */ uint32 fillDeadline; /** @dev Address of the validation layer on the origin chain. */ address localOracle; uint256[2][] inputs; OutputDescription[] outputs;}
The CatalystCompactOrder
needs to be appropriately signed. For TheCompact
settlement interface, use the following transformation:
struct BatchCompact { address arbiter; // Associated settlement contract address sponsor; // CatalystCompactOrder.user uint256 nonce; // CatalystCompactOrder.nonce uint256 expires; // CatalystCompactOrder.fillDeadline uint256[2][] idsAndAmounts; // CatalystCompactOrder.inputs CatalystWitness witness;}
struct CatalystWitness { uint32 fillDeadline; // CatalystCompactOrder.fillDeadline address localOracle; // CatalystCompactOrder.localOracle OutputDescription[] outputs; // CatalystCompactOrder.outputs}
The sponsor (user) shall then sign it as an EIP-712 signed structure.
Inputs
Section titled “Inputs”For TheCompact
, the inputs need to be provided as an array of [uint256 tokenId, uint256 amount]
. The tokenId
refers to the TheCompact
encoded tokenId, the first 12 bytes is a lock tag and the last 20 bytes is the token address.
Output
Section titled “Output”struct OutputDescription { bytes32 remoteOracle; bytes32 remoteFiller; uint256 chainId; bytes32 token; uint256 amount; bytes32 recipient; bytes remoteCall; bytes fulfillmentContext;}
Note that OutputDescription.remoteOracle
and CatalystCompactOrder.localOracle
need to match. Together, they define the validation layer used. Each order should only use one validation layer, which can be an aggregation of AMBs.
remoteFiller
specifies the output type. For limit orders, use the CoinFiller
and set fulfillmentContext
as empty (0x
). For Dutch auctions, use the CoinFiller
and set fulfillmentContext
appropriately.
Specify the token as the bytes32
identifier. For EVM, the address is left-padded, e.g., 0x000...00abcdef
.
RemoteCall
Section titled “RemoteCall”You can schedule additional calls to happen after token delivery. Note that if you have configured multiple outputs, the order of execution is not guaranteed (it may happen over multiple blocks). If remoteCall
is provided, the recipient
is called using the Catalyst interfaces. For arbitrary calls, the Catalyst Multicaller can be used.
Off-chain relay via resource locks
Section titled “Off-chain relay via resource locks”Off-chain relaying of swaps assumes that you have an existing resource lock somewhere. This guide we assumes you use TheCompact. When using pure resource lock flow, ensure funds are already deposited into the lock. For alternative integrations, refer to the on-chain flow.
// Submit an order to the order serverconst submitOrder = async () => { const response = await fetch("order-server-uri/orders/submit", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ // Type of order to submit orderType: "CatalystCompactOrder",
// Quote information for the swap quotes: [ { fromAsset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", toAsset: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", toPrice: "0.99", fromPrice: "1.01", intermediary: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", discount: "0.005", }, ],
// The CatalystCompactOrder structure order: { user: "0x9773DAcbc46CAFb4e055060565e319922B48607D", nonce: 1005, originChainId: 84532, fillDeadline: 1744884642, localOracle: "0xada1de62bE4F386346453A5b6F005BCdBE4515A1", inputs: [ [ "36286452483532273188258183071097127586156282419649613466036116694645176389502", 1000000, ], ], outputs: [ { remoteOracle: "0x0000000000000000000000007bc921c858c5390d9fd74c337dd009ec9a1b6b8f", remoteFiller: "0x0000000000000000000000005d14806127d7caafcb8028c7736ae6b8aec583d9", chainId: 11155111, token: "0x0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238", amount: 1000000, recipient: "0x0000000000000000000000009773dacbc46cafb4e055060565e319922b48607d", remoteCall: "0x", fulfillmentContext: "0x", }, ], },
// EIP-712 signature from the user/sponsor sponsorSignature: "...",
allocatorSignature: "...", }), });
/** * { "order": { // Order details (similar to the request) "user": "0x9773DAcbc46CAFb4e055060565e319922B48607D", "nonce": 1005, // Other order fields... }, "quotes": [ // Quote information ], "sponsorSignature": "0x9de6ca6df89a582d8c228b25fc84c947b52aac232e6e1d48b8a1f32c0610166226f773a501eda4489cc5d91c25c2b472e505bc3d9862690c18a6c38e8da27f371b", "allocatorSignature": "0x", "meta": { "submitTime": 1744909799926, "orderStatus": "Signed", "destinationAddress": "0x9773dacbc46cafb4e055060565e319922b48607d", "orderIdentifier": "co_6LFaJDL9CuW_y8nfNDocAdF0BizEQn", "signedAt": "2025-04-17T17:09:59.925Z", "expiredAt": null } } */ return await response.json();};
On-chain relay
Section titled “On-chain relay”For non-wallet integrators, on-chain deposits is the easiest integration. It uses more gas but abstracts the resource lock complexity away: There are 2 ways to do on-chain relaying of swaps:
- Using the
CompactSettlerWithDeposit
. This provides you with an easy and quick integration at a slight increased gas overhead.function depositFor(CatalystCompactOrder calldata order, ResetPeriod resetPeriod) external; - Using a custom
depositAndRegisterFor
implementation similar to the LI.FI facet. This requires a slightly more complicated