Skip to main content
This guide builds a flow that no single transaction can run. The user signs one transaction on Arbitrum. That transaction bridges USDC to Base, into an escrow. When the USDC lands, LI.FI’s settlement service runs the destination flow: it deposits the USDC into Aave on Base, and the user receives aBasUSDC. The user needs no gas on Base. The guide uses short fragments. The same flow as one runnable file, with the submit and tracking loops, is the Bridge and deposit recipe. For the model behind it, read Cross-chain flows. For every config field, rule and error, see continuation.settle.

Prerequisites

  • The Composer SDK and a server-side API key in LIFI_API_KEY; see Before you start. This guide also uses viem:
  • A destination chain from the served chains. The escrow lives on the bridge’s destination chain, and the outcome must land on that same chain. Here both are Base.
Mainnet only. Cross-chain flows are served on mainnets only, so this guide sends real transactions. Use a small amount, and a wallet in PRIVATE_KEY that holds USDC and gas on Arbitrum.

1. Write the destination flow

The destination flow runs later, on Base, from the escrow. Build it like any other flow, on the destination chain, with exactly one input: the token the bridge delivers. Then pass destFlow.build() to the settle node.
A destination flow runs in the escrow’s execution context: a proxy that LI.FI’s executor controls, funded from the escrow. It never runs in the user’s proxy. The compiler checks these rules:
  • Exactly one input, a resource input in the funding token on the destination chain. The settlement service binds the escrow’s balance to it.
  • Leave every outcome in the destination flow’s proxy. In a destination flow, destFlow.context.executionAddress is the escrow’s execution context, not the user’s proxy. Where a node needs a receiver, bind destFlow.context.executionAddress. Never bind destFlow.context.sender: in a destination flow it is LI.FI’s executor, and the compile fails with leg_binds_sender.
  • The settle node decides where outcomes go. You set the outcome’s destination and the refund address in the settle node’s config. The compiler then appends transfers to the destination flow (an epilogue): the outcome goes to the settlement contract, which checks the minimum received and forwards it to destination, and every leftover goes to refund. Do not send to destination from the destination flow, and do not give it an epilogue of its own.
  • No nested continuation.settle.
  • An empty destination flow is valid. One with no nodes delivers its funding unchanged: set the outcome token to the funding token.
Not every op is allowed in a destination flow; see which ops a destination flow can run. The compiler proves the destination flow at the minimum funding, the smallest balance that starts a run. A failure inside it is reported on the settle node, with a message that starts with continuation leg of '<id>' failed at the funding floor.

2. Bridge into the escrow

The source flow runs on Arbitrum. It bridges the USDC to Base and binds the bridge’s output to the settle node. The bridge has no destination, so it delivers into the settle node’s escrow on Base. The compiler infers this funding from the binding, so the settle node sets no funding.
The outcome token is what the destination flow produces (aBasUSDC), not the funding token (USDC). The settle node takes no guards. Omit outcomes[0].minAmount when the destination flow proves a minimum, as the zap’s slippage guard does here; the op reference explains how the minimum received is set.

3. Compile and read the escrow

The response adds settlements.settle.escrow, where the funds go, and outputs['settle.output'], the minimum and expected outcome. Flow wire format: settlements describes every field.
  • escrow. Every compile returns a new escrow address; never reuse one. Trust escrow.expiresAtMs, not the expiry you authored: the settlement service may shorten it. escrow.floor is the minimum funding, derived from the bridge’s quote.
  • amount.minimum is the minimum received, the only guaranteed amount: the settlement contract reverts a run that would deliver less. amount.estimate is the expected outcome; nothing enforces it. Show users the minimum as the guarantee and the estimate as the likely result.
  • actions holds the approval of Arbitrum USDC, if the proxy needs one, and execute. Both run on Arbitrum. There is no fundEscrow action, because the bridge funds the escrow. outputs['bridge.amountOut'] reports role: 'consumed' and consumedBy: 'settle': the bridged USDC goes into the escrow, not to the user.
  • result.fill.validUntilMs, when present, is the latest time to submit the user’s transaction. It is never later than the escrow’s expiry and is not enforced on chain.
If you compile with simulationPolicy: 'allow-revert', a simulated revert returns status: 'partial' (HTTP 206) instead of throwing. The compile registers the escrow before it simulates the source transaction, so a partial response can carry settlements and actions. Never fund an escrow from a partial response; compile again instead.

