Validation, lowering, simulation, and compilation each enforce rules introduced in the previous three pages: Flow shape, the resource model, and ref grammar.A Flow is declarative: it describes what the transaction should do, not how to produce the bytecode.
POST /compose turns the Flow JSON into a transactionRequest your wallet can sign. This page describes the user-observable behaviour: what the service checks, what it simulates, and what it returns.
What happens when you call POST /compose
Shape validation
The Flow JSON is validated against the wire-format schema: required fields, types, and ref shape. Malformed input is rejected with a
decode_error (HTTP 400) before anything else happens.Semantic validation
Every op and guard name must be registered, every bound port must exist and match its expected kind (resource vs. handle), and every ref must resolve to a node or input declared in the same flow. Failures surface as
validation_error, linearity_error, or resolve_error.Runtime input resolution
The
run.inputs values you passed are resolved against the Flow’s declared inputs. A plain integer or hex string is used verbatim; a materialiser descriptor (for example directDeposit or balanceOf) is expanded into the concrete amount and any preconditions that need to hold at execution time. See the materialisers catalog.Amount preparation (when needed)
When a node depends on a value that can only be known by reading on-chain state (for example, the current share price of a vault), the service resolves those amounts from current on-chain state before producing calldata.
Lowering to executable bytecode
Each high-level op (for example
lifi.swap, lifi.zap) is lowered into the routing and transfer calls the VM actually executes. Guards are attached to the nodes they protect. A no_route_error is raised if the routing layer cannot find a path for a zap edge.Sweeping
When
run.sweepTo is set, transfers are appended to move every terminal resource (tokens held by the execution proxy after the flow runs) to the requested address. The sweep target is usually builder.context.sender.Simulation and compilation (in parallel)
The compiled program is both simulated against current on-chain state and compiled to EVM calldata at the same time:
- Simulation runs the transaction against the latest state of the target chain (the current chain head), checks every attached guard (for example minimum output, slippage), and records the simulated output amount for each terminal resource. Violations produce
guard_error; a low-level revert producessimulation_revert. - Compilation produces the final calldata and confirms the resulting artifact against the service’s policy (contract allowlists, transfer patterns, instruction limits, and external-call analysis).
Price-impact check (optional)
When you pass
run.maxPriceImpactBps and the service has USD prices for both inputs and outputs, it aggregates the impact in USD and rejects the compilation with price_impact_exceeded if the bound is violated.Response
The response is a
ComposeCompileResult — a discriminated union on its status field:status: 'success'(HTTP 200): the compile completed cleanly. The payload carriestransactionRequest(calldata,to,value, optionalgasLimit),userProxy(the execution address),producedResources(a map of terminal-resource records; simulated amounts live under.simulated.amountOut/.simulated.amountOutMin), any requiredapprovals, and an optional price-impact breakdown.status: 'partial'(HTTP 206, only whensimulationPolicy: "allow-revert"was set on the run): the compile produced atransactionRequestbut simulation reverted. The payload additionally carrieserror({ kind, message }) andsimulationRevertso the caller can surface the revert reason rather than hide it.
result.status before reading shape-specific fields — accessing result.transactionRequest after an allow-revert call without checking status will work, but ignoring simulationRevert will hide the very signal you opted into.Errors you might see
The service returns a structuredComposeError with a kind field. The most common kinds:
| Error kind | HTTP status | When |
|---|---|---|
decode_error | 400 | The Flow JSON is malformed or fails shape validation. |
validation_error, linearity_error | 400 | Port bindings, ref targets, or resource linearity are invalid. |
preparation_error | 422 | An amount that required on-chain resolution could not be determined. |
no_route_error | 404 | No routing path exists for a zap edge. |
guard_error, simulation_revert, simulation_setup_error, price_impact_exceeded | 422 | The simulated transaction violates an invariant or reverts. |
resolve_error, lowering_error, simulation_error, compilation_error, verification_error | 500 | Transport or server-side failures. Retry is usually safe. |
simulationPolicy: "allow-revert", a structured revert is returned as HTTP 206 Partial Content with the transaction and the revert diagnostics, instead of failing the call. This is useful when you want to surface the revert reason to the user rather than hide it.
See also
- Flow Structure — what you submit.
- Guards — what the simulate stage asserts.
- Error codes — full catalog of
kindvalues.

