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

# Build a Flow

> End-to-end walkthrough of authoring a Composer Flow: setup, inputs, ops, materialisers, guards, preconditions, and submission.

This guide walks through every step of building a Flow with `@lifi/composer-sdk`: configuring the SDK for production, declaring inputs, chaining ops, supplying runtime values, attaching safety invariants, and submitting. Read it top to bottom on first integration; come back to specific sections as reference.

For a 5-minute first-flow demo, see the [Quickstart](/composer/composer-api/quickstart). For the SDK type surface, see the [SDK API reference](/composer/composer-api/reference/sdk-api).

## Production setup

### Configure the SDK

```ts theme={"system"}
import { createComposeSdk } from '@lifi/composer-sdk';

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

if (!process.env.LIFI_API_KEY && process.env.NODE_ENV === 'production') {
  throw new Error('LIFI_API_KEY is required');
}
```

`ComposeSdkOptions`:

| Option    | Type                       | Notes                                                                                                                                            |
| --------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `baseUrl` | `string`                   | Base URL of the Compose API. SDK default is `https://composer.li.quest`.                                                                         |
| `apiKey`  | `string`                   | Required during the technical preview. Sent as the `x-lifi-api-key` header on every request. Create one at [portal.li.fi](https://portal.li.fi). |
| `fetch`   | `typeof globalThis.fetch?` | Optional. Inject a polyfill, logging wrapper, or retry/backoff middleware.                                                                       |

### Wire in a signer

`@lifi/composer-sdk` itself does **not** sign or broadcast transactions. It returns a `transactionRequest` object (`{ to, data, value, gasLimit? }`) that you hand to your wallet library.

<Tabs>
  <Tab title="viem">
    ```ts theme={"system"}
    import { createWalletClient, custom } from 'viem';
    import { mainnet } from 'viem/chains';

    const wallet = createWalletClient({
      chain: mainnet,
      transport: custom(window.ethereum),
    });

    const [signer] = await wallet.requestAddresses();

    const result = await builder.compile({
      signer,
      inputs: { /* … */ },
    });

    const hash = await wallet.sendTransaction({
      account: signer,
      to: result.transactionRequest.to as `0x${string}`,
      data: result.transactionRequest.data as `0x${string}`,
      value: BigInt(result.transactionRequest.value ?? '0'),
    });
    ```
  </Tab>

  <Tab title="ethers v6">
    ```ts theme={"system"}
    import { BrowserProvider } from 'ethers';

    const provider = new BrowserProvider(window.ethereum);
    const wallet   = await provider.getSigner();
    const signer   = await wallet.getAddress();

    const result = await builder.compile({
      signer,
      inputs: { /* … */ },
    });

    const tx = await wallet.sendTransaction({
      to: result.transactionRequest.to,
      data: result.transactionRequest.data,
      value: BigInt(result.transactionRequest.value ?? '0'),
    });
    ```
  </Tab>
</Tabs>

The `signer` field in `run` is purely informational for the compiler; it derives the user's execution proxy address and sources approvals. The SDK does not validate that the address controls a private key.

### Custom fetch (optional)

```ts theme={"system"}
const sdk = createComposeSdk({
  baseUrl: 'https://composer.li.quest',
  fetch: async (url, init) => {
    const start = Date.now();
    const res   = await globalThis.fetch(url, init);
    console.log(`[compose] ${init?.method ?? 'GET'} ${url} → ${res.status} in ${Date.now() - start}ms`);
    return res;
  },
});
```

Common uses: metrics and tracing, retry with exponential backoff, request signing, `AbortSignal` wiring.

## Declare inputs

Inputs are a named record. Each entry is either a **resource declaration** (token) or a **scalar type name**.

```ts theme={"system"}
import { createComposeSdk, resources } from '@lifi/composer-sdk';

const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';

const builder = sdk.flow(1, {
  name: 'my-flow',
  inputs: {
    amountIn: resources.erc20(WETH, 1),
  },
});
```

The builder exposes typed handles as `builder.inputs.<name>`:

* `builder.inputs.amountIn` is a `ResourceInputHandle` carrying the WETH resource.

Inputs can also be scalar Solidity types (e.g. `'address'`, `'uint256'`), each surfaced as an `InputHandle<T>`. Declare a scalar input only when a later op or `sweepTo` actually consumes it — for example a `recipient: 'address'` you forward to `sweepTo` (see [`swapToRecipient.ts`](https://github.com/lifinance/composer-sdk-examples/blob/main/examples/swapToRecipient.ts)).

`options.name` is optional; when omitted, the SDK generates a UUID for the Flow's `id` field. Resource semantics (linear consumption, the `asResource` graduation, port behaviors) are covered in [Resource Model](/composer/composer-api/concepts/resources-and-ports).

## Chain ops in your flow

The SDK provides a typed method for each op the Composer API supports:

* `builder.lifi.swap(id, { bind, config, guards? })`
* `builder.lifi.zap(id, { bind, config, guards? })`
* `builder.core.call(id, { bind, config, ... })`
* `builder.core.asResource(id, ...)`, `builder.core.balanceOf(id, ...)`, and the arithmetic methods `builder.core.add(id, ...)`, `subtract`, `multiply`, `divideDown`, `divideUp`, `bpsDown`, `bpsUp` — one method per arithmetic op.

Each returns a typed map of output handles keyed by output-port name. Bind one call's output handle directly into another call's `bind` map:

```ts theme={"system"}
import { resources } from '@lifi/composer-sdk';

const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';

const swap = builder.lifi.swap('swap', {
  bind: { amountIn: builder.inputs.amountIn },
  config: {
    resourceOut: resources.erc20(USDC, 1),
    slippage: 0.03,
  },
});

builder.lifi.zap('zap', {
  bind: { amountIn: swap.amountOut },          // ← thread the swap output straight in
  config: { resourceOut: resources.erc20(A_ETH_USDC, 1) },
});
```

Each method is typed: passing a `'resource'` handle into a non-resource slot is a compile-time error. The dotpath strings the SDK ultimately serialises to (e.g. `swap.amountOut`) are covered in [References](/composer/composer-api/concepts/ref-grammar).

### Runtime context

Runtime values that the compiler fills in at execution time are accessed through `builder.context`:

* `builder.context.sender` is a typed ref to the signer address (`address`).
* `builder.context.executionAddress` is a typed ref to the predicted execution proxy address (`address`).

These are used most often in `bind` slots that want the signer as a recipient, or as the `sweepTo` target.

### `untypedOp` escape hatch

`builder.untypedOp` is the escape hatch for capabilities the typed SDK surface doesn't expose yet — a new op the generated methods don't cover, or an experimental config field. It lets you author the node directly instead of waiting for an SDK release:

```ts theme={"system"}
builder.untypedOp('custom', 'experimental.op', {
  bind: { x: { $ref: 'input.amountIn' } },
  config: { customField: 42 },
});
```

`untypedOp` returns `void` and accepts raw `Ref` values in its `bind` map — no handle conversion, no type checks. To feed an `untypedOp` node's output into a typed downstream call, wrap the path with `raw.ref<T>(path)`; you supply the phantom type and the SDK accepts it in any `Bindable<T>` slot (there is no runtime validation):

```ts theme={"system"}
import { raw } from '@lifi/composer-sdk';

builder.core.asResource('wrapped', {
  bind: { handle: raw.ref<'uint256'>('custom.result') },
  config: { resource: resources.erc20(USDC, 1) },
});
```

Prefer typed builder methods whenever they exist; reach for `untypedOp` only when they don't.

## Wire runtime values

A flow declares *what kind* of value each input expects. The concrete value is supplied at compile time, in `run.inputs`, when you call `builder.compile(run)` or `sdk.request(flow, run)`.

### Three input shapes

For every input the flow declares, `run.inputs[name]` must be one of three shapes:

* **`bigint`.** A literal numeric value. Used for scalar inputs declared as `uint256` and for resource inputs whose amount you already know upfront. Serialised on the wire as a `0x`-padded 32-byte hex string.
* **`string`.** A pre-encoded value. For scalar inputs, the string must already conform to the declared type (e.g. a `0x…` address for an `address` input, a decimal or hex integer for a `uint256`).
* **`MaterialiserInput`.** A descriptor `{ kind: "<materialiser>", …config }` telling the service *how* to compute the value (see below).

```ts theme={"system"}
const result = await builder.compile({
  signer: '0xYourSignerAddress',
  inputs: {
    minOut: 2_400_000_000n,                                  // literal bigint (uint256 scalar input)
    amountIn: materialisers.directDeposit({                  // materialiser descriptor
      amount: '1000000000000000000',
    }),
  },
  sweepTo: builder.context.sender,
});
```

Every input declared by the flow **must** appear in `run.inputs`. Missing or unknown keys fail with `validation_error`.

### What a materialiser is

A **materialiser** is a deterministic pure function `(flow, context) → binding` that resolves a flow input at compile time. It tells the service how to produce the handle the VM consumes at the top of the program: for example, "deposit exactly 1 WETH via `transferFrom`" (`directDeposit`) or "read the signer's on-chain USDC balance at execution time" (`balanceOf`).

Why not always use a literal?

* **The value isn't known until execution.** `balanceOf` reads the signer's balance at transaction time, which lets you write flows like "zap whatever USDC I hold right now."
* **The value needs specific VM instructions.** `directDeposit` doesn't just push a number; it bakes in the `transferFrom` semantics that pull the tokens into the proxy and, for exact deposits, an equality check that the expected amount arrived.

Built-in materialisers:

| Kind                    | Accepts    | What it does                                                                                                                                                                                                                                                                                                                       |
| ----------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `directDeposit`         | `resource` | Deposit into the VM. Native coin via `msg.value`, ERC-20 via `transferFrom`. `{ amount }` for exact; `{ allowNonExact: true }` to deposit whatever the approval allows.                                                                                                                                                            |
| `balanceOf`             | `resource` | Read `balanceOf(owner)` on the resource token (or native `balance` for native coin) at execution time and use it as the input amount.                                                                                                                                                                                              |
| `call`                  | `resource` | Execute an arbitrary contract call, then measure the balance delta of the resource for the execution proxy, and use that delta as the input amount.                                                                                                                                                                                |
| `flashloan` *(preview)* | `resource` | **ETHGlobal hackathon only — unaudited.** Borrow the amount from a flashloan provider (`aave-v3`, `erc3156`, `balancer-v2`, or `morpho-blue`); the proxy populates it at callback time. Available only on the hackathon deployment — not in the production SDK. See [`flashloan`](/composer/composer-api/materialisers/flashloan). |

See the [materialisers catalog](/composer/composer-api/materialisers) for the live list and per-kind config schemas.

### Assumptions for runtime amounts

When you use a runtime materialiser (`balanceOf`, `call`, or `directDeposit` with `allowNonExact: true`) the compiler doesn't know the concrete amount at compile time. Most of the time this is fine; the VM reads the amount at execution time. But when the compiler needs the amount to **plan routes** (for example, to pick an aggregator quote for a `lifi.swap`) it falls back to `run.assumptions[name]`, a per-input `bigint` hint.

```ts theme={"system"}
await builder.compile({
  signer: '0xYourSignerAddress',
  inputs: {
    amountIn: materialisers.balanceOf({ owner: '0xYourSignerAddress' }),
  },
  assumptions: {
    amountIn: 1_000_000_000_000_000_000n,
  },
  sweepTo: builder.context.sender,
});
```

If a route plan depends on a runtime amount and no assumption is supplied, compilation may fail with `preparation_error` (HTTP 422). Provide an assumption whenever you expect the runtime amount to differ materially from the default.

## Add safety: preconditions and guards

A flow's safety story has two halves. **Preconditions** describe state that must be true *before* the transaction runs, feeding the simulator as overrides. **Guards** are invariants the VM enforces *during* the transaction, baked into the calldata.

### Preconditions: state before execution

A precondition is a declarative assertion about on-chain state. Three types are registered:

| Type             | Asserts                                                             | Config                                 |
| ---------------- | ------------------------------------------------------------------- | -------------------------------------- |
| `Erc20Balance`   | A wallet holds at least a given ERC-20 balance                      | `{ wallet, token, balance }`           |
| `NativeBalance`  | A wallet holds at least a given native coin balance                 | `{ wallet, balance }`                  |
| `Erc20Allowance` | An `owner` has granted at least `allowance` of `token` to `spender` | `{ owner, spender, token, allowance }` |

The SDK exposes typed factories:

```ts theme={"system"}
import { preconditions } from '@lifi/composer-sdk';

preconditions.erc20Balance({
  wallet: '0xYourSignerAddress',
  token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  balance: '1000000',
});
preconditions.nativeBalance({ wallet: '0x…', balance: '1000000000000000000' });
preconditions.erc20Allowance({
  owner: '0x…',
  spender: '0x…',
  token: '0x…',
  allowance: '1000000000000000000',
});
```

Preconditions reach the simulator via two paths, concatenated at compile time:

* **Explicit.** Anything you pass in `run.preconditions`.
* **Materialiser-produced.** For example, `directDeposit({ amount })` on an ERC-20 resource automatically emits an `Erc20Balance` precondition and an `Erc20Allowance` precondition.

You rarely need to duplicate materialiser-produced preconditions explicitly. Add explicit preconditions when the flow's expectations aren't already captured by its inputs, for example when using `balanceOf` to source an amount that should already be on the proxy.

At compile time, preconditions are sent to the simulator as **state override requirements**. The simulator patches storage slots so the wallet has the specified balance / allowance, then runs the simulation against that synthetic state. Simulation **does not** fail just because live on-chain state doesn't match a precondition; the whole point is to let the simulator proceed *as if* the precondition held. The caller is responsible for waiting until the real state catches up before submitting the transaction.

### Guards: invariants during execution

Guards are passed as the `guards` option on an op call:

```ts theme={"system"}
import { guards } from '@lifi/composer-sdk';

builder.lifi.zap('zap', {
  bind: { amountIn: builder.inputs.amountIn },
  config: { resourceOut: resources.erc20(A_ETH_USDC, 1) },
  guards: [
    // 100 bps (1%) slippage floor on the zap's amountOut port.
    guards.slippage({ port: 'amountOut', bps: 100 }),
  ],
});
```

The builder rejects guards that aren't compatible with a call's ports at compile time. See [Guards](/composer/composer-api/concepts/simulation-and-guards) for what guards do under the hood; see the [guards catalog](/composer/composer-api/guards) for every registered guard.

You can attach more than one guard to the same op; they run independently during simulation. When two guards produce a minimum for the same port, the larger value wins and is reported on `producedResources[*].simulated.amountOutMin`.

Don't apply guards that duplicate the internal behaviour of an op. Configure `slippage` on the op itself, not via a guard, whenever the op accepts a `slippage` config field. `lifi.swap` is the canonical example: passing `slippage: 0.03` bakes a 3% floor into the aggregator quote, and the `amountOut` port carries `providesMinimum: true` so a redundant slippage guard would be refused at compile time.

### When to use which

|                    | Preconditions                                                | Guards                                           |
| ------------------ | ------------------------------------------------------------ | ------------------------------------------------ |
| **When**           | Before the transaction runs (state overrides for simulation) | During the transaction (on-chain assertions)     |
| **Where enforced** | Caller's responsibility (wait for real state to match)       | Compiled into the transaction itself             |
| **Shape**          | Plain data: `{ type, …config }`                              | Op-attached invariant with observed ports        |
| **Production**     | Explicit or materialiser-produced                            | Attached via `CallArgs.guards`                   |
| **Failure**        | Caller-visible pre-flight mismatch                           | `guard_error` at simulation; revert at execution |

Use **preconditions** to describe what must be true *before* the transaction runs. Use **guards** to enforce what must be true *during* the transaction.

## Submit the flow

```ts theme={"system"}
const result = await builder.compile({
  signer,
  inputs: {
    amountIn: materialisers.directDeposit({ amount: '1000000000000000000' }),
  },
  sweepTo: builder.context.sender,
});
```

`builder.compile(run)` is the canonical one-shot path: it calls `builder.build()`, forms a `ComposeCompileRequest` via `sdk.request`, and POSTs to `/compose`. For lower-level patterns (queueing, server-side proxy, signed-request inspection), call `builder.build()` and `sdk.request(flow, run)` directly. See the [SDK API reference](/composer/composer-api/reference/sdk-api).

`sdk.compile()` does **not** exist at the SDK level. Compile lives on the builder.

### The run input

Every submission passes a `ComposeRunInput`: per-input values, the signer address, optional preconditions, a `sweepTo` target for terminal resources, and policy fields (`simulationPolicy`, `checkOnChainAllowances`, `maxPriceImpactBps`). See the [SDK API reference](/composer/composer-api/reference/sdk-api) for the full type signature.

`sweepTo` is a catch-all that moves **every** residual proxy balance to the target — but some terminal resources can't be moved (e.g. aTokens that back an open debt). See [Terminal Resources](/composer/composer-api/concepts/sweeping-and-amounts#non-transferable-terminal-resources) for when to emit explicit transfers instead.

### Approvals

`ComposeCompileResult.approvals` lists any ERC-20 approvals the signer must grant before the compose transaction can execute. The signer must approve the execution proxy for each input token. Submit those approvals first, then the compose transaction.

Pass `checkOnChainAllowances: true` in `run` to have the server check current on-chain allowances and omit approvals that are already sufficient.

## Errors you might see

Compile-time errors:

| Kind                                                      | Cause                                                                               |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `Missing input "<name>"` (`validation_error`)             | A declared flow input has no entry in `run.inputs`.                                 |
| `Unknown input "<name>"` (`validation_error`)             | `run.inputs` has a key the flow did not declare. Check for typos.                   |
| `Unknown materialiser "<kind>"`                           | The descriptor's `kind` is not registered in the manifest.                          |
| `Materialiser "<kind>" accepts resource inputs only`      | The materialiser is typed for the other kind of input.                              |
| `linearity_error`                                         | Two ops both bind the same resource handle as a consuming input.                    |
| `Guard targets port "<p>" which provides its own minimum` | Attached a minimum-out guard to a port whose op already declares `providesMinimum`. |
| `Guard targets port "<p>" which does not exist`           | Port name typo or wrong op.                                                         |

Simulation-time and runtime errors:

| Kind                           | Cause                                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------------------ |
| `preparation_error` (HTTP 422) | A runtime amount was needed and no `assumptions` value was supplied.                       |
| `no_route_error` (HTTP 404)    | The routing layer could not find a path for a `lifi.zap` edge.                             |
| `guard_error` (HTTP 422)       | A guard assertion would fail at execution time. Loosen tolerance or investigate the quote. |
| `simulation_revert` (HTTP 422) | The simulated transaction reverts on-chain. Inspect the revert details.                    |

Under `simulationPolicy: "allow-revert"` a `simulation_revert` returns the compiled calldata *and* the revert diagnostics under HTTP 206 instead of failing outright. Use this when you want to surface the revert reason to the user.

For the full error catalog see [Errors](/composer/composer-api/reference/error-codes).

## See also

* [Quickstart](/composer/composer-api/quickstart) — five-minute first-flow walkthrough.
* [Recipes](/composer/composer-api/recipes) — three canonical flows you can copy.
* [SDK API reference](/composer/composer-api/reference/sdk-api) — exhaustive API surface.
* [Concepts](/composer/composer-api/concepts/flow-anatomy) — Flow shape, refs, resources, execution model, guards.
* [Errors](/composer/composer-api/reference/error-codes) — full `ComposeError` catalog.
