Steer Protocol
Developer Guide

Aggregator Market Access Plan

LI.FI-first quote-to-fill plan for Steer signed-quote liquidity.

Purpose

This pass makes Steer liquidity usable through LI.FI Intents without requiring the LI.FI frontend or integrators to understand M1, registered-issuer payloads, or Uniswap v4 hook data.

Current scope: LI.FI Intents only. 0x and UniswapX remain deferred.

Target Outcome

By the end of this work we should be able to:

  1. Register the solver fill account with LI.FI.
  2. Push a bounded quote to LI.FI.
  3. Receive one selected LI.FI order without manually pasting JSON.
  4. Build real hook material for that selected order.
  5. Generate LI.FI fill calldata.
  6. Dry-run the fill plan.
  7. Submit the fill in live mode.
  8. Reconcile fill status.

Architecture

Use a small vertical slice instead of a full venue operating platform.

lifi:register-solver
-> quote routes loop
-> live M1 capacity preview
-> LI.FI quote body
-> LI.FI /quotes/submit
-> selected-order intake
-> build request derivation
-> hook material generation
-> LifiHookExecutor.finaliseEscrowOrder calldata
-> escrowed input callback into SignedCollateralSwapRouter
-> hook-generated output delivery into LI.FI output settler
-> guarded transaction submit
-> reconciliation

The offchain signer does not discover price. The preview/pricing path selects a price, and the build/fill path only enforces the selected price envelope. In the default LI.FI path, the solver key submits and signs identity/quote material, but the output inventory is produced by the hook executor during the LI.FI same-chain escrow callback.

Kept Code

  • quote-server startup integration for LI.FI quote publishing
  • lifi:register-solver for solver account registration
  • lifi:run for dry-run checks and one-off fills
  • lifi:serve for the real quote-publish/order-poll/fill loop
  • LI.FI client
  • quote publication helpers
  • selected-order normalization
  • order validation primitives
  • fill calldata builder
  • approval/fill submitter
  • PGlite replay journal for approval/fill idempotency

Current Runtime

The retained commands are for registration, readiness, quote serving, explicit test order creation, and one-off fill debugging:

pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:register-solver -- --live
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:submit-trust-components
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:submit-trust-components -- --live --strict
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:readiness -- --strict
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:serve
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:serve -- --live --once
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:run -- --publish-quotes
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:run -- --fill-once
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:run -- --publish-quotes --fill-once
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:open-escrow-order -- --quote-backed
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:open-escrow-order -- --quote-backed --live

Add --live only after reviewing the dry-run output.

LI.FI solver API auth uses the x-api-key header in the current provisioned environment. Quote submission uses the live solver API chain fields fromChain and toChain, even though older docs have shown fromChainId and toChainId. Do not change the payload field names without rechecking the provisioned API.

lifi:serve is the preferred bounded check before unattended operation. Use --once for a single publish/poll/fill iteration, then remove --once for the continuous solver loop.

Required Work For Unattended Operation

1. Registration gate

  • Keep LI_FI_SOLVER_ACCOUNT_ADDRESS, LI_FI_SOLVER_ACCOUNT_MESSAGE, and LI_FI_SOLVER_ACCOUNT_SIGNATURE in server-only env.
  • Run lifi:register-solver -- --live during setup.
  • Verify the fill private key derives the registered solver account.
  • Treat registration, API key, inventory, and trust components as startup readiness gates.

Current support: lifi:serve emits a solver readiness report before loop work. In live mode, LI_FI_SERVE_VERIFY_IDENTITY=true checks /solver-api/solver/identities and fails closed before quote/order/fill work if the signer is not registered for the active API key. lifi:readiness provides the same non-mutating identity readback as a standalone command. Current live evidence: the active LI.FI dev API key reports one identity and the quote signer is registered. lifi:submit-trust-components validates the configured oracle/settler components locally and can submit them once LI.FI confirms the live dev route. The docs currently name /quotes/trustComponents, but dev returns 404 for that path.

2. Quote refresh loop

  • Wrap current --publish-quotes behavior in a supervised interval runner.
  • Add per-route logging for stale capacity, no-quote, quote body generation, and LI.FI submit result.
  • Continue using live M1 capacity as the only v1 quote source.
  • Keep range, TTL, decimals, exclusiveFor, and max quote count explicit in config.

