In the execution model, the simulation step observes runtime values. Guards are how those observations become on-chain assertions.After a flow has been lowered to an executable program, the compose service simulates it against the latest state of the target chain (the current chain head) before returning calldata. This simulation produces three things: values for every terminal resource (so
producedResources can be populated), values for every handle a guard asked to observe, and a gas estimate. Guards are the mechanism that turns those observed values into on-chain invariants baked into the transaction itself.
What a guard is
A guard is an invariant attached to an op output. The service reads the relevant port value during simulation, then bakes a corresponding check into the compiled calldata so the invariant is enforced on-chain when the transaction runs. Guards are not simulation-time checks that can be silently relaxed. The check is part of the final calldata and is verified on-chain when the transaction runs, so the calldata itself guarantees the invariant.When guards run
Guards fire during the simulation and assertion step of the compose pipeline (see Execution model). Specifically:- The service simulates the program against the current chain head, reading any on-chain values the guards observe. The simulator returns the concrete value of every observed port.
- For each guard, the observed value is baked into a check that enforces the invariant. That check is part of the final compiled program.
- The final calldata is re-verified against the service’s policy (contract allowlists, transfer patterns, instruction limits).
guard_error (HTTP 422) and no calldata is returned. Under simulationPolicy: "allow-revert" the calldata is returned as HTTP 206 along with the revert diagnostics instead.
Where guards can attach: providesMinimum ports
A guard declares a compatibility.selectors list describing which ports it can attach to. The compile-time validator rejects attachments to ports it cannot protect. In particular:
- Ports that already declare
providesMinimum: true(the router or aggregator has already baked in a minimum-out) cannot carry a redundant minimum-out guard. Attaching aslippageguard to such a port produces aguard_errorat compile time with the message “Guard targets port … which provides its own minimum”. lifi.swap’samountOutport is the canonical example: LI.FI’s aggregator already encodesminOutfrom theslippageconfig field, so you don’t (and can’t) attach a second slippage guard to it.lifi.zap’samountOutdoes not provide its own minimum, which is why the swap-and-zap recipe attaches aslippageguard there.
providesMinimum; use References and the port type guards when you need to discriminate programmatically.
Guaranteed minimums and amountOutMin
Some guards (e.g. slippage) also produce a guaranteed minimum: the worst-case value for the observed port given the guard’s tolerance. The service surfaces this on the response as producedResources[*].simulated.amountOutMin for any terminal resource whose output port is protected by such a guard. If you attach multiple guards to the same port, the tightest minimum (the largest one) wins.
This is why guards are the right place to encode safety bounds: the same tolerance value you give the guard becomes both an on-chain assertion and a user-visible field on the response, so your UI can display “you will receive at least X” without recomputing anything.
Common guard failures
| Failure | Cause |
|---|---|
Guard targets port "<p>" which provides its own minimum | Attaching a minimum-out-style guard to a port whose op already declares providesMinimum. Remove the guard. |
Guard targets port "<p>" which does not exist | The port name in the guard config doesn’t match any port on the op. Check the op’s manifest. |
Guard requires config field "<key>" | The guard selector uses selection: { kind: "config" } and the config is missing the key. |
Guard '<kind>' on '<call>' returned guaranteedMinimums key '<p>' which is not in its observedHandles | Internal bug in a third-party guard definition; report upstream. |
guard_error at simulation time | The simulated transaction would violate the invariant. Loosen the tolerance or investigate the quote. |
What guards do not do
- Guards don’t replace preconditions. Preconditions describe on-chain state that must be true before the transaction runs (e.g., “the user holds at least X USDC”) and feed the simulator as state overrides. Guards run during the transaction and enforce invariants on computed values.
- Guards don’t change routing. A slippage guard that would fail at the user’s requested tolerance surfaces as
guard_error; the service does not silently reroute. If you want the aggregator to honour the tolerance, configure it on the op (e.g.lifi.swap’sslippagefield). - Guards don’t run arbitrary user code on-chain. A guard enforces its invariant using a bounded, audited set of checks — it cannot execute arbitrary logic on-chain.
See also
- Guards catalog — every guard registered in the manifest.
- Build a Flow → Add safety — how to attach guards and preconditions to flows.
- Execution Model — where the simulation step sits in the pipeline.

