Intent 104: Hub & Spoke, how an order travels
Two contracts on every chain, one message each way. Follow an order from createOrder() on the Hub to fillOrder() on the Spoke and back.
The whole iLayer network revolves around one struct. Here are its thirteen fields, grouped by the question each one answers.
In Intent 101 we said an intent is a sentence: “I want this to happen.” In Intent 102 we said a real intent system never moves your tokens, it coordinates an outcome. Now let’s open the box. On iLayer the sentence is a Solidity struct called Order, and every contract, solver and interface in the network speaks it.
Thirteen fields sounds like a lot. Grouped by the question they answer, they collapse into four: who, what, when, and what else should happen.
struct Order {
bytes32 user;
bytes32 recipient;
bytes32 filler;
Token[] inputs;
Token[] outputs;
uint32 sourceChainId;
uint32 destinationChainId;
bool sponsored;
uint64 primaryFillerDeadline;
uint64 deadline;
bytes32 callRecipient;
bytes callData;
uint256 callValue;
}The user is the signer. Input tokens are pulled from this address into the OrderHub, so it must have approved them first (or attached an ERC-2612 permit, more on that in the integration guide).
The recipient is who receives the outputs on the destination chain. It is often the same person, but it does not have to be: a wallet can pay on Ethereum and have a friend receive on Base.
The filler is the solver bound to this order. It comes back from the RFQ together with the quote, and it guarantees that the solver who priced the order is the one who executes it. Leave it empty and any solver may fill.
All three are bytes32, not address. That is deliberate: a 20-byte EVM address fits, and so does a 32-byte non-EVM account. The BytesUtils library in the codebase does the conversion in one call: BytesUtils.addressToBytes32(addr).
Both arrays hold the same Token struct: a type, an address, an optional tokenId and an amount.
enum Type { NULL, NATIVE, FUNGIBLE_TOKEN, NON_FUNGIBLE_TOKEN, SEMI_FUNGIBLE_TOKEN }
struct Token { Type tokenType; bytes32 tokenAddress; uint256 tokenId; uint256 amount; }
That covers native coins, ERC-20, ERC-721 and ERC-1155 in one shape. Inputs must be non-empty: they are what gets locked. Outputs may be empty, which is how an order can pay purely for cross-chain code execution (see Intent 107).
Output amounts usually come from the RFQ: you ask the solver network for quotes, pick one, and the chosen quote fills in the output array and the filler.
sourceChainId is where the inputs live and where the OrderHub takes custody. destinationChainId is where the OrderSpoke pays out. Chain ids are the standard ones you find on chainlist.org; iLayer maps them to bridge-specific identifiers inside the Router, so you never deal with LayerZero EIDs or Axelar chain names yourself.
When the two ids are equal the order runs through the NullRouter: same-chain execution, no bridge at all, same contracts and same signature flow.
sponsored turns the order gasless for the user. The solver submits the order creation and pays every fee, and recoups it by adding a premium to its quote. The user only signs.
primaryFillerDeadline is the exclusive window of the chosen filler. Before it, only that solver can fill. After it, any bot in the network may fill the order, at the original quoted amounts. That is your protection against a solver that quotes and then disappears.
deadline is the end of the order’s life. Past it, nobody fills, and once a short buffer has elapsed you can withdraw your inputs from the Hub. The Hub enforces that the primary window ends before the deadline, and that the deadline does not exceed a configured maximum. Intent 106 is entirely about these clocks.
The last three fields are optional. callRecipient, callData and callValue describe a contract call to run on the destination chain after the outputs are delivered. On EVM the calldata is the usual abi-encoded selector plus arguments.
This is the field that turns a swap into a DeFi action: deposit into a vault, mint an NFT, register a position. The Spoke runs it through an isolated Executor contract and blocks reentrant calls, so a hook can fail but cannot reach back into the protocol.
Fill them with null values when you do not need them. The struct stays the same, which is the whole point: one shape, any action.
Next in the series: Hub & Spoke, the two contracts that carry an Order from custody to settlement.
Two contracts on every chain, one message each way. Follow an order from createOrder() on the Hub to fillOrder() on the Spoke and back.