> ## 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.

# Bridge and deposit with one signature

> Bridge USDC from Arbitrum to Base and deposit it into Aave on Base, with one signature on Arbitrum and no gas on Base.

## 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](https://docs.li.fi/composer/composer-api/concepts/cross-chain-flows.md) closes the gap. A [`lifi.bridge`](https://docs.li.fi/composer/composer-api/ops/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](https://docs.li.fi/composer/composer-api/guides/build-a-cross-chain-flow.md) explains each step of this file.

## 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 API key. The example signs with a private key through [viem](https://viem.sh). Set `LIFI_API_KEY` and `PRIVATE_KEY` in your environment; never ship either to a browser.

```ts theme={"system"}
import {
  createComposeSdk,
  guards,
  materialisers,
  resources,
} from '@lifi/composer-sdk';
import { createPublicClient, createWalletClient, http, type Address, type Hex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrum } from 'viem/chains';
import { setTimeout as sleep } from 'node:timers/promises';

const ARBITRUM = 42161;
const BASE = 8453;
const ARB_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
// Aave V3 aToken for USDC on Base
const A_BAS_USDC = '0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB';

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

const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
const owner = account.address;

// 1. The destination flow: runs later on Base, from the escrow, once the
//    bridged USDC lands. The zap deposits the USDC into Aave. The aBasUSDC
//    stays in the destination flow's proxy; the settlement forwards it to
//    the outcome's destination.
const destFlow = sdk.flow(BASE, {
  name: 'deposit-on-base',
  inputs: { funding: resources.erc20(BASE_USDC, BASE) },
});
destFlow.lifi.zap('deposit', {
  bind: { amountIn: destFlow.inputs.funding },
  config: { resourceOut: resources.erc20(A_BAS_USDC, BASE) },
  // The guard proves the minimum received that the compiler commits.
  guards: [guards.slippage({ port: 'amountOut', bps: 50 })],
});

// 2. The source flow: runs now, on Arbitrum, inside the user's transaction.
const source = sdk.flow(ARBITRUM, {
  name: 'bridge-and-deposit',
  inputs: { amountIn: resources.erc20(ARB_USDC, ARBITRUM) },
});
const bridge = source.lifi.bridge('bridge', {
  bind: { amountIn: source.inputs.amountIn },
  // No `destination`: the bridge delivers into the settle node's escrow on Base.
  config: { resourceOut: resources.erc20(BASE_USDC, BASE) },
});
source.continuation.settle('settle', {
  bind: { input: bridge.amountOut }, // funded by the bridge: omit `funding`
  config: {
    outcomes: [{ token: A_BAS_USDC, destination: owner, chainId: BASE }],
    continuationFlow: destFlow.build(),
    // At least max(3 × the bridge's ETA, 30 minutes) away.
    validity: { expiresAtMs: Date.now() + 60 * 60 * 1000 },
    // Receives the escrow's balance if the destination flow doesn't run.
    // Must be an address you control on Base.
    refund: owner,
  },
});

// 3. Compile.
const result = await source.compile({
  signer: owner,
  inputs: {
    amountIn: materialisers.directDeposit({ amount: '1000000000' }), // 1,000 USDC
  },
  // Returns anything the source flow leaves on the proxy, such as the bridge's unspentIn.
  sweepTo: source.context.sender,
});
if (result.status !== 'success') {
  // Never fund an escrow from a partial response.
  throw new Error(`${result.error.kind}: ${result.error.message}`);
}

const escrow = result.settlements?.settle?.escrow;
if (escrow === undefined) throw new Error('no escrow was registered');
const outcome = result.outputs['settle.output'];
console.log('escrow', escrow.address, 'on chain', escrow.chainId);
console.log('minimum received', outcome?.amount.minimum?.value);
console.log('expected outcome', outcome?.amount.estimate?.value);

// 4. Submit the actions in order: approvals, then `execute`. All on Arbitrum.
if (result.fill?.validUntilMs !== undefined && Date.now() > result.fill.validUntilMs) {
  throw new Error('quote expired: compile again');
}
const wallet = createWalletClient({ account, chain: arbitrum, transport: http() });
const publicClient = createPublicClient({ chain: arbitrum, transport: http() });
let sourceTxHash: Hex | undefined;
for (const action of result.actions ?? []) {
  if (action.kind !== 'transaction') continue;
  const tx = action.transactionRequest;
  const hash = await wallet.sendTransaction({
    to: tx.to as Address,
    data: tx.data as Hex,
    value: BigInt(tx.value),
    gas: tx.gasLimit === undefined ? undefined : BigInt(tx.gasLimit),
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== 'success') throw new Error(`${action.purpose} reverted`);
  if (action.purpose === 'execute') sourceTxHash = hash;
}
if (sourceTxHash === undefined) throw new Error('no execute action');

// 5. Track the bridge (LI.FI) and the escrow (Composer) until the escrow is final.
// COMPLETED and REFUNDED are final. REFUND_FAILED is retried automatically a
// limited number of times, so it needs attention only if it persists.
// An escrow that was never funded stays EXPIRED. The settlement service keeps
// checking it for about an hour (six checks, ten minutes apart), so stop with a
// margin past that.
const FINAL = ['COMPLETED', 'REFUNDED'];
const NEVER_FUNDED_AFTER_MS = 90 * 60 * 1000;
const REFUND_RETRY_WAIT_MS = 30 * 60 * 1000;
let refundFailedSince: number | undefined;

async function bridgeStatus(txHash: Hex): Promise<string> {
  const query = new URLSearchParams({
    txHash,
    fromChain: String(ARBITRUM),
    toChain: String(BASE),
  });
  const res = await fetch(`https://li.quest/v1/status?${query}`, {
    headers: { 'x-lifi-api-key': process.env.LIFI_API_KEY! },
  });
  if (!res.ok) return 'NOT_FOUND'; // LI.FI indexes the transfer shortly after mining
  const body = (await res.json()) as { status?: string };
  return body.status ?? 'NOT_FOUND';
}

