> ## Documentation Index
> Fetch the complete documentation index at: https://docs.li.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Pay cross-chain

> Pay exact amounts to a payee and a fee recipient on Base, from ETH on Arbitrum, with one signature: both get paid or neither does. For invoices, payouts and subscriptions.

## 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](https://docs.li.fi/lifi-intents/introduction.md) order delivers exactly 1,000 USDC into an escrow on Base, and an **exact destination flow** splits it and pays both recipients.

> [!NOTE]
>
> **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`](https://docs.li.fi/composer/composer-api/ops/core-call)). The escrow commits to the exact bytes either way, so nobody can change the recipients or the amounts after the payer signs.

> [!TIP]
>
> **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](https://docs.li.fi/composer/composer-api/concepts/cross-chain-flows.md#standard-and-exact-destination-flows).

## Full example

Install with `npm install @lifi/composer-sdk @lifi/compose-spec viem`; see [Before you start](https://docs.li.fi/composer/composer-api/overview.md#before-you-start) for versions and the server-side API key. Set `LIFI_API_KEY` and `PRIVATE_KEY` in your environment.

```ts theme={"system"}
import { createComposeSdk, materialisers, raw, resources } from '@lifi/composer-sdk';
import { privateKeyToAccount } from 'viem/accounts';
import type { Hex } from 'viem';

const ARBITRUM = 42161;
const BASE = 8453;
const ARB_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

const sdk = createComposeSdk({ baseUrl: 'https://composer.li.quest', apiKey: process.env.LIFI_API_KEY! });
const owner = privateKeyToAccount(process.env.PRIVATE_KEY as Hex).address;

// The payment: 990 USDC to the payee plus a 10 USDC fee (6 decimals).
const payment = {
  payee: '0x1111111111111111111111111111111111111111',
  amount: 990_000_000n,
  feeRecipient: '0x2222222222222222222222222222222222222222',
  fee: 10_000_000n,
};
const total = payment.amount + payment.fee; // 1,000 USDC

// 1. Destination flow, on Base: take exactly the total, then split it in two.
//    Splitting the total first keeps both amounts exact: anything extra sent
//    to the escrow stays out of the payment and goes to the refund address.
//    In production, replace the two transfers with one core.call to your
//    payment contract.
const destFlow = sdk.flow(BASE, {
  name: 'pay-on-base',
  inputs: { funding: resources.erc20(BASE_USDC, BASE) },
});
const { head: payable } = destFlow.core.splitAt('total', {
  bind: { source: destFlow.inputs.funding },
  config: { amount: `${total}` },
});
const { head: toPayee, tail: toFee } = destFlow.core.splitAt('split', {
  bind: { source: payable },
  config: { amount: `${payment.amount}` },
});
destFlow.core.transfer('payPayee', {
  bind: { amount: toPayee, recipient: raw.literal('address', payment.payee) },
  config: {},
});
destFlow.core.transfer('payFee', {
  bind: { amount: toFee, recipient: raw.literal('address', payment.feeRecipient) },
  config: {},
});

// 2. Source flow, on Arbitrum: swap ETH to USDC, then open an Intents order
//    that delivers exactly 1,000 USDC into the escrow on Base.
const source = sdk.flow(ARBITRUM, {
  name: 'pay-from-arbitrum',
  inputs: { eth: resources.native(ARBITRUM) },
});
const swap = source.lifi.swap('swap', {
  bind: { amountIn: source.inputs.eth },
  config: { resourceOut: resources.erc20(ARB_USDC, ARBITRUM), slippage: 0.005 },
});
const order = source.lifiIntents.order('order', {
  bind: { amountIn: swap.amountOut },
  config: { resourceOut: resources.erc20(BASE_USDC, BASE), exactAmountOut: `${total}` },
});
source.continuation.settle('settle', {
  bind: { input: order.amountOut },
  config: {
    exact: true,   // the escrow commits to these exact transfers
    outcomes: [],  // everything is paid out; nothing comes back on Base
    continuationFlow: destFlow.build(),
    validity: { expiresAtMs: Date.now() + 60 * 60 * 1000 },
    refund: owner,
  },
});

// 3. Compile. Payments leave the flow, so turn off the price-impact check.
const result = await source.compile({
  signer: owner,
  inputs: { eth: materialisers.directDeposit({ amount: '500000000000000000' }) }, // 0.5 ETH
  sweepTo: owner, // unspent Arbitrum USDC returns in the same transaction
  maxPriceImpactBps: 0,
});

// 4. Submit result.actions on Arbitrum (same loop as every guide), then track the escrow.
```

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](https://docs.li.fi/composer/composer-api/recipes/bridge-and-deposit.md) at step 4, and run it:

```bash theme={"system"}
LIFI_API_KEY=... PRIVATE_KEY=0x... npx tsx pay-cross-chain.mts
```

> [!WARNING]
>
> 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](https://docs.li.fi/composer/composer-api/guides/build-a-cross-chain-flow.md#optional-verify-the-binding-before-funding).
* **`actions`** holds the `execute` transaction on Arbitrum and no `fundEscrow`: one signature.
* **The escrow** reaches `COMPLETED` ([Escrow status](https://docs.li.fi/composer/composer-api/reference/escrow-status.md#polling)), 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](https://docs.li.fi/composer/composer-api/ops/lifi-intents-order.md#track-and-refund-the-order).

> [!WARNING]
>
> **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

| Error | Fix |
| - | - |
| `preparation_error` (422) naming two amounts | The swap's guaranteed minimum doesn't cover the Arbitrum USDC the order needs to deliver exactly 1,000 USDC. Send more ETH: size it as the USD value of the payment at a recent price, plus the swap's slippage, plus a small margin. The extra returns through `sweepTo`. |
| `price_impact_exceeded` | Pass `maxPriceImpactBps: 0`. The payment leaves the flow, so it reads as lost value. |
| `delivery_error` (`quote_failed`) with a top-level `shortfall` | The order can't be quoted at exactly 1,000 USDC with the swap's guaranteed output. Send more ETH, or compile again. |

Every other settle-node error is in [`continuation.settle` errors](https://docs.li.fi/composer/composer-api/ops/continuation-settle#errors).

> [!WARNING]
>
> **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.

## Related

* [Cross-chain flows](https://docs.li.fi/composer/composer-api/concepts/cross-chain-flows.md#standard-and-exact-destination-flows): standard and exact destination flows.
* [`continuation.settle`](https://docs.li.fi/composer/composer-api/ops/continuation-settle#exact-destination-flows): the exact rules.
* [Bridge and deposit](https://docs.li.fi/composer/composer-api/recipes/bridge-and-deposit.md): a standard destination flow funded by a bridge.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.