Current support: lifi:readiness -- --strict reads LI.FI uploaded quotes and checks that the configured quote routes are represented by uploaded quote ranges for the expected chains, assets, exclusiveFor, and amount. Live evidence: after publishing, LI.FI dev reports one uploaded quote matching sepolia-collateral-usdm1-route-0 and zero current orders.

3. Selected-order intake

  • Replace LI_FI_SELECTED_ORDER_JSON with live order fetch/polling first.
  • Add WebSocket intake only after polling proves the fill path.
  • Deduplicate by orderId.
  • Validate every selected order against configured chains, tokens, trusted components, exclusiveFor, and deadline buffers.

Current one-shot support: if LI_FI_SELECTED_ORDER_JSON is omitted, lifi:run -- --fill-once fetches orders from LI_FI_ORDER_FETCH_PATH or /orders. The solver path consumes the open-order feed rather than targeting a single order id. Controlled order support: lifi:open-escrow-order -- --quote-backed requests a LI.FI integrator quote, selects the quote assigned to our configured hook executor/exclusive filler, follows the returned approval check, and opens using LI.FI's returned order.openIntentTx. In live mode it approves the returned spender when needed and opens the order on-chain so the order server can discover it without manual selected-order JSON. It reports open.onChainOrderId when the order id can be extracted from the receipt; that value can be used as operator evidence. The raw local StandardOrder builder still exists for compatibility/debugging but is not the intended solver queue path. The order opener can use LI_FI_TEST_ORDER_PRIVATE_KEY and LI_FI_TEST_ORDER_ADDRESS for the funded escrow user while SPONSOR_SIGNER_PRIVATE_KEY remains the registered solver/fill key. If the order key is omitted, the opener falls back to the quote signer for single-wallet testing. In live mode the opener preflights signer native balance, input-token balance, and allowance before submitting approval/open transactions. It fails closed when native balance is below LI_FI_MIN_NATIVE_BALANCE_WEI or token balance is below LI_FI_TEST_INPUT_AMOUNT. For same-chain controlled tests, the opener defaults missing settlement addresses to LI.FI's maintained InputSettlerEscrow 0x000025c3226C00B2Cdc200005a1600509f4e00C0 and Output Settler 0x0000000000eC36B683C2E6AC89e9A75989C22a2e, and defaults both oracle fields to the Output Settler.

4. Build request derivation

  • Derive BuildRequest from the validated selected order.
  • Bind orderId, LI.FI quote reference, selected deadline, pool id, output token, output amount, settlement executor, solver account, and replay key.
  • Persist the replay key before transaction submission.

Current one-shot support: if LI_FI_BUILD_REQUEST_JSON is omitted, the runner derives the build request from the selected order and requires TARGET_POOL_ID. Plain and non-M1 registered-issuer fills still need a manual LI_FI_QUOTED_PRICE_E18; registered-issuer-m1 replaces that manual price with the M1 collateral/usdm ratio. registered-issuer-m1-deposit remains a legacy alias for existing environments.

5. Hook material generation

  • Replace LI_FI_HOOK_MATERIAL_JSON with internal hook material generation.
  • Ask the quote-server signed-quote path for the selected price envelope.
  • Fetch optional registered-issuer/M1 payloads when the route requires them.
  • Refuse fill if hook material cannot be built for the selected order.

Current one-shot support: if LI_FI_HOOK_MATERIAL_JSON is omitted, the runner uses LI_FI_HOOK_MATERIAL_MODE to generate hook material internally. plain signs standard CollateralManager hook data, registered-issuer signs an AtomicBroker registered-issuer payload from LI_FI_BROKER_PAYLOAD, and registered-issuer-m1 derives the M1 direction from the selected order tokens: collateral -> USDM1 fetches deposit material, while USDM1 -> collateral fetches redeem material. The legacy registered-issuer-m1-deposit value maps to the same direction-aware path. In that mode the signed quotedPriceE18 is derived as collateralPrice.index / usdmPrice.index; a caller-supplied price is accepted only if it matches the M1 ratio.

For LI_FI_FILL_MODE=hook-executor, the default quote trader is LI_FI_HOOK_EXECUTOR. This matches the v1 accounting path: the executor receives escrowed inputs, calls the signed swap router, and owns the resulting CollateralManager collateral/debt position while generated output tokens are handed to LI.FI.

The executor contract is the registered LI.FI solver identity for v1. The operator EOA submits transactions through the executor, and the executor validates EIP-1271 registration signatures from SOLVER_SIGNER_ROLE.