async function escrowStatus(chainId: number, address: string): Promise<string> {
  const query = new URLSearchParams({
    chainId: String(chainId),
    address: address.toLowerCase(),
  });
  const res = await fetch(`https://composer.li.quest/intent-factory/status?${query}`, {
    headers: { 'x-lifi-api-key': process.env.LIFI_API_KEY! },
  });
  if (!res.ok) return `HTTP_${res.status}`;
  const body = (await res.json()) as { data: { status: string } };
  return body.data.status;
}

for (;;) {
  const [bridgeState, escrowState] = await Promise.all([
    bridgeStatus(sourceTxHash),
    escrowStatus(escrow.chainId, escrow.address),
  ]);
  console.log(`bridge: ${bridgeState}  escrow: ${escrowState}`);
  if (FINAL.includes(escrowState)) break;
  if (escrowState === 'REFUND_FAILED') {
    refundFailedSince ??= Date.now();
    if (Date.now() - refundFailedSince > REFUND_RETRY_WAIT_MS) break; // contact LI.FI support
  } else {
    refundFailedSince = undefined;
  }
  if (bridgeState === 'FAILED') break; // the escrow never receives funds
  if (escrowState === 'EXPIRED' && Date.now() > escrow.expiresAtMs + NEVER_FUNDED_AFTER_MS) break; // never funded
  await sleep(10_000);
}
```

## Run it

Save the file as `bridge-and-deposit.mts` and run it:

```bash theme={"system"}
LIFI_API_KEY=... PRIVATE_KEY=0x... npx tsx bridge-and-deposit.mts
```

> [!WARNING]
>
> 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](https://docs.li.fi/composer/composer-api/ops/continuation-settle.md#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](https://docs.li.fi/composer/composer-api/reference/escrow-status.md#what-to-show-your-user) 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`](https://docs.li.fi/composer/protocols-and-chains); 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](https://docs.li.fi/composer/composer-api/ops/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.

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

| Error | Fix |
| - | - |
| `delivery_error` with `details.reason: 'floor_drift'` | The bridge's second quote dropped below the minimum funding registered from its first quote. Compile again. |
| `delivery_error` with `details.reason: 'eta_exceeds_expiry'` | The bridge's ETA does not fit the expiry rule. Push `validity.expiresAtMs` further out. |
| The destination flow fails "at the funding floor" | The zap has no route on Base for this landing token, or the guard cannot hold at the minimum funding. Pick another position or loosen the guard. |

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

## Related

* [Build a cross-chain flow](https://docs.li.fi/composer/composer-api/guides/build-a-cross-chain-flow.md): this flow, step by step.
* [`continuation.settle`](https://docs.li.fi/composer/composer-api/ops/continuation-settle): every config field and port.
* [Pay cross-chain](https://docs.li.fi/composer/composer-api/recipes/pay-cross-chain.md): an exact destination flow funded by a LI.FI Intents order.


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