Skip to main content
Guards explained how minimums are produced during simulation. This page explains how they’re reported, alongside any tokens still held by the proxy at the end.
A Flow describes how resources move through the program. At the end of execution, any balance still held by the per-signer execution proxy needs an explicit destination; otherwise tokens get stranded. Sweeping is the mechanism. Amount preparation is what fills in values that depend on on-chain state. This page covers both. They sit at opposite ends of the compile pipeline (see Execution Model for the full sequence).

Terminal resources

A resource is terminal when no op in the flow consumes it. There are two ways to arrive there:
  • Producing-op outputs nothing else binds to. The receipt token from lifi.zap into a vault is terminal if no later node consumes it.
  • Dangling split outputs. When core.split produces { a, b } and only a is bound downstream, b is terminal. See the dust-sweep recipe for the canonical case where this is intentional.
The compiler identifies terminal resources at build time by walking the binding graph. Every terminal resource ends up in producedResources on the response and is eligible for sweeping. See Resource Model for the resource lifecycle.

sweepTo policy

The run.sweepTo field tells the backend where to send terminal resources at the end of execution. Three forms:
  • builder.context.sender. Sweep to the transaction signer. Most flows use this. On the wire, it serialises to { "$ref": "context.sender" }.
  • A literal address (e.g. '0xRecipientAddress'). Sweep to a specific address. Used when the flow’s purpose is to deliver tokens to someone other than the signer.
  • Omitted. No sweep. Terminal resources stay on the per-signer execution proxy. This is rare and mostly useful for flows that intentionally accumulate balance on the proxy across multiple submissions.
When sweepTo is set, the compiler appends transfer instructions for every terminal resource. The transfers run after all op calls and after any attached guards have asserted their invariants.

What does and does not get swept

  • Does: all terminal resources, including dangling split outputs and producing-op outputs nothing consumed.
  • Does not: resources consumed by another op (they are inputs to that op, not residual). Handles (typed scalars). They are not balances and never sweep.
If an input materialiser supplies more of a resource than the consuming op uses, the unused remainder is also a terminal resource. This is what makes dust-sweep work: core.split partitions an input, only one partition is consumed, and the unbound partition naturally becomes terminal.

Non-transferable terminal resources

sweepTo is a catch-all: when set, it tries to transfer every residual proxy balance to the target. Most tokens move freely, but some terminal resources are bound and cannot be transferred out — most commonly aTokens (or other collateral receipts) that back an open debt. Aave’s finalizeTransfer reverts if moving the aToken would leave the position under-collateralised, so a blanket sweepTo over such a balance makes the whole transaction revert. When a flow deliberately leaves a position on the proxy — a borrow, leverage, or debt-migration flow that keeps collateral in place — don’t set a blanket sweepTo. Leave the collateral on the persistent per-signer proxy and emit explicit transfers for only the genuinely-loose tokens: read each with core.balanceOf (owner defaults to the proxy) and move it with core.transfer.
// Read only the loose USDC sitting on the proxy...
const loose = builder.core.balanceOf('read-usdc', {
  bind: {},
  config: { token: USDC },
});

// ...and transfer just that to the signer. The aToken collateral is left untouched.
builder.core.transfer('payout', {
  bind: { amount: loose.balance, recipient: builder.context.sender },
  config: {},
});
The Aave→Morpho debt-migration recipe uses exactly this pattern.

Amount preparation

Most amounts are known at build time. directDeposit({ amount: '1000000000000000000' }) supplies a literal value. Some amounts can only be known by reading on-chain state at execution time:
  • balanceOf materialiser. Read the proxy’s current balance of a token before consuming it.
  • call materialiser. Invoke a view function and use its return value as the input amount.
  • Vault share-price reads. When a downstream op needs to know “how many share tokens did the deposit produce,” the service resolves that amount from current on-chain state before producing calldata.
The amount preparation step (step 4 of the execution model pipeline) resolves these. It runs after runtime input resolution and before lowering. Failures surface as preparation_error (HTTP 422), typically because the on-chain read returned 0 or reverted.

Simulated amounts on the response

After simulation, every terminal resource has a known simulated amount. The compiler returns these as producedResources on ComposeCompileResult:
result.producedResources;
// {
//   'zap.amountOut': {
//     kind: 'erc20',
//     token: '0x98C2…',
//     chainId: 1,
//     // owner, availability …
//     simulated: { amountOut: '12345678', amountOutMin: '12000000' },
//   },
//   'split.b': {
//     kind: 'erc20',
//     token: '0xA0b8…',
//     chainId: 1,
//     // owner, availability …
//     simulated: { amountOut: '2000000', amountOutMin: '2000000' /* dust, no minimum guard */ },
//   },
// }
Each entry carries the resource’s own metadata (kind, token, chainId, owner, availability) plus a simulated block:
  • simulated.amountOut. The exact simulated amount at current chain state (the latest block). Display this as “you will receive ≈ X” in your UI.
  • simulated.amountOutMin. The worst-case amount given the slippage / minimum-out guards attached to that resource’s output port. Display this as “you will receive at least X.” When no minimum-out guard is attached (e.g. dust from a split with no slippage protection), amountOutMin may equal amountOut; check the response rather than assuming it’s omitted.
  • Multiple guards. When several guards target the same port, the tightest minimum wins (the largest simulated.amountOutMin).
See Guards for how amountOutMin is derived from a guard’s tolerance.

See also