6. Fill and replay safety

  • Default fills use the hook executor path. The fill transaction calls LifiHookExecutor.finaliseEscrowOrder(...); LI.FI's input settler transfers escrowed inputs to the executor and calls orderFinalised(...).
  • The callback approves and calls SignedCollateralSwapRouter with exact-input calldata built from the LI.FI order, LI_FI_POOL_KEY_JSON, and signed hook material. It then approves LI.FI's output settler and calls fillOrderOutputs with hook-generated output tokens.
  • Keep LI_FI_FILL_MODE=direct-output-settler only as a compatibility test; it requires solver-side output inventory and is not the Steer production path.
  • Use LI_FI_REPLAY_PGLITE_DATA_DIR for durable PGlite replay storage.
  • Make approval and fill submission idempotent by replay key.
  • Add pending-fill timeout and operator alerts.

Current one-shot support: live fills fail closed unless LI_FI_REPLAY_PGLITE_DATA_DIR is configured. Approval and fill submissions are stored durably in PGlite and conflicting replay-key reuse is rejected.

7. Reconciliation

  • Query LI.FI/order status or on-chain receipts after submission.
  • Record confirmed and reverted approvals/fills.
  • Add settlement/finalisation after output delivery is proven.

Current support: lifi:serve queries GET /orders/status with the order's onChainOrderId after each attempted fill and includes the raw status payload in its report. It also reads durable replay storage and reports fills older than LI_FI_PENDING_FILL_ALERT_SECONDS that remain unconfirmed. When status reports a delivered/proven output and finalise config is present, it builds escrow or compact input-settler finalise calldata and reports the plan. Live mode can submit finalise only when LI_FI_SERVE_SUBMIT_FINALISE=true, LI_FI_REPLAY_PGLITE_DATA_DIR, and RPC config are present; submissions are recorded through the same PGlite replay store.

8. Unattended command

pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:serve

lifi:serve should own quote refresh, selected-order intake, build request derivation, hook material generation, fill execution, and reconciliation. It must fail closed unless live mode, API key, registered solver account, durable storage, RPC, quote routes, and trust components are configured.

Current support: lifi:serve refreshes quotes, polls the order API, dedupes orders by orderId in-process, and delegates each new order to the validated fill path. Use --once for a bounded single iteration. Use LI_FI_SERVE_PUBLISH_QUOTES, LI_FI_SERVE_FILL_ORDERS, LI_FI_SERVE_POLL_INTERVAL_MS, and LI_FI_SERVE_MAX_ITERATIONS for loop control.

Quote Inputs

  • LI_FI_INTENTS_API_KEY
  • LI_FI_ORDER_SERVER_BASE
  • LI_FI_INVENTORY_ROUTES_JSON, static quote route list; each route includes token decimals
  • LI_FI_QUOTE_TTL_SECONDS
  • LI_FI_PREVIEW_POLICY_JSON
  • LI_FI_TRUST_COMPONENTS_JSON
  • LI_FI_ALLOWED_ORIGIN_CHAINS
  • LI_FI_ALLOWED_OUTPUT_CHAINS
  • LI_FI_ALLOWED_INPUT_TOKENS
  • LI_FI_ALLOWED_OUTPUT_TOKENS
  • LI_FI_MAX_CALL_BYTES
  • LI_FI_MAX_CONTEXT_BYTES

LI_FI_SOLVER_ACCOUNT_ADDRESS and LI_FI_EXCLUSIVE_FOR are derived from the deployed LI.FI hook executor when CHAIN_ID and deployments/manifests/<chainId>-lifi-hook-executor.json are present. Keep LI_FI_SOLVER_ACCOUNT_MESSAGE and LI_FI_SOLVER_ACCOUNT_SIGNATURE only for the one-time lifi:register-solver operation.

