Skip to main content
Flow Structure showed where resources and handles appear in the wire format. This page covers how they behave: consumption rules, port behaviors, and the asResource graduation.
A Flow’s runtime objects come in two flavours: resources (tokens that flow through the program and have a balance) and handles (typed scalar values like uint256, address, bool). Op calls connect to them through ports, which the manifest declares with a type and a mode. The compiler enforces the rules on this page at build time, so most “I expected this to type-check” surprises trace back here.

Resources

A resource represents a balance the execution proxy holds: either an ERC-20 ({ kind: "erc20", token, chainId }) or the native asset ({ kind: "native", chainId }). Resources are created in three ways:
  • Declared as inputs. A ResourceInput declares a resource the flow consumes. The runtime materialiser (e.g. directDeposit, balanceOf) makes the actual amount available before execution. See Build a Flow → Wire runtime values.
  • Produced by an op. Most LI.FI ops (lifi.swap, lifi.zap) produce a resource on their amountOut port. The receiving op binds that handle to its own input port.
  • Graduated by core.asResource. Sometimes you need to treat the output of an external contract call as a tracked resource, for example the receipt token an ERC-4626 vault returns from deposit(). core.asResource takes a typed handle plus a resource declaration and graduates it into a first-class resource. See the Op Catalog for the manifest signature.

Linear consumption

Resources are linear by default: exactly one downstream call may consume a given resource. If two ops both bind to the same resource handle as a consuming input, the compiler rejects the flow with a linearity_error. This guarantees the proxy never tries to spend the same balance twice. A copy-mode binding lets an op read a resource without consuming it: core.approve references a resource to approve a spender while leaving the balance available for a later consuming binding. A copy-mode read can coexist with one consuming binding for the same resource. The op manifest’s port metadata determines which binding consumes and which only reads. You don’t have to track this manually; the SDK’s typed handles model the rule.

Terminal resources

A resource that is never consumed by any op is a terminal resource. This happens either because it was produced by a terminal op (e.g. the receipt token from a vault deposit) or because a core.split output was deliberately left dangling. Terminal resources stay on the proxy at the end of execution and are subject to the flow’s sweepTo policy. See Terminal Resources for the full sweep semantics.

Handles

A handle is a typed scalar produced by the SDK builder: an input handle for a flow input, an output handle for an op’s output port, or a context handle (builder.context.sender, builder.context.executionAddress). The SDK serialises handles into wire-format $ref strings; see References for the conversion rules and the raw.ref escape hatch. Unlike resources, handles are non-linear: the same handle may be referenced by many downstream ops. Reading a handle does not “consume” it. A uint256 representing a deadline or a slippage tolerance can be wired into every op that needs it.

Ports

Ops declare typed input and output ports in the manifest. Each port has three properties the validator checks at build time:
  • Type. The value the port carries: a resource (token balance) or a handle (typed scalar).
  • Mode. For resource ports, whether the binding consumes the resource or only reads it. The default is consuming; copy-mode ports (e.g. core.approve’s) read the resource without consuming it.
  • Availability. Whether the port is an input (the op consumes or reads from it) or an output (the op produces a value other ops can bind).
Bindings whose handle doesn’t match a port’s type or mode are rejected at compile time.

Port discrimination

Use the typed handle returned by the builder (swap.amountOut) whenever possible; port type and mode are implicit and the validator infers them. Reach for raw.ref<T>(path) only for paths the typed API can’t reach (see References). When you do, the type guards in the manifest decide whether the path is a handle or a resource port; if you pick the wrong type parameter, the validator rejects the flow.

providesMinimum

Some output ports already encode a minimum-out invariant. lifi.swap.amountOut is the canonical example: the aggregator bakes in minOut from the swap’s slippage config, so the port declares providesMinimum: true in the manifest. The validator rejects redundant minimum-out guards on those ports. See Guards for the full attachment rules.
Bindings between ports are written as refs. The next page, References, covers the grammar.

See also