4. Submit

Send every action in order, each on its own transactionRequest.chainId. This is the same loop as in Build a same-chain flow, with a check that every action runs on Arbitrum. It uses viem with a private key; use your own wallet stack in production.
In this example owner must be account.address. After a failure, compile again and use the new response. Never fund an escrow from an earlier response.

5. Track the bridge and the escrow

Two services report progress after the execute transaction is mined:
  • The bridge. Poll the LI.FI API’s GET /v1/status with txHash=<sourceTxHash>&fromChain=42161&toChain=8453 and your API key. It reports PENDING, then DONE or FAILED.
  • The escrow. Poll GET /intent-factory/status with the escrow’s chainId and address:
COMPLETED means the aBasUSDC reached the user. An escrow at READY or later means the bridge has delivered, even if LI.FI still reports PENDING. If the bridge transfer FAILED, the escrow never receives funds and stays EXPIRED after its expiry. A bridge refund on Arbitrum goes to the user’s execution proxy, not the wallet; Track the delivery shows how to recover it. If the USDC lands but the destination flow doesn’t run, the USDC is refunded to refund automatically. Escrow status maps every status to what to show your user, and has a polling function.

Other ways to fund the escrow

A bridge isn’t the only way to fund an escrow. The two other modes fund an escrow on the flow’s own chain, so the destination flow runs later on that same chain. Same-chain continuations explains when to pick each. Each example below is a separate flow, not a change to the running example.
  • Pay by transfer. Set funding: 'external' and hand the returned fundEscrow transfer to whoever pays.
  • Exact payment on one chain. Set exact: true, and bind the settle node’s input to a declared input or an upstream output instead of a bridge’s amountOut. The user’s transaction then funds an escrow on the same chain, and the escrow runs only the destination flow’s compiled bytes. The cross-chain version of this pattern is Pay cross-chain.

Pay by transfer

With funding: 'external', the source flow does not touch the funds. The response carries a fundEscrow action: a plain transfer to the escrow that any wallet can submit, for example a payer who scans a QR code. This flow escrows 100 USDC on Base for an empty destination flow, which delivers the USDC to owner unchanged:
The settle node is the only node in this flow, so depositResult.transactionRequest is absent and actions holds only the fundEscrow transaction. Track the escrow as in step 5.

Optional: verify the binding before funding

An exact destination flow adds settlements[id].binding, which discloses what the escrow will run; Flow wire format: settlements describes each field. The compile already checks that payload matches payloadHash, so this step is not mandatory. We recommend recomputing the hash yourself before anyone funds the escrow. Run this on the result of a compile with exact: true, such as the one in Pay cross-chain:
What an exact destination flow cannot check for you:
  • The compile checks that payload wraps the compiled destination flow and that its hash matches. It cannot prove that the settlement service put that hash in the escrow’s constraint. You also cannot check yet how the escrow address is derived; a way to check it is planned.
  • The bytes are fixed at compile time. An on-chain assumption that stops holding before the run (a cap that fills up, an epoch that ends) makes the run revert, and the escrow is refunded.
  • No outcome, no minimum. With outcomes: [], only the executor, the payload hash and the spending cap constrain the run on chain. Nothing checks what the destination flow achieves. To check it yourself, decode calldata before funding; for an on-chain check of the effect, add an invariant.* op to the destination flow. A zero-outcome exact destination flow has no settle.output entry.
Double payment. The escrow runs the payment whenever the destination flow runs, up to the escrow’s expiry. If the payee is paid another way in that window, the exact destination flow pays again, unless the contract it calls rejects duplicates. Keep the expiry short.

Errors you might see

source.compile throws a ComposeError. Settle-node authoring errors carry a fix field that names the one edit that clears the error.
The most common ones in this guide: delivery_error with floor_drift (compile again) or eta_exceeds_expiry (set a later expiry), and validation_error with forbidden_operation (replace the destination flow’s op). continuation.settle errors lists every settle-node error, its cause and its fix.

Next steps

Bridge and deposit

This guide as one runnable file, with polling.

Pay cross-chain

An exact destination flow that pays two recipients.

Escrow status

Status values, what to show your user, and a polling function.

continuation.settle

Every config field, rule and error.