Fill Inputs

  • CHAIN_ID
  • QUOTE_RPC_URL or RPC_URL for live fills. If omitted, loadRuntimeEnv falls back to SEPOLIA_RPC_URL.
  • SPONSOR_SIGNER_PRIVATE_KEY. If omitted, loadRuntimeEnv falls back to REGISTERED_ISSUER_QUOTE_SIGNER_PRIVATE_KEY.
  • LI_FI_ORDER_FETCH_PATH, used when selected-order JSON is omitted
  • LI_FI_SERVE_PUBLISH_QUOTES
  • LI_FI_SERVE_VERIFY_IDENTITY
  • LI_FI_SERVE_FILL_ORDERS
  • LI_FI_SERVE_POLL_INTERVAL_MS
  • LI_FI_SERVE_MAX_ITERATIONS
  • LI_FI_PENDING_FILL_ALERT_SECONDS
  • LI_FI_SERVE_PLAN_FINALISE
  • LI_FI_SERVE_SUBMIT_FINALISE
  • LI_FI_INPUT_SETTLER_KIND, escrow or compact
  • TARGET_POOL_ID, required when build-request JSON is omitted. If omitted, loadRuntimeEnv falls back to REGISTERED_ISSUER_POOL_ID.
  • LI_FI_REPLAY_PGLITE_DATA_DIR, required for live fills
  • LI_FI_MIN_FILL_BUFFER_SECONDS
  • LI_FI_FILL_MODE, defaults to hook-executor; use direct-output-settler only for compatibility tests
  • LI_FI_POOL_KEY_JSON
  • M1_JWT
  • LI_FI_M1_RISK_AWARE, optional
  • LI_FI_M1_DEPOSIT_REQUEST_JSON / LI_FI_M1_REDEEM_REQUEST_JSON, optional signer request overrides

Deployed registered-issuer profiles derive the LI.FI executor, input/output settlers, broker adapter, collateral, USDM1, issuer id, settlement controller, collateral manager, router, solver address, exclusive-for address, and quote-backed M1 defaults from deployments/manifests/<chainId>-*.json, with broadcast/DeployLifiHookExecutor.s.sol/<chainId>/run-latest.json as the executor fallback.

Keep these only for controlled compatibility tests:

  • LI_FI_SELECTED_ORDER_JSON
  • LI_FI_BUILD_REQUEST_JSON
  • LI_FI_HOOK_MATERIAL_JSON
  • LI_FI_QUOTED_PRICE_E18
  • LI_FI_HOOK_EXECUTOR_ROUTER_CALLDATA
  • LI_FI_HOOK_EXECUTOR_APPROVALS_JSON
  • LI_FI_HOOK_EXECUTOR_POST_SWAP_APPROVALS_JSON
  • LI_FI_HOOK_EXECUTOR_REFUND_RECIPIENT
  • LI_FI_FINALISE_DESTINATION
  • LI_FI_FINALISE_CALL
  • LI_FI_FINALISE_SIGNATURES
  • LI_FI_FINALISE_SOLVER
  • LI_FI_SOLVE_TIMESTAMPS_JSON
  • LI_FI_TEST_INPUT_TOKEN, LI_FI_TEST_OUTPUT_TOKEN, LI_FI_TEST_INPUT_AMOUNT, and LI_FI_TEST_OUTPUT_AMOUNT, optional controlled escrow order overrides. When omitted, the opener uses the first LI_FI_INVENTORY_ROUTES_JSON route and reuses route maxToAmount when it is present. Manual plain tests may still compute output from LI_FI_QUOTED_PRICE_E18.
  • optional LI_FI_TEST_* order overrides for controlled escrow order opening
  • LI_FI_MIN_NATIVE_BALANCE_WEI, optional live opener gas-balance floor

Testing

The first pass tests only the retained vertical slice:

  • dry-run quote publication produces LI.FI quote bodies without submitting
  • M1 route config uses live capacity for published quote ranges
  • solver registration command builds and submits the expected registration body
  • escrow order opener builds StandardOrder bytes and open(StandardOrder) calldata
  • selected order + build request + hook material produce fill calldata
  • fetched selected order can derive build request and plain hook material
  • fetched selected order can derive registered-issuer and M1 deposit hook material without manual hook JSON
  • PGlite replay journal persists approval and fill submissions across process opens
  • serve loop refreshes quotes, polls orders, fills new order ids, and skips duplicate order ids
  • serve loop queries LI.FI order status after attempted fills
  • serve loop plans escrow/compact finalise calldata after delivered/proven status when finalise config is present
  • finalise submitter preflights, waits for receipts, and reuses replay records
  • serve loop reports stale pending fills from durable replay storage
  • dry-run fill reports replay key, target, chain id, and no transaction hash
  • existing signer, quote, route, config, and HTTP tests continue passing

Detailed test notes live in LI.FI Intents Testing.

Next Phases

  1. Live solver registration.
  2. Live quote publish for one narrow dev route.
  3. Controlled escrow order opened on LI.FI dev and ingested from /orders.
  4. Live controlled fill.
  5. Live finalise submission against real delivered/proven LI.FI status payloads.

External References

On this page