Skip to main content

Scenario

A user holds USDC on Arbitrum and wants an Aave USDC position (aBasUSDC) on Base, with one signature on Arbitrum and no gas on Base. The bridged USDC lands after the Arbitrum transaction has ended, so no node in that transaction can deposit it. A cross-chain flow closes the gap. A lifi.bridge with no destination delivers the USDC into an escrow on Base. Once it lands, LI.FI’s settlement service runs the destination flow, one lifi.zap into Aave, and the settlement contract checks the minimum received before it delivers the aBasUSDC to the user. Build a cross-chain flow explains each step of this file.

Full example

Install with npm install @lifi/composer-sdk @lifi/compose-spec viem; see Before you start for versions and the API key. The example signs with a private key through viem. Set LIFI_API_KEY and PRIVATE_KEY in your environment; never ship either to a browser.

Run it

Save the file as bridge-and-deposit.mts and run it:
It sends real transactions on Arbitrum: cross-chain flows are served on mainnets only. Start with a few USDC, and a wallet that holds USDC and gas on Arbitrum.

What to check

  • settlements.settle.escrow.chainId is 8453: the escrow is on Base. escrow.floor is the minimum funding, derived from the bridge’s quote; see Funding rules.
  • outputs['settle.output'].amount.minimum is the minimum received, the only amount the settlement contract enforces. Show it to the user as the guarantee; amount.estimate is not enforced.
  • actions holds the approval of Arbitrum USDC (if the proxy needs one), then execute, all on Arbitrum. There is no fundEscrow action, because the bridge funds the escrow. outputs['bridge.amountOut'] reports role: 'consumed' and consumedBy: 'settle'.
  • The escrow status reaches COMPLETED, with destinationTxHash set. An escrow at READY or later means the bridge has delivered, even if LI.FI still reports PENDING. Escrow status maps each status to what to show your user.
The choices in the example:
  • No destination on the bridge makes it deliver into the escrow. A bridge without destination must feed a settle node; on its own it fails with validation_error.
  • No funding on the settle node. Bridge funding is inferred from the binding, and any authored funding value is rejected.
  • One hour of validity. The expiry must leave room for the bridge; the settlement service may shorten it, and the stored value is escrow.expiresAtMs.
  • refund: owner. The escrow lives on Base, so the refund address must be one you control on Base. For a smart-contract wallet that exists only on Arbitrum, set another address.
  • Only positions LI.FI can reach. The destination flow’s lifi.zap needs a synchronous enter-position routing edge on Base for the landing token. Pick targets from GET /compose/zap-packs; the compile proves the destination flow and fails if no route exists.
If LI.FI reports the bridge transfer FAILED, the escrow never receives funds: it has nothing to refund and stays EXPIRED after its expiry. Stop polling the escrow and read the transfer’s substatus in the LI.FI status response. A bridge that refunds on Arbitrum returns the USDC to the user’s execution proxy (result.userProxy), the bridge call’s sender, not to the wallet. lifi.bridge: Track the delivery shows how to move it back to the wallet. If the bridged USDC lands but the destination flow doesn’t run, or a run ultimately fails, the USDC is refunded automatically to refund on Base; no user action is needed.
Never fund an escrow from a status: 'partial' (HTTP 206) response or from an older compile. Every compile registers a fresh escrow. Compile again instead.

Troubleshooting

Every other settle-node error is in continuation.settle errors.