Skip to content

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:

  1. Off-chain relay, via the order server (recommended)
  2. 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.

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();
};

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.

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.

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.

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.

Learn more about Sub-Calls →

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 server
const 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();
};

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:

  1. 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;
  2. Using a custom depositAndRegisterFor implementation similar to the LI.FI facet. This requires a slightly more complicated