Skip to main content

Scenario

A payer holds ETH on Arbitrum. A merchant must receive exactly 990 USDC on Base, and a platform takes a 10 USDC fee. The payer signs one transaction on Arbitrum. A LI.FI Intents order delivers exactly 1,000 USDC into an escrow on Base, and an exact destination flow splits it and pays both recipients.
What this recipe represents. Any payment that must land as exact amounts at specific addresses on another chain: an invoice with a platform fee, a payout split, a subscription charge.Here the destination flow is two plain transfers, so the recipe runs as is. In production you would usually replace them with one call to your own payment or invoice contract (core.call). The escrow commits to the exact bytes either way, so nobody can change the recipients or the amounts after the payer signs.
Why this needs a cross-chain flow. Two separate exact-output Intents orders could each deliver to one recipient, but one could fill while the other doesn’t. Here both payments run in one transaction on Base: the payee and the fee recipient are both paid, or neither is. The same holds when the destination flow is a single call that has to run, such as an invoice contract.
A standard destination flow can’t do this: it is quoted again when it runs, so it rejects core.transfer and core.call. An exact destination flow (exact: true) binds the escrow to its compiled bytes. See Standard and exact destination flows.

Full example

Install with npm install @lifi/composer-sdk @lifi/compose-spec viem; see Before you start for versions and the server-side API key. Set LIFI_API_KEY and PRIVATE_KEY in your environment.
How the pieces fit:
  • The Intents order delivers exactly the total. With exactAmountOut and no destination, the Intents solver delivers exactly 1,000 USDC into the escrow on Base, and escrow.floor is exactly the total, with no margin.
  • Split the total first. A run spends min(escrow balance, escrow.cap), the cap is 10% above the expected funding, and anyone can send tokens to the escrow. A single split at 990 USDC would give the fee recipient 10 USDC plus any extra, up to 110 USDC. Splitting off the 1,000 USDC total first keeps both payments exact: the extra stays in the proxy and goes to refund.
  • outcomes: []. Everything is paid out, so nothing comes back on Base, and the response has no outputs['settle.output']. A zero-outcome exact destination flow must run on the funding chain.
  • sweepTo: owner returns the order’s unspent Arbitrum USDC to the payer in the same transaction. All slack (swap slippage, the Intents solver’s price, your sizing buffer) comes back there, not on Base.

Run it

Save the file as pay-cross-chain.mts, add the submit loop from Bridge and deposit at step 4, and run it:
Submitting sends real transactions on Arbitrum. Replace the two example recipient addresses before you run it.

What to check

  • settlements.settle.escrow.floor is exactly 1000000000: an exact-output order adds no margin.
  • settlements.settle.binding is present: the escrow is bound to the compiled transfers. Optional: verify the binding before funding.
  • actions holds the execute transaction on Arbitrum and no fundEscrow: one signature.
  • The escrow reaches COMPLETED (Escrow status), and Base shows two USDC transfers from the proxy of LI.FI’s executor: 990 to the payee and 10 to the fee recipient. An escrow that is still EXPIRED 90 minutes after its expiry was never funded, for example because no Intents solver filled the order.
  • An unfilled order keeps the payer’s Arbitrum USDC locked in the LI.FI Intents escrow on Arbitrum. Nothing refunds it automatically. After the order expires (48 hours after it opens), anyone can call lifiIntents.refund, which pays the order’s user: the signer’s wallet on Arbitrum. For an earlier refund, run lifiIntents.attestNonFill on Base after the fill deadline, then lifiIntents.refundOnNonFill on Arbitrum. See Track and refund the order.
Price protection is off. maxPriceImpactBps: 0 turns the price-impact check off for the whole flow, so the ETH to USDC swap is bounded only by its own slippage. Before signing, compare outputs['swap.amountOut'].amount.minimum with an independent ETH price.

Troubleshooting

Every other settle-node error is in continuation.settle errors.
Paying twice. The escrow pays whenever the destination flow runs, up to its expiry. If the merchant is paid another way in that window, this pays again. Keep the expiry short (one hour here), and don’t offer a second payment route while an escrow is open.