# Almanak SDK

> Python SDK for writing, checking, running and backtesting autonomous DeFi strategies

# Overview

# Almanak SDK

Almanak is a Python SDK for writing, checking, backtesting and operating autonomous DeFi strategies. A strategy consists of two author-maintained files: `strategy.py`, which makes decisions, and `config.yaml`, which states the economic choices the strategy may use.

The same strategy definition runs in historical evaluation, identified forks and supported production environments. The SDK keeps network access, secrets, signing, durable execution and protocol mechanics outside strategy code.

## Learn how it works

- [How the SDK works](https://sdk.docs.almanak.co/how-it-works.html) explains the decision loop, trusted components, durable state and the differences between backtest, fork and production runs.
- [Getting started](https://sdk.docs.almanak.co/getting-started.html) walks through a complete Uniswap V4 trading reference, validates it offline, and then runs a backtest or optional fork rehearsal.

Start here if you are evaluating the SDK or need to understand why its author, operator and execution boundaries are separate.

## Build a strategy

- [Strategy authoring](https://sdk.docs.almanak.co/authoring.html) is the contract for hooks, decision context, typed memory, outcomes and teardown.
- [Configuration](https://sdk.docs.almanak.co/configuration.html) describes author YAML, exact quantities, named accounts and markets, and trusted resolution.
- [Backtesting](https://sdk.docs.almanak.co/backtesting.html) covers explicit models, prepared data and offline reproduction.
- [Example strategies](https://sdk.docs.almanak.co/examples.html) provides a curated route through the executable references by concept and complexity.
- [Supported connectors](https://sdk.docs.almanak.co/support.html) is generated from the declarations used by admission. Use it to verify a connector, chain, action and run mode before choosing an example.

These pages are the preferred path for both human authors and coding agents. Complete executable behavior remains in the linked reference strategies.

## Operate and inspect

- [Operating](https://sdk.docs.almanak.co/operating.html) covers private operator configuration, retained workspaces, pause, resume, teardown, shutdown and production prerequisites.
- [CLI reference](https://sdk.docs.almanak.co/cli/almanak.html) lists every command and option from the current parser.
- [API reference](https://sdk.docs.almanak.co/api/authoring.html) documents the public Python contracts used by strategy code.
- [JSON schemas](https://sdk.docs.almanak.co/api/schemas.html) are the machine-readable author and backtest configuration contracts.
- [Daily QA](https://sdk.docs.almanak.co/qa.html) describes the evidence and coverage used to qualify declared support.

## Core guarantees

Safe funds own protocol positions and proceeds. Operators submit and pay gas; EOA execution is also supported. Strategies cannot access secrets, sign or contact providers. The gateway independently checks authority, while the runtime retains pending identities, accounting evidence and outcome delivery.

Every action keeps its named fund, CAIP-2 chain and CAIP-19 asset scope. Missing measurements remain missing rather than becoming zero or an unrequested fallback.

The package is published on PyPI as `almanak` from version 3.0.0. The import module is also `almanak`.

# How the SDK works

An Almanak strategy proposes economic actions. It does not talk directly to a chain, hold credentials, sign transactions or decide whether an operation was completed. Trusted SDK components resolve those responsibilities and retain the evidence needed to recover safely.

## The complete path

```
strategy.py + config.yaml
          |
          v
sandboxed strategy hook  -->  decision + proposed memory
          |                            |
          v                            v
trusted runtime  <-->  connector  <-->  authenticated gateway  <-->  provider / chain
          |
          v
durable state, outcomes, accounting and reports
```

Backtests use the same strategy hooks and economic intents, but replace live execution with an explicit historical model. Fork and production runs use the shared execution, reconciliation and accounting path with different operator authority.

## What each part owns

| Part                   | Owns                                                                              | Does not own                                                            |
| ---------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Strategy               | Economic decisions and proposed memory changes                                    | Secrets, network access, signing, persistence or authoritative holdings |
| Author configuration   | Markets, accounts, allocations, quantities, timing and selected financial sources | Providers, wallet addresses, signing material or fork fixtures          |
| Runtime                | Scheduling, durable work, recovery, outcome delivery and teardown                 | Protocol-specific transaction construction or credentials               |
| Connector              | Protocol observations, actions, transaction plans, position semantics and cleanup | Strategy scheduling or independent durable state                        |
| Gateway                | Controlled reads, authorization, credentials, signing and submission              | Strategy decisions or lifecycle progression                             |
| Operator configuration | Providers, account bindings, fork fixtures and production authority               | Economic strategy choices                                               |
| Durable record         | Captured inputs, pending work, outcomes, positions and evidence                   | A second scheduler or an alternative view of completion                 |

This separation lets the gateway reject an unauthorized request even if strategy code proposes it, and lets the runtime recover admitted work without asking the strategy to guess what happened.

## The decision cycle

1. The runtime checks whether a strategy evaluation is eligible.
1. Connectors acquire the explicitly requested observations through the gateway, or read retained historical inputs in a backtest.
1. The sandbox receives resolved configuration, strategy memory, owned positions, observations, chain availability and the current trigger.
1. `decide(ctx)` returns at most one typed economic intent and an optional memory update.
1. The runtime records the admitted decision before external execution.
1. The connector expands the economic intent into the protocol steps it owns, such as approvals, a swap, repayment or allowance cleanup.
1. The gateway independently checks the exact operation and its execution authority before a live or fork submission.
1. The runtime reconciles the result, updates connector-owned positions and delivers the retained outcome to `on_outcome(ctx)`.

An approval or another setup step can produce an `intermediate` outcome. The runtime continues that already admitted economic action without calling `decide()` again. Only economic completion produces `completed`; a refused or unsuccessful operation produces `failed`.

## State, positions and recovery

Strategy memory records decision state such as a phase or start time. It is not an authoritative account balance or proof that a position exists. Connectors and the durable runtime record own economic positions, pending transactions, allowances, debt and other obligations.

Before resuming, the runtime reconciles recorded external work. An unknown outcome cannot authorize a replacement action. Captured strategy source and configuration remain fixed for the run. Persisted strategy memory evolves through accepted updates and remains available for recovery, so editing the original files does not change an open deployment.

Teardown is also a durable lifecycle state. It bypasses the ordinary decision interval and asks the strategy and connectors to reduce risk and close their known obligations. A successful transaction receipt alone is not proof that all debt, positions or spending authority are clear.

## Three environments

| Environment | What it proves                                                           | Authority and data                                                                                      |
| ----------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Backtest    | Strategy behavior under a named historical model and frozen assumptions  | Hypothetical inventory; preparation may use read-only providers, execution and reproduction are offline |
| Fork        | Real contract behavior at an identified chain state with simulated funds | Generated fixture accounts and authority on retained local fork processes                               |
| Production  | Supported execution against a live chain                                 | Explicit operator account bindings, payer ceilings and signing authority                                |

Results from one environment do not imply guarantees from another. A passing backtest does not prove contract execution, and a fork lifecycle does not authorize real spending.

## Identity and scope

Chains use strict CAIP-2 identifiers such as `eip155:1`. Assets use CAIP-19 identifiers such as `eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48`. Symbols such as USDC and WETH are display metadata, not identity.

Every action remains scoped to a named fund account, chain, connector target and allocation. The gas payer can be different from the account that owns the position. Likewise, two allocation names do not create separate ownership of the same account and protocol balance.

## Continue from here

- Follow [Getting started](https://sdk.docs.almanak.co/getting-started.html) for a complete reference journey.
- Use [Strategy authoring](https://sdk.docs.almanak.co/authoring.html) while writing `strategy.py`.
- Use [Configuration](https://sdk.docs.almanak.co/configuration.html) while writing `config.yaml`.
- Check [Supported connectors](https://sdk.docs.almanak.co/support.html) before selecting a connector, chain, action or environment.
- Browse [Example strategies](https://sdk.docs.almanak.co/examples.html) to find the nearest executable starting point.
# Learn

# Getting started

This tutorial follows the [Uniswap V4 USDC trading reference](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v4_trading). It is small enough to read in one sitting but includes the full strategy lifecycle: enter, hold, recover after restart and exit.

The tutorial uses a repository checkout because reference strategies and sample operator configurations are not installed with the Python package.

## Requirements

Install Python 3.12+ and [uv](https://docs.astral.sh/uv/). Local strategy hook execution is currently verified on Linux. On Debian or Ubuntu, install the required sandbox with:

```
sudo apt-get install bubblewrap libseccomp2
```

Fork execution also requires Foundry's `anvil`. macOS packages and CLI commands are available, but macOS currently rejects the author-worker memory limit, so strategy hook execution fails closed there until that policy is redesigned.

Clone the repository and install its locked dependencies:

```
git clone https://github.com/almanak-co/sdk-v3.git
cd sdk-v3
uv sync --locked
```

For a separate project, install the SDK with `uv add almanak`. To install only the command-line tool, use `uv tool install almanak`. Commands in this tutorial run from the checkout and therefore use `uv run --locked`.

## Read the two author files

The reference has two inputs maintained by the strategy author:

- [`strategy.py`](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v4_trading/strategy.py) contains the decision hooks and typed memory.
- [`config.yaml`](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v4_trading/config.yaml) selects the market, fund, capital, risk bounds and timing.

The configuration joins three names. This abbreviated excerpt shows the binding; use the complete reference file as the executable input:

```
strategy:
  allocations:
    trading:
      market: trading_market
      account: trading
      capital:
        asset: eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
        quantity: '100'

markets:
  trading_market:
    connector: uniswap_v4
    chain: eip155:1
    # Complete pool identity follows in the reference.

accounts:
  trading:
    chain: eip155:1
```

`trading` is a local allocation and account name. `trading_market` is a local market name. On-chain identity comes from the connector target, CAIP-2 chain and CAIP-19 assets, never from these labels or token symbols.

The strategy keeps only the time at which each purchase completed:

```
from pydantic import Field

from almanak.api import StrategyState, declared


class State(StrategyState):
    bought_at: dict[str, int] = Field(default_factory=dict)


def capabilities(config):
    return declared(config)
```

At each evaluation, `decide()` reads the connector observation for an allocation. It asks the connector to enter if the position is not complete, and to perform a full exit when the hold expires or teardown begins. `hold_seconds` is optional and read only by this code; without it the allocation holds until teardown:

```
def decide_allocation(context, allocation, observed):
    started = state(context).bought_at.get(allocation.name)
    due = (
        started is not None
        and allocation.hold_seconds is not None
        and int(observed.effective_at.timestamp()) >= started + allocation.hold_seconds
    )
    if context.teardown or due:
        return context.actions.exit(allocation.name)
    if not observed.owned.entry_complete:
        return context.actions.swap(allocation.name)
    return None
```

The connector owns approvals, swap construction, residual conversion and allowance cleanup. `on_outcome()` records the purchase time only after the economic swap is complete. An intermediate approval does not advance strategy memory.

## Check it offline

Run the source and author-configuration checks without provider access:

```
uv run --locked almanak check strategies/uniswap_v4_trading --format text
```

The result should report `valid: True`, with `configuration` and `sandbox` passed. It reports `ready: False` because market resolution, hooks and online admission deliberately remain `not_checked` without an explicit provider and run mode. A skipped online stage is not a passing readiness claim.

Inspect the exact connector declaration used during admission:

```
uv run --locked almanak support \
  --connector uniswap_v4 --chain eip155:1 --format text
```

Use this command before adapting the strategy to another connector, chain, action or mode. The generated [support catalog](https://sdk.docs.almanak.co/support.html) presents the same data.

## Run the historical evaluation

The reference's `backtest-economic.yaml` adds a historical window, hypothetical inventory, execution latency, sampling bounds, costs and end treatment. Provider access is needed only to prepare or find matching historical inputs. Create an ignored `.env` containing the operator's Alchemy key:

```
ALCHEMY_API_KEY=your-key
```

Check the author and backtest inputs together, then run:

```
uv run --locked almanak check strategies/uniswap_v4_trading \
  --backtest-config strategies/uniswap_v4_trading/backtest-economic.yaml \
  --format text
uv run --locked almanak backtest run strategies/uniswap_v4_trading \
  --backtest-config strategies/uniswap_v4_trading/backtest-economic.yaml \
  --gateway-config examples/operators/backtest/eip155-1.yaml \
  --env-file .env --output runs/uniswap-v4-tutorial
```

Preparation resolves the configured market at the historical boundary and collects the explicit model inputs. Economic execution then runs offline. The workspace retains the resolved facts, data identity, decisions, hypothetical positions, costs and progress.

Reproduce the run without provider access and render a portable report:

```
uv run --locked almanak backtest reproduce --run runs/uniswap-v4-tutorial
uv run --locked almanak report --run runs/uniswap-v4-tutorial \
  --format html --output uniswap-v4-tutorial.html
```

## Optional fork rehearsal

A fork exercises the real deployed contracts at the exact anchor in the operator fixture. It uses simulated funds and generated authority; it does not authorize real spending.

First resolve and validate the complete fork inputs through a read-only gateway:

```
uv run --locked almanak check strategies/uniswap_v4_trading --fork --online \
  --gateway-config examples/operators/uniswap_v4_trading.fork.yaml \
  --env-file .env --format text
```

Then start the supervised run:

```
uv run --locked almanak run strategies/uniswap_v4_trading --fork \
  --gateway-config examples/operators/uniswap_v4_trading.fork.yaml \
  --env-file .env --output runs/uniswap-v4-fork
```

Ctrl-C pauses the coordinator while retaining the gateway, fork process, positions and pending work. Inspect and close the deployment explicitly:

```
uv run --locked almanak status --run runs/uniswap-v4-fork
uv run --locked almanak resume --run runs/uniswap-v4-fork
uv run --locked almanak teardown --run runs/uniswap-v4-fork
uv run --locked almanak status --run runs/uniswap-v4-fork
uv run --locked almanak shutdown --run runs/uniswap-v4-fork
```

Wait for recorded closure before shutdown. A stopped fork cannot be recreated as the same deployment, and resume never invents replacement history.

## Build your own

Use this order when adapting a reference:

1. Find the nearest supported lifecycle in [Example strategies](https://sdk.docs.almanak.co/examples.html).
1. Confirm its connector, chain, actions and run modes in [Supported connectors](https://sdk.docs.almanak.co/support.html).
1. Change author choices in `config.yaml` while preserving strict chain and asset identities.
1. Change `strategy.py` through the public hook and action contracts in [Strategy authoring](https://sdk.docs.almanak.co/authoring.html).
1. Run the offline check after every small change.
1. Add an explicit backtest configuration and inspect the retained assumptions.
1. Use a fork only when the strategy passes its earlier checks.

Read [Configuration](https://sdk.docs.almanak.co/configuration.html) for the author schema, [Backtesting](https://sdk.docs.almanak.co/backtesting.html) for data and model limits, and [Operating](https://sdk.docs.almanak.co/operating.html) before working with retained fork or production workspaces.
# Build

# Strategy authoring

A strategy directory contains `strategy.py` and `config.yaml`. There is no strategy base class. The SDK captures the validated parameters and Python source for each run, so editing the original files does not change an open or resumed deployment.

Use [Getting started](https://sdk.docs.almanak.co/getting-started.html) for an end-to-end walkthrough and [Example strategies](https://sdk.docs.almanak.co/examples.html) to choose an executable starting point. This page is the authoring contract to keep beside the code.

## Public imports

Import shared author types from `almanak.api` and connector decision types from the selected connector's public API:

```
from pydantic import Field

from almanak.api import Memory, StrategyState, declared
from almanak.connectors.uniswap_v4.api import Decision, DecisionInput, OutcomeInput
```

Do not import runtime, gateway, execution or signer implementations into a strategy. Connector-specific parameters and observation types are resolved from the author configuration before hooks run.

## Hook contract

| Hook                            | Required | Contract                                                                                                                                                                                             |
| ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `capabilities(parameters)`      | Yes      | Return the result of public `declared(parameters)`. It declares the economic actions the resolved configuration allows.                                                                              |
| `decide(ctx)`                   | Yes      | Return the connector's typed decision with at most one economic intent and the proposed memory.                                                                                                      |
| `validate_config(parameters)`   | No       | Return `None` or raise an actionable error. It runs in the sandbox after trusted resolution.                                                                                                         |
| `data_dependencies(parameters)` | No       | Return explicit historical or financial observation dependencies, such as the [price series](#price-series-and-technical-analysis) decisions read. An omitted hook means no additional dependencies. |
| `on_outcome(ctx)`               | No       | Return memory after an admitted action stage. An omitted hook leaves memory unchanged.                                                                                                               |
| `on_event(ctx)`                 | No       | Return memory after a connector-owned external event such as a liquidation. An omitted hook leaves memory unchanged.                                                                                 |
| `dashboard(ctx)`                | No       | Return a typed read-only dashboard over retained evidence. Failure cannot change execution state or suppress the shared report.                                                                      |

Hooks are plain module-level functions. Expected neutral market conditions should return a decision without an intent. Programming errors should raise and remain visible rather than being converted into healthy no-action results.

## Declaring capabilities

Every strategy must declare the bounded actions implied by its resolved author configuration:

```
def capabilities(config):
    return declared(config)
```

The trusted connector derives exact assets, protocol calls, recipients and cleanup permissions from this declaration. Strategy code cannot add authority, install permissions or provide a handwritten permission override.

`validate_config()` can enforce policy that is specific to the strategy rather than the connector schema:

```
def validate_config(config):
    if any(allocation.range is not None for allocation in config.allocations):
        raise ValueError("trading allocations must not request liquidity permissions")
```

Schema validation and trusted gateway checks still apply. Strategy validation does not replace either boundary.

## Reading decision context

`decide(ctx)` receives the current resolved view. The important fields and helpers are:

| API                                    | Meaning                                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `ctx.config`                           | One authoritative resolved parameter instance, fixed for the run.                                 |
| `ctx.memory`                           | The strategy's last committed memory.                                                             |
| `ctx.owned`                            | Connector-owned position records. These are separate from strategy memory.                        |
| `ctx.observations`                     | Current connector observations made available for this evaluation.                                |
| `ctx.availability`                     | Per-chain readiness or retained fault state.                                                      |
| `ctx.teardown`                         | `True` when the durable deployment is closing.                                                    |
| `ctx.prices`                           | The declared [price series](#price-series-and-technical-analysis), as available at this decision. |
| `ctx.price(asset)`                     | The one declared series for an asset, or raise.                                                   |
| `ctx.config.allocation(name)`          | Resolve a named validated allocation.                                                             |
| `ctx.observation(name)`                | Return an observation or `None`, while tracking its scope.                                        |
| `ctx.require_observation(name)`        | Return an observation or raise a scoped missing-data error.                                       |
| `ctx.position(name)`                   | Return a retained owned position or raise. It never invents zero holdings.                        |
| `ctx.actions`                          | Connector-owned typed economic actions bound to this context.                                     |
| `ctx.decision(intent=..., memory=...)` | Build the typed decision and retain every observation scope accessed by the intent.               |

A decision must not treat missing observations as zero. For a multi-chain strategy, unavailable work on one chain can be skipped only when the intended action and its evidence are independent of that chain.

A compact decision loop looks like this:

```
def decide(context: DecisionInput) -> Decision:
    for item in context.observations:
        observed = context.require_observation(item.allocation)
        allocation = context.config.allocation(observed.allocation)
        intent = decide_allocation(context, allocation, observed)
        if intent is not None:
            return context.decision(intent=intent)
    return context.decision()
```

The complete implementation is in the [Uniswap V4 trading reference](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v4_trading/strategy.py).

## Price series and technical analysis

A strategy that needs price history, for an indicator such as RSI or a moving average, declares each series in `data_dependencies`. Every decision then receives the latest points of that series in `ctx.prices`. The SDK reads the prices from a pool's own on-chain state through the operator's RPC provider, with no other data service, and computes no indicator; the strategy computes one itself, typically with TA-Lib.

```
import numpy as np
import talib

from almanak.api import AssetType, DataDependencies, PoolPrice, PriceBinding

WETH = AssetType("eip155:8453/erc20:0x4200000000000000000000000000000000000006")


def data_dependencies(config):
    return DataDependencies(
        prices=(
            PriceBinding(
                purpose="strategy",
                chain=config.target("position").fund.chain,
                asset=WETH,  # priced in the pool's other token, here USDC
                window=120,  # hourly closes each decision sees
                source=PoolPrice(
                    kind="pool_state",
                    connector="uniswap_v3",
                    pool="0xd0b53d9277642d899df5c87a3966a349a798f224",
                    timeframe="1h",
                    max_age_seconds=7200,
                    availability_lag_seconds=300,
                ),
            ),
        )
    )


def decide(ctx):
    series = ctx.price(WETH)
    if not series.complete:
        return ctx.decision()  # a short or stale window decides nothing
    rsi = float(talib.RSI(np.array([float(v) for v in series.values]), 14)[-1])
    ...
```

**Declaration.**

- **Purpose:** `purpose` is `strategy`.
- **One pool per series**, on the binding's chain, named for its connector: `connector` is `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome` (classic volatile or stable pools) or `curve` (StableSwap NG, TwoCrypto NG and TriCrypto NG pools) with the pool contract address as `pool`, or `uniswap_v4` with the pool id. The connector's own pool resolution verifies the pool, so a pool the connector refuses (a Curve metapool, a pool no reviewed factory registers, a hooked V4 pool) is refused as a price source too. The declared `asset` must be one of the pool's coins. The pool need not be one the strategy trades: a strategy on any connector may name a supported pool on the same chain. `ax pool` checks a V3-style pool and its tokens.
- **Quote:** `quote` names the coin the asset is priced in. It defaults to the other coin of a two-coin pool and is required for a Curve pool of three or more coins (TriCrypto, multi-coin StableSwap), where it must be another coin of the pool; the SDK never picks one.
- **Two sources:**
- `PoolPrice` (`kind: pool_state`): one close per candle, the pool's price at the end of the last block at or before the candle boundary. A boundary with no trades still has a close, the pool's actual state, so the series has no gaps. The close is the pool's spot price: the V3/V4 pool price; for Aerodrome the reserves' ratio (volatile) or the marginal price of the x³y + y³x invariant on decimal-normalized reserves (stable); for Curve StableSwap the invariant's marginal price from balances, coin decimals and the current A; for Curve TwoCrypto and TriCrypto the pool's own `last_prices`, its spot price after its last trade. All exclude fees.
- `PoolSwapCandles` (`kind: pool_swaps`): OHLCV candles. A candle opens at the previous boundary's close and closes at this boundary's; its high and low also cover every trade price in between, and its volume is the sum of the trades' absolute amounts of the quote coin, in whole tokens (USDC above). A trade's price is the pool's resulting spot price for V3/V4 Swap events and Aerodrome `Sync` events (Aerodrome volume is the larger of each `Swap`'s quote amounts in and out). For Curve it is the execution price of each `TokenExchange` between the asset and the quote, the bought amount over the sold amount, so it includes the pool fee: Curve highs and lows can lie outside the spot closes by about the fee, and trades between other coins of the pool are not counted. A candle without trades is flat with zero volume. It reads every trade log of the pool, so it costs more provider reads than closes.
- **Price unit:** the declared asset in its quote coin, decimal-adjusted (WETH in USDC above), rounded to 30 significant digits. `series.quote` names the quote. The SDK converts to no other unit. Ratio and relative indicators (RSI, MACD, EMA/SMA crosses, Bollinger bands, Stochastic, percentage moves) do not depend on the unit. A strategy that needs absolute USD levels, or USD comparisons across assets, names a stablecoin-quoted pool on the same chain as its price source; any supported pool may be named, not only the traded one.
- **Timeframe:** `1m`, `5m`, `15m`, `1h`, `4h`, `12h` or `1d`.
- **Window:** `window` is how many points each decision sees, at most 8760.
- **Age and lag:** `availability_lag_seconds` is how long after its boundary a point counts as available: block-finality and confirmation slack. `max_age_seconds` must cover the timeframe plus the lag; it is how old the latest point may be for the series to count as `complete`.
- **One series per asset.**
- **Single prices (Aave V4 economic backtests only):** a strategy price without a window may instead read a CoinGecko coin price (`CoinGeckoPrice`) or a Spoke oracle (`ProtocolPrice`). It arrives in `ctx.prices` as one `PriceObservation` (`value`, `source_time`, `available_at`), may also be listed in the backtest file (it must then equal the hook's declaration), and is not delivered on forks or live.

A backtest configuration never repeats a strategy's series; the backtest reads them from this hook.

**What a decision sees.** `ctx.prices` holds one `PriceSeries` per declaration: the latest `window` points, oldest first, that were available at the decision.

- **Cutoff:** a point is available once its boundary has passed and its availability lag has elapsed. Nothing later is visible.
- **No substitution:** a failed read is never replaced by another source.
- **Members:**
- `values`: the closes as `Decimal`.
- `opens`, `highs`, `lows`, `volumes` (swap candles only; a closes series raises): the other candle members as `Decimal`.
- `points`: `source_time` (the boundary), `value` (the close) and, for swap candles, `opened_at`, `open`, `high`, `low` and `volume`.
- `quote`: the token the points are priced in.
- `latest`: the newest point.
- `gaps`: consecutive points further apart than the timeframe.
- `complete`: true when the window is full, evenly spaced and its latest point is within `max_age_seconds`.

Decide nothing on an incomplete series, as above. `ctx.price(asset)` raises for an asset without exactly one declared series.

**Every run mode sees the same points.**

- **Backtests:** preparation reads each series through the operator's archive provider from `window` points before the start, so the first decision already has a full window.
- **Fork and live runs:** the trusted gateway reads the series from its chain before each decision, cut off at the earlier of the decision time and the observed block time, and retains what it served. A fork therefore sees the points as of its fork block. The gateway refuses to start when a declared pool does not resolve on its chain or does not hold the declared asset. If a read fails at a decision, that decision holds (no action) and the next one tries again. Teardown decisions receive no series in any run mode, backtests included, so a failed read never blocks an unwind: exit before reading `ctx.price` when `ctx.teardown` is true, as the reference does.

**Indicators.**

- **Library:** hosted runtimes and backtests include numpy and TA-Lib; elsewhere install `almanak[ta]`. Convert `values` (and `highs`, `lows` or `volumes` for indicators such as ATR, stochastics or OBV, which need `PoolSwapCandles`) to float arrays.
- **Window size:** smoothed indicators settle only after several periods, so declare a window several times the indicator's period (`window=120` for RSI-14 on hourly closes).
- **Crossings:** keep the previous indicator value in memory and act when it crosses a level, not on every decision past the level, which would repeat the trade. A previous value or an entry price is a decision record, which memory may hold. Memory is committed with the decision, before its action executes: to retry an entry whose action failed, record it as pending and confirm it in `on_outcome`, as the reference does. Decisions more frequent than the timeframe see the same latest point until the next boundary.
- **Levels or crossings:** a crossing needs the series to move between two decisions. On a fork the series stands still at the fork block, so a strategy that acts only on crossings never acts there. A rule on a level (RSI inside a band, a close above its average) gives the same answer for the same series and can be exercised on a fork anchored where the level holds. Keep memory unchanged while holding: derive values such as the highest close since entry from the series instead of writing them every decision.
- **The strategy bounds its own trades.** An exchange allocation is offered an entry quote whenever it holds funding inventory, and authority carries no amount bound, so a signal that fires again can enter again. `entry_complete` records first exposure; it does not stop re-entry. A strategy that should trade one cycle records that in memory, as the reference does.
- **Timed exits are the strategy's.** `allocation.hold_seconds` is optional and only strategy code reads it; no connector closes a position when it elapses. When it is `None` there is no scheduled exit and the allocation holds until teardown, so a stop-only strategy omits it.

The [Uniswap V3 Base RSI reference](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v3_base_rsi/strategy.py) is a complete strategy: it declares the hourly WETH closes of its own pool, computes RSI-14 with TA-Lib, buys on a cross below 30 and sells on a cross above 70. Three more Base references use levels: an [RSI-gated LP](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v3_base_rsi_lp/strategy.py), a [loan held above a moving average](https://github.com/almanak-co/sdk-v3/blob/main/strategies/aave_v3_base_sma_borrow/strategy.py) and an [ATR breakout on full candles](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v3_base_atr_breakout/strategy.py).

## Requesting economic actions

Use `ctx.actions` instead of constructing protocol transactions. Actions remain bound to the configured allocation and the observations accessed by the decision.

Lending connectors expose actions such as `supply`, `borrow`, `repay` and `withdraw`. Exchange connectors expose actions such as `swap`, `open_liquidity`, `collect`, `close_liquidity` and `exit`. The exact supported set is connector-specific; check [Supported connectors](https://sdk.docs.almanak.co/support.html) and the nearest reference before using one. The generated [strategy actions](https://sdk.docs.almanak.co/support.html#strategy-actions) list gives each connector's exact `ctx.actions` signatures. In the bundled docs it is the "Strategy actions" section of Supported connectors and chains, with one heading per connector such as "uniswap_v3 actions" or "gmx_v2 actions" listing each method and what it does, for example what `exit` teardown closes, converts and revokes, and `repay` versus `repay_amount`.

Concentrated-liquidity allocations configured with a `band` instead of a fixed `range` recentre from strategy code. `open_liquidity` selects the position centred on the observed tick, the retained `owned.position.range` says where it sits, and `swap(name, rebalance=True)` follows the `rebalance_quote` that brings inventory to the ratio the next centred position consumes:

```
position = observed.owned.position
if position is not None:
    if position.range.lower <= observed.pool.tick < position.range.upper:
        return None
    return ctx.actions.close_liquidity(name)
if observed.rebalance_quote is not None and not already_rebalanced:
    return ctx.actions.swap(name, rebalance=True)
return ctx.actions.open_liquidity(name)
```

Every selection is checked against the band by the planner and the gateway; a strategy cannot move liquidity outside it.

An action is economic, not a promise of one transaction. For example, an exit can close an owned LP position, collect fees, convert residual strategy inventory to the configured funding asset and revoke remaining spending authority. The connector owns that sequence and its completion test.

Some counter inventory is too small to swap. Fees left behind by an out-of-range close are a common example. When its exact-size exit quote returns nothing, the observation carries it as `exit_residual` (`exit_residuals` for Curve) instead of an `exit_quote`. No swap is offered for it, `actions.exit` returns `None` once it is the only remaining obligation, and the dust stays in owned inventory and reports. A zero-output entry or rebalance quote is simply absent.

An entry slice the market cannot fill at the observed block, because the Quoter reverts, the pool has no liquidity for it or a backtest swap would leave the prepared ticks, offers no `entry_quote`. The observation records why as `entry_quote_error`, and its exit and rebalance quotes are unaffected. Exit observations and teardown in backtests measure no entry quote.

## Typed memory

Define `State` as a subclass of `StrategyState` when a strategy needs durable decision state:

```
from typing import cast


class State(StrategyState):
    version: int = 1
    bought_at: dict[str, int] = Field(default_factory=dict)


def state(context) -> State:
    return cast(State, context.memory)
```

The worker hydrates `State` inside the sandbox. Return an immutable update with `state.updated(...)`:

```
return context.decision(memory=state(context).updated(bought_at=new_times))
```

The durable store retains a JSON envelope containing the version, schema identity and values. Incoming and returned state are validated. The fields `values` and `schema_id` are reserved for that envelope. A strategy without a `State` class uses generic `Memory`.

Memory records decisions, phases and timers. It must not duplicate authoritative balances, protocol positions, debt, transaction identities or execution progress. Those belong to connectors and the runtime.

Resume uses the captured source and its memory schema. Existing memory cannot silently become another schema, reset to defaults or migrate automatically.

## Outcomes and intermediate work

`on_outcome(ctx)` receives the retained result of the admitted economic action:

```
def on_outcome(context: OutcomeInput) -> Memory:
    if context.result.status != "completed":
        return context.memory
    return state(context).updated(...)
```

`ctx.result.status` is:

- `completed` when the connector's economic completion condition is satisfied.
- `intermediate` when an admitted action still has steps to perform.
- `failed` when the operation did not succeed.

Do not advance economic state after an approval or another intermediate step. The runtime continues intermediate work without another call to `decide()`. A crash between external completion and outcome delivery replays the retained outcome rather than the economic action.

Connector obligation helpers report `clear`, `remaining` or `unknown`. Unknown is not clear and cannot authorize more risk or shutdown without an explicit operator decision.

## Teardown

When `ctx.teardown` is true, the strategy should stop opening risk and request the appropriate exit for any known position. Teardown bypasses the ordinary decision interval but uses the same actions, authorization, durable work and outcome handling as normal execution.

Prefer the connector's full-exit action when it exists. It can own protocol cleanup that a strategy cannot safely infer from a single balance. What each connector's `exit` closes, converts and revokes, and whether it must be called again until it returns None, is listed under that connector's heading in [Strategy actions](https://sdk.docs.almanak.co/support.html#strategy-actions). If required observations are missing, preserve the obligation rather than returning a false closure claim.

## Isolation and external data

All author hooks run without network access, credentials, signing or filesystem writes. They share bounded resources and output limits. Missing or unusable OS isolation refuses execution; there is no unsandboxed fallback.

External information must be declared and delivered as typed observations through the trusted gateway or retained historical inputs. A requested venue, pool, oracle or vendor source cannot silently become another price source.

## Author checklist

- Import only shared and connector public author contracts.
- Declare capabilities from the same resolved parameters used by decisions.
- Access observations before returning an intent so their scopes are retained.
- Keep CAIP chain and asset identity intact throughout configuration and state.
- Keep economic positions and transaction progress out of strategy memory.
- Declare price history in `data_dependencies` and decide nothing on an incomplete series.
- Advance memory only on the outcome status that actually proves the transition.
- Handle teardown and missing evidence explicitly.
- Run `almanak check` after every small change, then add backtest and fork evidence in that order.

# Author configuration

`config.yaml` is the directory convention. `--config PATH` selects another YAML file explicitly. Backtests always require `--backtest-config PATH`. Runtime manifests, resolved facts, effective parameters and persistence remain JSON.

## Files and ownership

The SDK keeps economic choices separate from operational authority:

| Input                    | Maintained by                | Contains                                                                                              |
| ------------------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------- |
| `strategy.py`            | Strategy author              | Decision hooks, capability declaration and optional typed memory                                      |
| `config.yaml`            | Strategy author              | Markets, named funds, allocations, quantities, timing and explicit financial sources                  |
| Backtest YAML            | Backtest author or evaluator | Historical window, hypothetical inventory, model, data bounds, costs and end treatment                |
| Operator configuration   | Deployment operator          | Provider bindings, account addresses, fork fixtures, payer ceilings and production signing references |
| Private environment file | Deployment operator          | Provider and vendor credentials referenced by name from operator configuration                        |

Provider URLs, keys, wallet addresses and signing files do not belong in author configuration. Conversely, the operator file cannot silently change the strategy's market, quantity, risk or financial-source choices.

See the [Uniswap V4 trading configuration](https://github.com/almanak-co/sdk-v3/blob/main/strategies/uniswap_v4_trading/config.yaml) for a compact exchange example and the [Aave V4 configuration](https://github.com/almanak-co/sdk-v3/blob/main/strategies/aave_v4_weth_supply/config.yaml) for multiple named funds, supply positions and debt.

## Author sections

| Section     | Meaning                                                                                                                                              |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `strategy`  | Decision interval, named allocations, optional debts and optional gas policy.                                                                        |
| `markets`   | Named market selections: the connector, CAIP-2 chain and the connector's exact target (for Aave V4 the Spoke and reserve ID).                        |
| `accounts`  | Named fund accounts with their CAIP-2 chain.                                                                                                         |
| `assets`    | Optional assertions: a CAIP-19 identity mapped to expected decimals or a display symbol. An ERC-20 asset without one is named by its own `symbol()`. |
| `inventory` | Explicit account-scoped owned cash for composed strategies, with verified asset precision.                                                           |

The author file holds choices only. Everything a run must verify is resolved from the chain through a read-only gateway and retained beside the run: the reserve's Hub, the underlying asset and its precision, implementation identities and the anchor block. Providers, account bindings, fork fixtures and production authority live in the operator configuration described in the [operating guide](https://sdk.docs.almanak.co/operating.html#operator-configuration). All reference variants use this author root, including composed strategies with multiple connectors.

Allocations refer to local market names. Names and symbols are never on-chain asset identity. Markets and accounts must agree on chain. Multiple allocations can share a named fund, but names do not split ownership of one protocol balance. `strategy.gas` names payer accounts, the chain, the quote source and the exact fee bounds. Production runs require it. Only `accounts`, `chain`, `max_transaction_fee` and `exit_reserve` are required, plus `total_budget` for a lifetime cap: the amounts are spending authority and never default. The fields that tune how execution bids default as below, and a check lists every default it applied under `gas_defaults`.

| Field                     | Default                                                                                                    |
| ------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `source`                  | `network_rpc_fee_history`                                                                                  |
| `base_fee_basis`          | `next_block`; `observed_block` on Avalanche, whose fee history reports no next block                       |
| `reward_percentile`       | `50`                                                                                                       |
| `base_fee_multiplier_bps` | `20000`; `12500` on Arbitrum and Robinhood Chain, whose Nitro sequencer refunds a bid down to the base fee |
| `max_gas_limit`           | `1200000`, every action measured through Safe/Roles with the default buffer except GMX order creation      |
| `max_gas_price_wei`       | `max_transaction_fee` in wei divided by `max_gas_limit`, rounded down                                      |
| `gas_limit_buffer_bps`    | `2500`                                                                                                     |

The derived `max_gas_price_wei` is the highest price the fee ceiling admits at `max_gas_limit`, so the defaults never raise `gas_fee_ceiling`. Above it every transaction, exits included, waits for gas to fall; nothing is lost. It is also the strictest price that fee admits: a 200k-gas transaction could afford six times that price within the same fee, but waits. On L2s the market price sits far below it; on Ethereum, state `max_gas_price_wei` and a `max_gas_limit` near your largest action if smaller transactions should not wait. Set any of these fields to override its default. Fork runs with an authored gas policy require matching operator ceilings under `fork.gas`; the policy is retained and enforced against the generated fixture payers. Production runs on Base and Optimism support authored gas policies: the gateway bounds the L1 data and operator fees at quote time inside the same payer ceilings, and reconciles the exact charges from each receipt. Only fork runs on Base and Optimism refuse an authored gas policy, because the fork fixtures have no verified bound for those fees. Gasless forks on those chains remain supported.

Gas amounts are chain-specific: native gas prices differ by orders of magnitude between chains, so never reuse another chain's reference values. Size `max_transaction_fee` to pay for `max_gas_limit` at the target chain's current gas price; the defaulted price ceiling follows from it. `check --online` stays silent when it does, and otherwise reports the current quote under `gas_price` or `gas_fee_quote`. A stated `max_gas_price_wei` must keep `max_gas_limit` x `max_gas_price_wei` \<= `max_transaction_fee`. On Base and Optimism leave room in `max_transaction_fee` for the L1 data fee as well. When the live quote rises above `max_gas_price_wei`, or a transaction's quoted fee above `max_transaction_fee`, every transaction, exits included, waits until gas falls; nothing is lost, but nothing moves either. The Uniswap V3 LP references for Base, Arbitrum and Optimism carry L2 gas policies.

## Exact values

```
assets:
  eip155:1/erc20:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2:
    decimals: 18
    symbol: WETH
```

A token quantity is a quoted decimal such as `'0.01'`, or an explicit `{base_units: 10000000000000000}`. An asset amount contains `asset` and `quantity`. Conversion rejects excess precision. Trusted live or historical metadata must match declared decimals before economic activity. Display symbols cannot alter identity or precision. Resolution reads each ERC-20 market asset's `symbol()` at the anchor block and uses it where the author declares none; a token that answers no short plain symbol stays unnamed. The symbol is contract-controlled text for display: it is kept outside the verified facts and never selects an asset. Financial and strategy parameters do not expand environment variables.

Durations accept whole seconds or strings such as `5s`, `10m`, `2h`, `1d`. Dates use whole-second ISO timestamps with a timezone, such as `'2026-08-20T00:00:00Z'`. YAML rejects duplicate keys, aliases, merges, executable tags, non-finite numbers and unknown schema fields.

Trading allocations select `kind: trading` and omit LP range and collection time. LP allocations select `kind: liquidity` with explicit tick range and collection cadence. Native new-risk balance thresholds are allocation policy; payer budgets are shared execution policy and remain distinct.

### Liquidity bands

Uniswap V3, PancakeSwap V3, Aerodrome Slipstream and Uniswap V4 LP allocations declare either a fixed `range` or a `band`:

```
band:
  width_bps: 500
```

Each position opens centred on the observed tick, either `width` ticks wide or, with `width_bps`, that many basis points either side of the current price rounded up to the tick spacing. A strategy recentres by closing the position, swapping with `swap(name, rebalance=True)` and opening again, at any price level. The gateway admits only an aligned range of exactly the band width centred on the tick the decision observed.

An author who wants to provide liquidity only within a price range adds an optional envelope, `lower` and `upper` ticks in multiples of the pool tick spacing:

```
band:
  lower: -200120
  upper: -196120
  width_bps: 500
```

Positions are then clamped inside the envelope, and the gateway refuses any mint outside it before submission. Once the price leaves the envelope, positions open at its nearest edge and stay out of range until the price returns or the envelope changes; whether the strategy holds or reopens them is its own rule. The envelope is a gateway price policy, not on-chain authority, so changing it needs no new permissions. Economic backtests of a band select `liquidity_model: bracketed_historical_fee_growth` and a tick coverage that contains the whole envelope, or for a band without one, the price range the run should measure; see [Backtesting](https://sdk.docs.almanak.co/backtesting.html#liquidity-bands).

## Validation and normalization

```
uv run --locked almanak check strategies/aave_v4_weth_supply --normalized --json
uv run --locked almanak check strategies/aave_v4_weth_supply --fork \
  --gateway-config examples/operators/aave_v4_weth_supply.fork.yaml --env-file .env
uv run --locked almanak check strategies/aave_v4_weth_supply --fork --online \
  --gateway-config examples/operators/aave_v4_weth_supply.fork.yaml --env-file .env
uv run --locked almanak support --json
```

Offline checking parses the author file with source locations, checks every market, account and collateral reference and confirms the strategy sandbox. It makes no provider calls and runs no strategy hooks, because the hooks need the resolved parameters. `--production` also refuses a file without `strategy.gas` in the configuration stage, since production runs require it. A check without `--fork`, `--production` or `--backtest-config` cannot refuse that rule, so its result carries a `strategy.gas` entry under `warnings` instead. With `--gateway-config` and `--fork` or `--production` it also binds the authored choices to the operator file. `--online` starts a read-only gateway with the operator's providers, resolves the named markets, runs validation, capability and dependency hooks once on the resolved parameters, and adds the mode stage: the compiled fork fixture, or production gas-policy compatibility with the operator ceilings. `--backtest-config` checks the backtest inputs offline and, online, the historical model. `--normalized` adds the parsed inputs and, online, the resolved parameters. Every check whose file omits a defaulted gas field reports a `gas_defaults` list of `{field, value}` entries with the values the SDK supplied. Online `--fork` and `--production` checks also report a `funding` block: for each bound account, the CAIP-19 assets and base units it must hold before its first entry, and for each gas payer, the native entry minimum `max(entry_transactions x max_transaction_fee + exit_reserve, largest minimum_native_balance)` with any gas policy amounts. `entry_transactions` is an upper bound on the transactions entering every allocation the payer pays for submits, through the last one that adds exposure (an Aave supply-and-borrow allocation pair counts its approvals, supplies, collateral enablement and borrow; a trade or liquidity allocation with `entry_swap_bps` below 10000 counts each entry slice of its `capital` as a swap with its approvals), because each pays its fee and each that adds exposure must leave `exit_reserve` unspent. In a composed strategy a trade is also offered slices of shared or borrowed funding inventory beyond its `capital`; those swaps are not counted and draw on what entry leaves. It states requirements only; compare it with your own balances. For Aave V3, SparkLend, Compound V3, Morpho Blue and Aave V4 supply allocations the token amount is each allocation's `capital`, in the market asset: what the allocation starts with. Production admission, the fork fixture funding and the economic backtest inventory check use the same amount. Composed strategies state their `inventory`. Interest on borrowing is not prefunded: repaying the fund's own debt takes no configured amount, and a repayment the fund cannot cover refuses with the shortfall, which a transfer to the fund resolves without a restart.

Allocations state no spending budgets. Generated Safe permissions pin contracts, functions, assets, markets, pools and the Safe as recipient or beneficiary, with no amount limits ([scope-only authority](https://sdk.docs.almanak.co/internal/blueprints/27-scope-only-authority.html)). A lending allocation sets `borrow: true` to borrow and repay; the amounts it supplies and borrows are the strategy's decisions. An exchange allocation is offered an entry quote whenever it holds funding inventory, one `capital x entry_swap_bps / 10000` slice at a time; a strategy that should enter once checks `entry_complete`. On the Uniswap V3, PancakeSwap V3 and Aerodrome Slipstream pools the slice is the swap, and a liquidity mint offers all inventory held, so 5000 swaps about half and invests about all. On Uniswap V4 the slice sizes the whole entry quote: a trading entry swaps it, while a liquidity entry quote swaps only the part of it the mint pairs with and is offered only while no position is held. The mint still uses all inventory held, so with the usual single entry swap then `open_liquidity` a value below 10000 (the V4 default) leaves about the rest of capital idle; a banded allocation that recentres through `rebalance_quote`, or a strategy that swaps repeatedly before minting, is not limited this way. `hold_seconds` is optional on every allocation that offers it: only strategy code reads it, for its own timed exit, and connectors never act on it. Omit it to hold until teardown. `strategy.gas.total_budget` is optional; without it there is no lifetime gas cap, while `max_transaction_fee` and `exit_reserve` still apply. If the operator policy or fixture stage is refused, the error output still carries it. Backtest checks omit it. Online admission of funding and installed authority is never checked here; each result identifies passed, failed or not-checked stages.

Every check also reports findings under `warnings`, each with `code`, `field` and `message`. They are advisory:

| Code                   | Finding                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gas_limit`            | A `strategy.gas` `max_gas_limit` is below the largest measured gas of any action the policy's connectors plan through Safe/Roles, raised by its own `gas_limit_buffer_bps`. The measurements ship as reviewed fork receipts with their provenance.                                                                                                                                                                                                                                             |
| `gas_amount_units`     | A gas fee, budget or reserve exceeds 10 whole native units, which usually means base units were written as a quoted decimal.                                                                                                                                                                                                                                                                                                                                                                   |
| `gas_fee_ceiling`      | A `max_transaction_fee` is below `max_gas_limit` x `max_gas_price_wei`, so the fee ceiling refuses transactions both other ceilings admit.                                                                                                                                                                                                                                                                                                                                                     |
| `gas_budget`           | A `total_budget`, when set, is below one `max_transaction_fee` per entry transaction plus `exit_reserve`. Execution keeps `exit_reserve` unspent while it enters, so at the fee ceiling the deployment stops partway through entry. The entry count is a lower bound per allocation the policy pays for: 2 for a V3 pool trade (approve, swap), 4 for a V3 pool liquidity allocation (approve, swap, approve, mint), 1 for anything else.                                                      |
| `gas_exit_reserve`     | An `exit_reserve` is below one `max_transaction_fee` per exit transaction: 2 for a V3 pool trade (approve, swap), 3 for a V3 pool liquidity allocation (close, approve, swap), 1 for anything else.                                                                                                                                                                                                                                                                                            |
| `gas_price`            | Online only: a `max_gas_price_wei` is below the gas price execution would quote now with the policy's own `reward_percentile` and `base_fee_multiplier_bps`, so every transaction is refused until the market falls. A failed quote is reported under this code too.                                                                                                                                                                                                                           |
| `gas_fee_quote`        | Online only: at the current quote, `max_transaction_fee` pays for less gas than `max_gas_limit`, so a larger transaction waits until gas falls.                                                                                                                                                                                                                                                                                                                                                |
| `market_read`          | Online only: an unexpected failure while reading the current market for the gas price findings. The findings made before it are kept; it never fails a check.                                                                                                                                                                                                                                                                                                                                  |
| `gas_entry_slices`     | An allocation's `entry_swap_bps` splits capital into more than 10 entry slices (`ceil(10000 / entry_swap_bps)`, below 1000 bps). Each slice is its own swap with its approvals, so the payer's entry minimum includes at least that many `max_transaction_fee`s for the allocation; the message states the amount.                                                                                                                                                                             |
| `entry_budget_partial` | A Uniswap V4 liquidity allocation without a band sets `entry_swap_bps` below 10000. Its entry quote is sized from that share of capital, so with the usual single entry swap then `open_liquidity` about `capital - capital x entry_swap_bps / 10000` (rounded down) is not swapped into the pair and stays idle; a strategy that swaps repeatedly before minting is not limited this way. Banded allocations are not flagged: they recentre through `rebalance_quote` and mint all inventory. |
| `production_gas`       | A check without a mode names the production requirement for `strategy.gas`.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `decision_interval`    | Production and mode-agnostic checks: `strategy.decision_interval` is shorter than 30 seconds. Every live decision starts a fresh sandboxed worker, whose startup alone took 8 to 11 seconds on a CPU-limited hosted container, so a shorter interval can starve a hosted deployment of CPU until its evaluations time out. Fork and backtest checks do not report it.                                                                                                                          |

## Support

`almanak support` reads the same immutable connector/chain declarations used at admission. These distinguish execution modes, economic intents, account profiles and historical starting bounds. A supported combination does not imply that the selected provider is currently available or that every market has history.

Filter declarations with exact public connector and CAIP-2 chain identities:

```
uv run --locked almanak support --connector uniswap_v3 --chain eip155:8453 --json
```

An empty result means there is no declared support matching the filters. Unknown connector IDs are errors with supported candidates and a spelling suggestion when available. Suggestions never change the selected protocol. Configuration errors identify the author file, connector field and line, including named markets in composed strategies.

The [reference directories](https://github.com/almanak-co/sdk-v3/tree/main/strategies) contain explicit market examples. Their choices are author inputs, not connector defaults. `almanak support --references` lists the copies the installed SDK carries, with the same `--connector` and `--chain` filters; see [Example strategies](https://sdk.docs.almanak.co/examples.html). Editor schemas live in `docs/schemas/`; `check --normalized` shows the resolved configuration used by the SDK. `almanak support --schema strategy` (or `backtest`) prints the bundled schema set of the installed version as one JSON document, `root` naming the entry schema and `files` holding every referenced schema by file name. `--schema dashboard` prints the schema of the dashboard response the evaluator emits, for hosts that validate and render retained dashboards.

# Backtesting

Backtests execute economic intents in the shared simulator, with durable hypothetical inventory and connector-owned positions. They do not use a Foundry fork. Strategy hooks retain sandbox isolation.

The required backtest YAML contains `mode`, `window`, `inventory`, `execution`, `costs` and, for economic models, the connector `data` policy. Named funds use the author's account names and explicit asset balances; their simulation addresses are deterministic artifacts and cannot grant production authority. A modeled payer and its inventory are required when the selected cost model needs them. Historical provider bindings come from the operator configuration, one per chain. A backtest that requests vendor data names the vendor and kind in its data policy, while the operator binds that exact name and kind to an access tier and credential environment variable. Author and backtest files never contain access settings.

A strategy's [price series](https://sdk.docs.almanak.co/authoring.html#price-series-and-technical-analysis) come from its `data_dependencies` hook, never from the backtest file. Resolution runs the hook and adds each declared series to the data policy. A backtest file that lists a series is refused; only an Aave V4 economic backtest file may list a single strategy price, which must equal the hook's. Preparation reads each series from the pool's state through the operator's archive provider for the series' chain, from `window` boundaries before the window start, so the first decision already has a full window. Its historical gateway resolves the declared pool with the connector's resolution and admits reads of that pool only. The dataset keeps compact evidence: the resolved pool, each boundary's block, its successor and the raw state reads, and for swap candles each log window's trade events with a digest of the provider's answer; opening a dataset rebuilds every point from it. The series and their warm-up are part of the dataset identity, and a strategy that declares no series prepares exactly as before.

## Run and reproduce

```
uv run --locked almanak backtest prepare strategies/aave_v4_weth_supply \
  --backtest-config strategies/aave_v4_weth_supply/backtest-economic.yaml \
  --gateway-config examples/operators/backtest/eip155-1.yaml \
  --env-file .env --output datasets/aave
uv run --locked almanak backtest run strategies/aave_v4_weth_supply \
  --backtest-config strategies/aave_v4_weth_supply/backtest-economic.yaml \
  --gateway-config examples/operators/backtest/eip155-1.yaml \
  --dataset datasets/aave --output runs/aave
uv run --locked almanak backtest resume --run runs/aave
uv run --locked almanak backtest reproduce --run runs/aave
```

Preparation and a fresh run first resolve the named markets at the last block at or before the window start through a read-only gateway, retain the verified facts as `resolution.json`, and construct the parameters from them. A dataset records the facts digest it was prepared with; a run with `--dataset` reuses that retained resolution and needs no provider credentials. Without `--dataset`, economic runs prepare or reuse a matching dataset before execution. Preparation uses the operator's archive providers and the explicitly requested vendors through the read-only gateway. Execution makes no data network calls. Missing operator bindings, samples, tick coverage or price sources cannot trigger online quotes, select another vendor or substitute models. Dataset identity includes effective settings, the resolved facts and input evidence. Every fresh run requires the selected operator file, including when a retained dataset supplies the resolved facts without provider access. Every shipped reference uses the public author and backtest inputs. Archived legacy YAML is read only by private component and baseline tooling; the runtime consumes retained JSON and has no legacy author fallback.

A `resolution.json` left in the output or dataset directory by an earlier or interrupted preparation with other inputs is refused, and the error names the file: prepare into a fresh output directory, or remove that directory and prepare again.

Reproduction requires the captured implementation, frozen settings and the retained resolution whose facts digest the manifest names; it replays the retained reads offline and refuses missing or changed evidence. Resume uses retained state, pending actions and outcome delivery. Author edits cannot change that run. Do not replace datasets or silently change sampling on resume.

## Models and assumptions

- `historical_path` retains detailed operation and account-profile assumptions. Uniswap uses historical Quoter calls; Aave detailed lending currently supports Ethereum supply/withdraw only. Remove optional debts from the author configuration when selecting that Aave model.
- `economic` uses prepared Aave lending samples, including borrowing, collateral, repayment and liquidation, or bounded integer Uniswap pool state with price impact and tick crossings. LP fees use explicit fixed historical fee growth.

Hypothetical trades and liquidity do not change later external market history. LP fees exclude hypothetical dilution and fees from the strategy's own trades. Explicit vendor prices may approximate economic risk but never replace a requested oracle or venue observation. Missing measurements and model exclusions remain reported. No model here establishes production execution authority.

The strategy interval is inherited by default. A coarser override is explicit, fixed and reported as reduced cadence fidelity. Unchanged observations do not skip decisions. Keep sampling interval, maximum source age, collection limits, tick coverage, latency, costs and end treatment explicit. Collection-limit failures require an author decision; the SDK does not coarsen resolution automatically.

Progress appears on stderr; redirected stdout remains JSON. `--progress json` produces versioned snapshots, also retained beside the run directory. ETA is a heuristic from comparable timings and current throughput; unsupported estimates remain absent. Observer failures do not affect financial state.

The [historical integration evidence](https://sdk.docs.almanak.co/internal/reports/2026-09-06-backtest-integration.html) separates contract comparisons, sampled-path fidelity and measured preparation and execution timings. Component replay and fork execution are different evidence.

### Liquidity bands

Uniswap V3, PancakeSwap V3, Aerodrome Slipstream and Uniswap V4 `band` allocations run in `economic` mode only, with `liquidity_model: bracketed_historical_fee_growth`. That value requires a band allocation; fixed ranges in the same run keep exact historical range growth. Each allocation's `data.coverage` must contain the whole band envelope. A band without an envelope follows the price, so its coverage is the explicit tick range the run measures: it must hold at least one aligned band width and the position centred on the first sample's tick, and a full-range coverage measures every position the strategy can open. A run that would open a position outside the coverage stops with an error instead of crediting unmeasured fees. Preparation samples every initialized tick of the coverage with its fee growth plus every liquidity event of the pool, within the usual sampling, age and collection limits.

A position the strategy opens can sit on ticks that no historical position initialized, so its fee growth is not directly observable. Between two samples, the SDK measures the growth accrued below every tick that stayed initialized for the whole interval and brackets each position boundary between its neighbours. The lower bracket is credited; it equals the exact growth when both boundaries are continuously initialized ticks, which is common on pools with dense liquidity. The report labels band positions `fee_attribution: bracketed_lower_bound` and retains `uncollected_fee_upper_bound` and `collected_fee_upper_bound_excess`, the fees the sampled state cannot rule out. On sparse pools the credited fees can be far below the upper bound; nothing is interpolated between samples. Fees accrue between the latest samples at or before each liquidity action. Rebalance swaps use the same prepared integer pool calculator as other swaps. Recentring is bounded by the allocation's inventory, as in live execution. `historical_path` backtests and composed strategies refuse band allocations.

### Results in the funding asset

An economic Uniswap V3, PancakeSwap V3 or Aerodrome Slipstream run whose allocations share one funding asset is valued in that asset. The other pool asset is priced by the prepared pool sample in force at each mark, so the valuation reads nothing outside the run's own dataset. The report then carries:

| Field                                             | Meaning                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `valuation`, `valuation_quote`, `valuation_asset` | `pool_price`, the funding asset's symbol (or its CAIP-19 identity when it has none) and its identity.                                                                                                                                                                                                                                                              |
| `equity_curve`                                    | Wallet balances of the pool assets plus each open position's principal and credited pending fees, at every action and on an even grid of at most 480 marks.                                                                                                                                                                                                        |
| `price_series`                                    | The other pool asset's price in the funding asset at the same marks.                                                                                                                                                                                                                                                                                               |
| `metrics`                                         | `total_return_pct`, `max_drawdown_pct` and `sharpe_ratio` (daily marks, annualized; absent below three days).                                                                                                                                                                                                                                                      |
| `net_quote_return`                                | The net result and return in the funding asset.                                                                                                                                                                                                                                                                                                                    |
| `performance`                                     | What produced the result: `fees_value` (fees at the pool price they were collected at), `swap_cost_value` and `swap_volume_value` (each swap against the pool price it executed on), `price_effect_value` (the rest: the price change of the inventory held, impermanent loss included), time in market and in range, holding benchmarks and one row per position. |
| `valuation_excludes`                              | Assets with a balance that the pools do not price, such as the gas asset. They stay in asset units.                                                                                                                                                                                                                                                                |

`fees_value - swap_cost_value + price_effect_value` equals the net result. Gas is not converted: an `excluded` cost policy leaves it out and a `fixed_native` policy reports it in the native asset under `gas_payers`. Allocations with different funding assets keep the asset-unit report, as do Uniswap V4, Curve and Aerodrome runs.

`boundary_evidence` names each pool's continuity proof by its dataset digest with the block interval and the number of liquidity events and inputs it covers. The proof itself stays in the prepared dataset.

# Example strategies

The repository contains executable references used for development and qualification, plus candidate strategies that are not yet promoted. The existence of a directory does not establish SDK support. Always confirm the connector, chain, action and run mode in [Supported connectors](https://sdk.docs.almanak.co/support.html) or with `almanak support`.

This page is a curated starting point, not a catalog of every strategy directory. An installed SDK also carries a read-only copy of every reference's author files, so the full catalog is available without a source checkout:

```
almanak support --references --connector uniswap_v3 --chain eip155:4663 --json
```

Each entry names the reference, its connector and CAIP-2 chain markets, the installed directory and the files in it: `strategy.py`, `config.yaml`, the backtest YAML files and the README where present. Without filters the command lists every reference. Copy a reference into your own directory before changing it; the listing never loads or runs a strategy and does not establish support.

## Recommended starting points

| Example                                                                                                                | Chain                  | Concepts demonstrated                                                                                    | Relative complexity |
| ---------------------------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------- | ------------------- |
| [Uniswap V4 trading](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v4_trading)                     | Ethereum               | Exact-input entry, timed hold, complete exit, typed memory and intermediate approvals                    | Basic               |
| [Uniswap V4 fixed-range LP](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v4_lp)                   | Ethereum               | Acquire both currencies, open an NFT position, collect, close, convert residuals and revoke authority    | Intermediate        |
| [Aave V4 WETH supply and borrowing](https://github.com/almanak-co/sdk-v3/tree/main/strategies/aave_v4_weth_supply)     | Ethereum               | Multiple named funds and positions, collateral, debt, repayment and independent withdrawal               | Intermediate        |
| [Aave V4 Avalanche borrowing](https://github.com/almanak-co/sdk-v3/tree/main/strategies/aave_v4_avalanche_borrowing)   | Avalanche              | Collateralized borrowing, native gas scope, repayment and collateral release                             | Intermediate        |
| [Aave V4 two-chain borrowing](https://github.com/almanak-co/sdk-v3/tree/main/strategies/aave_v4_multichain_borrowing)  | Ethereum and Avalanche | Explicit chain routing, independent work and healthy-chain progress during a peer outage                 | Advanced            |
| [Uniswap V4 Avalanche LP](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v4_avalanche_lp)           | Avalanche              | Fixed-range LP lifecycle with the funding asset in currency1                                             | Intermediate        |
| [Uniswap V3 Base RSI](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v3_base_rsi)                   | Base                   | Declared price series, RSI with TA-Lib, crossing detection in memory, holding on an incomplete series    | Intermediate        |
| [Uniswap V3 Base RSI-gated LP](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v3_base_rsi_lp)       | Base                   | An indicator gating a banded LP position: open while RSI ranges, close when it trends                    | Intermediate        |
| [Aave V3 Base SMA-gated borrowing](https://github.com/almanak-co/sdk-v3/tree/main/strategies/aave_v3_base_sma_borrow)  | Base                   | A lending strategy reading a pool series: borrow above a moving average, repay below it with a margin    | Intermediate        |
| [Uniswap V3 Base ATR breakout](https://github.com/almanak-co/sdk-v3/tree/main/strategies/uniswap_v3_base_atr_breakout) | Base                   | Full OHLCV candles (`PoolSwapCandles`): channel midline entry, ATR trailing stop derived from the series | Intermediate        |

Start with the Uniswap trading reference when learning the hook contract. It has one account, market and allocation, and its durable state is a single purchase timestamp. Use the Aave WETH reference for named lending positions and the two-chain reference only after the single-chain lifecycle is clear.

## What an example contains

A complete reference normally contains:

| File                     | Purpose                                                               |
| ------------------------ | --------------------------------------------------------------------- |
| `strategy.py`            | Sandboxed decision, capability, outcome and optional dashboard hooks  |
| `config.yaml`            | Author-owned economic choices and named chain scope                   |
| `backtest-economic.yaml` | Prepared economic model, historical window and hypothetical inventory |
| `backtest.yaml`          | Optional detailed historical-path model where supported               |
| `README.md`              | The lifecycle, commands, assumptions and important authority limits   |

Operator provider bindings, account addresses and fork fixtures live under [`examples/operators/`](https://github.com/almanak-co/sdk-v3/tree/main/examples/operators), not in the strategy directory. Generated deployments and resolved facts are runtime artifacts.

## Choosing an example

Choose by economic lifecycle before choosing by protocol name:

1. Decide whether the strategy lends, borrows, trades, provides liquidity or composes several of those actions.
1. Filter the support declaration by exact connector and CAIP-2 chain.
1. Confirm every required action, account profile and backtest model is declared.
1. Select the closest promoted reference and preserve its connector-owned action sequence while changing author policy.
1. Run the offline check before adding provider or execution inputs.

For example:

```
uv run --locked almanak support \
  --connector uniswap_v4 --chain eip155:1 --format text
uv run --locked almanak check strategies/uniswap_v4_trading --format text
```

An empty support result means the requested combination is not declared. A candidate README that says qualification or promotion is pending must not be used as evidence of support.

## Adapting safely

- Keep `strategy.py` and `config.yaml` as the only author-maintained runtime inputs.
- Preserve strict CAIP-2 and CAIP-19 identities. Do not replace them with chain numbers, token symbols or guessed addresses.
- Keep providers, secrets, wallet bindings and execution authority out of author files.
- Use public `ctx.actions` methods instead of copying protocol transaction construction from SDK internals.
- Treat the example's configuration as explicit sample choices, not connector defaults or recommended financial parameters.
- Retain teardown, failure and intermediate-outcome behavior when simplifying an example.
- Add new actions only when the support declaration and connector public API expose them.

Continue with [Strategy authoring](https://sdk.docs.almanak.co/authoring.html) for the hook contract, [Configuration](https://sdk.docs.almanak.co/configuration.html) for author inputs, and [Backtesting](https://sdk.docs.almanak.co/backtesting.html) for model and reproduction requirements.

# Supported connectors and chains

Connectors advertise immutable per-chain support for execution modes, economic actions, account profiles and historical coverage. A declaration does not establish provider availability, wallet funding or installed Safe authority.

| Connector              | `eip155:1`                 | `eip155:10`                | `eip155:137`               | `eip155:42161`             | `eip155:43114`             | `eip155:4663`              | `eip155:56`                | `eip155:8453`              |
| ---------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- |
| `aave_v3`              | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |                            | fork, production, backtest | fork, production, backtest |
| `aave_v4`              | fork, production, backtest |                            |                            |                            | fork, production, backtest |                            |                            |                            |
| `aerodrome`            |                            |                            |                            |                            |                            |                            |                            | fork, production, backtest |
| `aerodrome_slipstream` |                            |                            |                            |                            |                            |                            |                            | fork, production, backtest |
| `compound_v3`          | fork, production, backtest | fork, production, backtest |                            | fork, production, backtest |                            |                            |                            | fork, production, backtest |
| `curve`                | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |
| `gmx_v2`               |                            |                            |                            | fork, production, backtest | fork                       |                            |                            |                            |
| `morpho_blue`          | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |
| `pancakeswap_v3`       | fork, production, backtest |                            |                            | fork, production, backtest |                            |                            | fork, production, backtest | fork, production, backtest |
| `sparklend`            | fork, production, backtest |                            |                            |                            |                            |                            |                            |                            |
| `uniswap_v3`           | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |
| `uniswap_v4`           | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |

## aave_v3

| Chain          | Modes                      | Actions                                                                    | Account profiles | Restrictions                                                                                                                                                                                             | Backtest models                                     | Variants |
| -------------- | -------------------------- | -------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified Pool and reserve-token revision 11 implementations, ordinary collateral mode and variable-rate debt, configured Pool, reserve assets and oracle feeds; no default markets                       | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 L2Pool and reserve-token implementations, complete proxy fingerprints and explicitly configured oracle sources                                                                      | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 PoolInstance and reserve-token implementations, complete proxy fingerprints and explicitly configured oracle sources                                                                | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 L2Pool, aToken and variable-debt semantics, full runtime fingerprints retain documented proxy metadata differences, ordinary collateral mode and explicit configured oracle sources | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 PoolInstance and reserve-token implementations, complete proxy fingerprints and explicitly configured oracle sources                                                                | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 PoolInstance and reserve-token implementations, complete proxy fingerprints; reserve proxy metadata difference retained                                                             | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified Pool and reserve-token revision 11 implementations, ordinary collateral mode and variable-rate debt, pinned Jovian L1 and operator fee accounting                                               | `aave-v3-revision11-economic-indices-v1` (economic) | none     |

The `ctx.actions` methods for `aave_v3` and what each does are listed under "aave_v3 actions" in Strategy actions below.

## aave_v4

| Chain          | Modes                      | Actions                         | Account profiles | Restrictions                                                                    | Backtest models                                                                                 | Variants |
| -------------- | -------------------------- | ------------------------------- | ---------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, borrow, repay | eoa, safe_roles  | verified V4 implementation, local fund/Spoke collateral, standard ERC-20 assets | `aave-v4-supply-historical-shares-v1` (historical_path), `aave-v4-economic-rates-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | supply, withdraw, borrow, repay | eoa, safe_roles  | verified V4 implementation, local fund/Spoke collateral, standard ERC-20 assets | `aave-v4-economic-rates-v1` (economic)                                                          | none     |

The `ctx.actions` methods for `aave_v4` and what each does are listed under "aave_v4 actions" in Strategy actions below.

## aerodrome

| Chain         | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                             | Backtest models                                          | Variants         |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------- |
| `eip155:8453` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Aerodrome factory, clone, router and fee contracts, fungible pool shares and separate account-wide fee claims, one allocation per fund; no unrelated initial shares, fees or pool LP deposit, stable and volatile pools; no staking or reward authority, economic execution assumes access; approval and revocation mechanics excluded, pinned Jovian L1 and operator fee accounting | `aerodrome-economic-integer-fixed-indices-v1` (economic) | stable, volatile |

The `ctx.actions` methods for `aerodrome` and what each does are listed under "aerodrome actions" in Strategy actions below.

## aerodrome_slipstream

| Chain         | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                     | Backtest models                                          | Variants |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | -------- |
| `eip155:8453` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured compiled Slipstream factory, clone, periphery and fee modules, unstaked NFTs and undiscounted submitter fee context, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `slipstream-economic-integer-fixed-growth-v1` (economic) | none     |

The `ctx.actions` methods for `aerodrome_slipstream` and what each does are listed under "aerodrome_slipstream actions" in Strategy actions below.

## compound_v3

| Chain          | Modes                      | Actions                                                        | Account profiles | Restrictions                                                                                                                                                                                                                                                                                           | Backtest models                                       | Variants |
| -------------- | -------------------------- | -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks                                               | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks, pinned Jovian L1 and operator fee accounting | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks                                               | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks, pinned Jovian L1 and operator fee accounting | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |

The `ctx.actions` methods for `compound_v3` and what each does are listed under "compound_v3 actions" in Strategy actions below.

## curve

| Chain          | Modes                      | Actions                                                                                                                   | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                             | Backtest models                                                                                                      | Variants                                       |
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng      |
| `eip155:10`    | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution, pinned Jovian L1 and operator fee accounting | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng_2024 |
| `eip155:137`   | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng_2024 |
| `eip155:42161` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng      |
| `eip155:43114` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic)                                                        | stableswap_ng                                  |
| `eip155:4663`  | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng_2024 |
| `eip155:56`    | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng                    |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution, pinned Jovian L1 and operator fee accounting | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng      |

The `ctx.actions` methods for `curve` and what each does are listed under "curve actions" in Strategy actions below.

## gmx_v2

| Chain          | Modes                      | Actions                                                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                      | Backtest models                                         | Variants |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------- |
| `eip155:42161` | fork, production, backtest | increase_position, decrease_position, close_position, revoke_position, swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, revoke_exchange | eoa, safe_roles  | Verified market and compiled deployment closure; wrapped-native collateral., Solvent liquidation reconciles as an external effect; native insolvency is not advertised (16 September 2026 disposition)., Dedicated Safe with accepted broad cancellation grants and retained-key checks.                                                          | `gmx-v2-published-opportunity-integer-gm-v1` (economic) | none     |
| `eip155:43114` | fork                       | increase_position, decrease_position, close_position, revoke_position, swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, revoke_exchange | eoa, safe_roles  | Candidate identified-fork request lifecycle; not promoted SDK support., Verified market and compiled deployment closure; wrapped-native collateral., Position and swap candidate implementation; GM liquidity candidate implementation; offline models unproven., Dedicated Safe with accepted broad cancellation grants and retained-key checks. | none                                                    | none     |

The `ctx.actions` methods for `gmx_v2` and what each does are listed under "gmx_v2 actions" in Strategy actions below.

## morpho_blue

| Chain          | Modes                      | Actions                                                        | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                         | Backtest models                                     | Variants |
| -------------- | -------------------------- | -------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks, pinned Jovian L1 and operator fee accounting | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit ERC-4626 conversion oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                     | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:4663`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks, pinned Jovian L1 and operator fee accounting | `morpho-blue-archive-anchored-market-v1` (economic) | none     |

The `ctx.actions` methods for `morpho_blue` and what each does are listed under "morpho_blue actions" in Strategy actions below.

## pancakeswap_v3

| Chain          | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                                    | Backtest models                                              | Variants |
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |

The `ctx.actions` methods for `pancakeswap_v3` and what each does are listed under "pancakeswap_v3 actions" in Strategy actions below.

## sparklend

| Chain      | Modes                      | Actions                                                                    | Account profiles | Restrictions                                                                                                                                                                                                  | Backtest models                                      | Variants |
| ---------- | -------------------------- | -------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -------- |
| `eip155:1` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | Ethereum Pool revision 4 and token revision 1, ordinary collateral mode and variable-rate debt, isolated and siloed reserves remain unsupported, configured Pool, assets and oracle feeds; no default markets | `sparklend-revision4-economic-indices-v1` (economic) | none     |

The `ctx.actions` methods for `sparklend` and what each does are listed under "sparklend actions" in Strategy actions below.

## uniswap_v3

| Chain          | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                     | Backtest models                                          | Variants |
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:4663`  | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |

The `ctx.actions` methods for `uniswap_v3` and what each does are listed under "uniswap_v3 actions" in Strategy actions below.

## uniswap_v4

| Chain          | Modes                      | Actions                                                                                                   | Account profiles | Restrictions                                                                                                                                                                                                                                                                                           | Backtest models                                                                                                        | Variants |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only), pinned Jovian L1 and operator fee accounting | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-economic-integer-fixed-growth-v1` (economic)                                                               | none     |
| `eip155:42161` | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:4663`  | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-economic-integer-fixed-growth-v1` (economic)                                                               | none     |
| `eip155:56`    | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-economic-integer-fixed-growth-v1` (economic)                                                               | none     |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading, single pool, fixed LP range or recentring band (bands backtest in economic mode only), pinned Jovian L1 and operator fee accounting | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |

The `ctx.actions` methods for `uniswap_v4` and what each does are listed under "uniswap_v4 actions" in Strategy actions below.

## Strategy actions

Methods a strategy calls on `ctx.actions` for each connector, read from the binding the connector installs. The first argument is the configured allocation name. Each entry gives the signature and what the action does: for example that an `exit` teardown closes liquidity, converts residual inventory to the funding asset and revokes allowances, how `swap` picks its quote, and `repay` versus `repay_amount`.

### aave_v3 actions

Strategy actions on `ctx.actions` for the `aave_v3` connector: `borrow`, `repay`, `repay_amount`, `set_collateral`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` base units of the reserve asset; needs `borrow` authority.
- `repay(name: str)`: Repay the whole outstanding debt, including accrued interest.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `set_collateral(name: str, enabled: bool)`: Enable or disable the supplied balance as collateral. Disabling with debt outstanding must keep the minimum health factor.
- `supply(name: str, amount: int)`: Supply `amount` base units of the reserve asset from fund inventory. A first supply into a reserve with an LTV enables collateral; needs `collateral: true`.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### aave_v4 actions

Strategy actions on `ctx.actions` for the `aave_v4` connector: `borrow`, `repay`, `supply`, `withdraw`.

- `borrow(name: str, amount: int)`: Borrow `amount` base units on a debt allocation. Borrowing is backed by the debt allocation's named collateral allocations.
- `repay(name: str)`: Repay the whole outstanding debt, including accrued interest.
- `supply(name: str, amount: int)`: Supply `amount` base units of the reserve asset from fund inventory.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.

### aerodrome actions

Strategy actions on `ctx.actions` for the `aerodrome` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### aerodrome_slipstream actions

Strategy actions on `ctx.actions` for the `aerodrome_slipstream` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### composed actions

Strategy actions on `ctx.actions` for the `composed` connector: `borrow`, `close_liquidity`, `collect`, `component`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `repay`, `repay_amount`, `revoke`, `set_collateral`, `supply`, `swap`, `withdraw`, `withdraw_amount`.

- `borrow(name, amount)`: Route `borrow` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `close_liquidity(name)`: Route `close_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve and uniswap_v4. GMX takes minimums: call it through `component(name)`.
- `collect(name)`: Route `collect` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and uniswap_v4.
- `component(name)`: The `ctx.actions` of the component connector that owns allocation `name`. Call it for anything a wrapper here does not forward, e.g. `ctx.actions.component(name).open_liquidity(name)` on uniswap_v4, or GMX `increase`.
- `decrease_liquidity(name, liquidity)`: Route `decrease_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and curve. GMX takes `shares` and minimums: call it through `component(name)`.
- `exit(name)`: Route `exit` (full teardown) to the component that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve, uniswap_v4 and gmx_v2; lending components have none. Returns None when that component has nothing left to exit.
- `increase_liquidity(name, *, fraction_bps=10000)`: Route `increase_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and curve. GMX takes maximums: call it through `component(name)`; uniswap_v4 has none.
- `open_liquidity(name, *, fraction_bps=10000)`: Route `open_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and curve. uniswap_v4 takes no `fraction_bps` and GMX takes maximums: call them through `component(name)`.
- `repay(name)`: Route `repay` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `repay_amount(name, amount)`: Route `repay_amount` to the component connector that owns allocation `name`. Lending components: aave_v3, sparklend, compound_v3, morpho_blue; aave_v4 has none.
- `revoke(name)`: Route `revoke` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve and gmx_v2.
- `set_collateral(name, enabled)`: Route `set_collateral` to the component connector that owns allocation `name`. Components: aave_v3 and sparklend.
- `supply(name, amount)`: Route `supply` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `swap(name, *, exit=False)`: Route `swap` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve and uniswap_v4, forwarding only `exit`; call `component(name)` for `rebalance=True` and GMX swaps.
- `withdraw(name)`: Route `withdraw` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `withdraw_amount(name, amount)`: Route `withdraw_amount` to the component connector that owns allocation `name`. Lending components: aave_v3, sparklend, compound_v3, morpho_blue; aave_v4 has none.

### compound_v3 actions

Strategy actions on `ctx.actions` for the `compound_v3` connector: `borrow`, `repay`, `repay_amount`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` of the Comet base asset against collateral in the same Comet. Needs `borrow` authority on the base allocation.
- `repay(name: str)`: Repay the whole outstanding debt, including accrued interest.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `supply(name: str, amount: int)`: Supply `amount` base units of the allocation's asset to its Comet. The base asset earns interest; a collateral asset backs borrowing of the base asset.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### curve actions

Strategy actions on `ctx.actions` for the `curve` connector: `close_liquidity`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Burn all owned LP shares with slippage-bounded minimums. Withdraws in `single_coin_exit` when configured, otherwise proportionally.
- `decrease_liquidity(name, liquidity)`: Burn `liquidity` LP shares, keeping some owned, with slippage-bounded minimums. Withdraws in `single_coin_exit` when configured, otherwise proportionally.
- `exit(name)`: Full exit/teardown: burn owned LP shares, swap non-funding coins to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Deposit up to `fraction_bps` of owned inventory into the owned LP shares.
- `open_liquidity(name, *, fraction_bps=10000)`: Deposit up to `fraction_bps` of owned inventory of every pool coin for LP shares. Refuses when shares are already owned; use `increase_liquidity` then.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False)`: Swap along `entry_quote`, or the first of `exit_quotes` with `exit=True`. The minimum output is the quote less `slippage_bps`; raises when that quote is absent.

### gmx_v2 actions

Strategy actions on `ctx.actions` for the `gmx_v2` connector: `close`, `close_liquidity`, `decrease`, `decrease_liquidity`, `exit`, `increase`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close(name)`: Close the whole perpetual position through a keeper order.
- `close_liquidity(name, *, minimum0: AssetAmount, minimum1: AssetAmount)`: Redeem all owned GM market tokens for at least `minimum0`/`minimum1`.
- `decrease(name, *, collateral_base_units, size_delta_usd_e30)`: Shrink the perpetual position by `size_delta_usd_e30` and withdraw collateral. `collateral_base_units` of the collateral asset are withdrawn alongside the size change.
- `decrease_liquidity(name, *, shares: AssetAmount, minimum0: AssetAmount, minimum1: AssetAmount)`: Redeem `shares` GM market tokens for at least `minimum0`/`minimum1`.
- `exit(name)`: Next exit/teardown step, or None when nothing remains; call it every decision. Perpetuals close then revoke; swap allocations swap non-funding inventory to the funding asset at `exit_minimum_output`; GM liquidity redeems at `withdrawal_minimums`.
- `increase(name, *, collateral_base_units, size_delta_usd_e30)`: Open or grow the perpetual position by collateral and USD size. `collateral_base_units` is in the collateral asset; `size_delta_usd_e30` has 30 decimals.
- `increase_liquidity(name, *, maximum0: AssetAmount, maximum1: AssetAmount)`: Deposit up to `maximum0`/`maximum1` more into the owned GM tokens.
- `open_liquidity(name, *, maximum0: AssetAmount, maximum1: AssetAmount)`: Deposit up to `maximum0`/`maximum1` of the market currencies for GM tokens.
- `revoke(name)`: Zero the allocation's remaining allowances (exchange or position authority).
- `swap(name, *, amount: AssetAmount, minimum_output: AssetAmount)`: Request an asynchronous keeper swap of `amount` with an explicit `minimum_output`.

### morpho_blue actions

Strategy actions on `ctx.actions` for the `morpho_blue` connector: `borrow`, `repay`, `repay_amount`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` of the loan asset against collateral in the same market. Needs `borrow` authority on the loan allocation.
- `repay(name: str)`: Repay the whole outstanding debt, including accrued interest.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `supply(name: str, amount: int)`: Supply `amount` base units of the allocation's asset to its market. A loan allocation lends the loan asset; a collateral allocation posts collateral.
- `withdraw(name: str)`: Withdraw everything: all supplied loan shares or all posted collateral.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### pancakeswap_v3 actions

Strategy actions on `ctx.actions` for the `pancakeswap_v3` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### sparklend actions

Strategy actions on `ctx.actions` for the `sparklend` connector: `borrow`, `repay`, `repay_amount`, `set_collateral`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` base units of the reserve asset; needs `borrow` authority.
- `repay(name: str)`: Repay the whole outstanding debt, including accrued interest.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `set_collateral(name: str, enabled: bool)`: Enable or disable the supplied balance as collateral. Disabling with debt outstanding must keep the minimum health factor.
- `supply(name: str, amount: int)`: Supply `amount` base units of the reserve asset from fund inventory. A first supply into a reserve with an LTV enables collateral; needs `collateral: true`.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### uniswap_v3 actions

Strategy actions on `ctx.actions` for the `uniswap_v3` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### uniswap_v4 actions

Strategy actions on `ctx.actions` for the `uniswap_v4` connector: `close_liquidity`, `collect`, `exit`, `open_liquidity`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory. Minimums are the observed principal less the allocation's `slippage_bps`.
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `open_liquidity(name)`: Open a position using all owned inventory of both currencies as maximums. A banded allocation centres the range on the observed tick; a fixed one uses its range.
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

Generated from `almanak support --json` and the connector action bindings; do not edit by hand.
# Operate

# Operating retained workspaces

Execution requires `run --fork` or `run --production` with an explicit operator file (`--gateway-config`) and the private provider env file (`--env-file`). Credentials never select production implicitly. Each private workspace captures the authored inputs, source, the generated read context, resolved facts, setup stages, endpoint references, process identities and logs. `state.sqlite` contains authoritative execution facts; `workspace.json` contains supervision. Keep credentials outside strategy-readable paths.

## Operator configuration

The operator file is YAML or JSON with `schema_version: operator-gateway-config-v1`. It selects the enabled `modes`, one provider per chain under `networks` (`alchemy` with `api_key_env`, or `rpc_url` with `url_env`; the env file holds the values), named vendor access under `vendors` when a backtest requests one, the named `accounts.bindings` the author file requires, and mode authority: `fork` holds the fixture anchors, funds and operators; `production` holds execution networks, independent payer ceilings and signing file references. When the author requests gas policy, `fork.gas` also supplies explicit operator ceilings, grouped by the fixture's actual payer relationships. A shared operator requires one compatible policy and ceiling for all accounts it submits for. The [fork](https://github.com/almanak-co/sdk-v3/blob/main/examples/operators/aave_v4_weth_supply.fork.yaml), [production](https://github.com/almanak-co/sdk-v3/blob/main/examples/operators/aave_v4_weth_supply.production.yaml) and [backtest](https://github.com/almanak-co/sdk-v3/blob/main/examples/operators/backtest/eip155-1.yaml) examples cover the Aave reference. An operator file never carries provider keys.

A run first binds the authored choices to the operator file and writes the read context, then starts a read-only gateway that resolves the named markets at the selected anchors and retains the verified facts. Fork runs read the anchors from the operator fixture; production runs pin fresh anchors. Hooks run once on the resolved parameters and the results are frozen before any execution authority exists. `check --online` performs the same resolution and hook stages in a temporary directory and reports a preview of the resolved facts without creating a workspace.

| Command                                         | Effect                                                              |
| ----------------------------------------------- | ------------------------------------------------------------------- |
| `status --run PATH`                             | Retained financial state and current local process availability.    |
| `pause --run PATH`                              | Wait for coordinator pause; leave services and positions available. |
| `resume --run PATH`                             | Explicit recovery through captured inputs and retained execution.   |
| `teardown --run PATH`                           | Persist closure intent and start valid cleanup.                     |
| `shutdown --run PATH`                           | Pause then stop services after closure.                             |
| `shutdown --run PATH --force`                   | Stop despite unresolved obligations and preserve evidence.          |
| `report --run PATH --format html --output FILE` | Offline shared report and optional custom hook.                     |

`resume` returns once the supervisor accepts it. Admission, the gateway start and the coordinator start then run in the background: status reports `provisioning: true` until they finish and then the resulting stage, such as `running`, `action_required` or `failed` with its diagnostic. Other commands except `pause` are refused meanwhile.

Production admission reports every gap in one diagnostic. When funding is missing and Safe permissions are not installed, the stage is `action_required` with both reasons, and the owner transactions are already exported under `private/deployment/permissions/`.

Ctrl-C on a foreground run requests pause and waits for acknowledgement. Pending work and open positions remain recorded. A failed coordinator is not restarted automatically. Its diagnostic is the refusal the coordinator reported, such as an `inventory` shortfall or a `gas_limit` or `gas_budget` refusal naming the requested amount, the estimate or quote and the configured limit, and status keeps reporting it after the supervisor exits. A coordinator that stopped without a reported refusal leaves the generic instruction to inspect its log. A market fee quote above `max_gas_price_wei` (`gas_price`) or `max_transaction_fee` (`gas_fee_quote`) does not stop a single-chain coordinator: it signs nothing, keeps its decision and quotes again with a back-off of up to a minute, for as long as the market stays above the ceiling. While it holds, status stays `running` and the diagnostic names the kind and the policy field; the coordinator log has a `gas_ceiling_hold` line per attempt with the quote, then `gas_ceiling_recovered` once a plan is admitted or `gas_ceiling_dropped` when the decision is dropped. On a multi-chain deployment the refusal is the chain's retained fault and the chain is retried on each pass. `almanak test run` stops on such a hold at once. Explicit resume preserves pending identities and performs the existing lease, signer-claim and receipt audits. Never remove a journal to clear a pending transaction.

A strategy that declares [price series](https://sdk.docs.almanak.co/authoring.html#price-series-and-technical-analysis) needs no additional service or credential: the gateway reads each series from the chain reader of the series' chain, which the deployment must route. Provisioning refuses a series on a chain the deployment has no provider for, or whose connector has no reviewed deployment there; at launch the gateway resolves each declared pool and refuses to serve one that does not resolve or does not hold the declared asset. Before each decision the gateway reads the series as of the earlier of the decision time and the observed block time, and returns only the series the frozen preparation declared. Boundary blocks, points and their evidence are retained in `strategy-prices.sqlite` beside the journal, so a decision reads only boundaries it has not seen, and each served series, holds included, is appended there. A fork reads history before its fork block through its node, which forwards those reads to the upstream provider. Single strategy prices are a backtest-only Aave V4 input and are not served. A failed read at a decision records a hold for that decision; teardown decisions read no series.

The gateway holds the journal's write lock only to commit a request. Provider reads and remote signing run against the checkpoint read under the current lease; the commit re-checks the lease and refuses a recording whose checkpoint changed meanwhile, and a broadcast re-checks its envelope, hash and nonce and sends only under the lock. Renewals therefore never wait behind a slow plan or signature. Replacement families keep the lock for their whole request. Each `gateway_request` log line reports provider calls by method (`rpc_calls`, `rpc`), HTTP exchanges with the provider (`rpc_round_trips`, a batch counting once), provider, signer and lock time (`rpc_ms`, `signer_ms`, `lock_wait_ms`, `lock_held_ms`) alongside `duration_ms`.

Fork fixtures fund each declared asset with simulated capital. The chain's wrapped native asset is funded by wrapping and transfer, and nothing else is tried. Every other ERC-20 goes through an ordered ladder: a reviewed token-specific funder, a transfer from a listed holder, a known balance slot or seed recipe, `anvil_dealERC20`, then a probe of common balance slots unless the token has a seed recipe. A token with a reviewed funder or a listed holder never falls through to the generic routes; a holder short of the amount at the fork head (after any earlier fixture transfers) is refused. Funding refuses a token address with no contract code. Each ladder route must leave the exact balance and pass a transfer check: the account sends part of it to an address derived from the account and token, which must start empty and receive exactly that amount, and the fork reverts the check. The storage routes (`known_slot`, `seed_recipe`, `deal_erc20`, `probed_slot`) write the balance without minting, so `totalSupply` is left unchanged; such a route is refused when `totalSupply` is below the funded amount. A failed route is rolled back before the next one runs. When no route passes, the fixture refuses and names each outcome; nothing is partially funded. If `anvil_dealERC20` is still running after 20 s, the node keeps tracing in the background, so funding stops without rolling back and the fork must be recreated. In the compiled fixture plan every non-wrapped ERC-20 has `method: local_storage`, which means "any ERC-20 route", not a storage write. The `simulated_initial_capital` rows in `fixture-installation.json` record the route actually used in `method` (`wrap_native`, `reviewed`, `holder_transfer`, `known_slot`, `seed_recipe`, `deal_erc20` or `probed_slot`) with its `slot`, `seed` or `holder`, so a fork pass that relied on unreviewed funding is visible.

Fork resume verifies the original process and Anvil instance, chain and origin. A stopped fork cannot be recreated as the same deployment. Interrupted fixture funding reverts only through the retained snapshot token of the original node; an uncertain rollback, a lost node, or execution state without its preparation is refused. A completed fixture is reused and never rolled back. Production provisioning writes reviewable artifacts and performs no owner actions. Multi-host supervision is unsupported.

## Fork tests

`almanak test config` writes a fork operator file from the author configuration, with the shape of the shipped `examples/operators/*.fork.yaml`:

```
uv run --locked almanak test config ./my-strategy --env-file /private/providers.env \
  --output /private/operator/my-strategy.fork.yaml [--block N] [--force]
```

It names one Alchemy provider per authored chain through `ALCHEMY_API_KEY`, placeholder account bindings, anchors at the latest block (or `--block`, for a single-chain strategy; a chain with a GMX market anchors at least three hours behind the head, so the authentic keeper reports a fork test replays exist for its requests), Safe/Roles fixture funds sized from an online fork check's `funding` block plus 1%, one fixture operator per chain and gas policy holding at least one native unit and twice its entry minimum plus gas budget, and `fork.gas` ceilings equal to the authored `strategy.gas`. On Base and Optimism, where forks cannot bound the L1 data and operator fees and authored ceilings are refused, the file sets `fork.author_gas: unenforced` and carries a note: the fork runs gasless and the authored policy is retained but not enforced. An operator file without that choice keeps the refusal. An existing `--output` is replaced only with `--force`.

`almanak test run` runs one supervised fork lifecycle through the same workspace `run --fork` creates:

```
uv run --locked almanak test run ./my-strategy \
  --gateway-config /private/operator/my-strategy.fork.yaml \
  --env-file /private/providers.env --output /private/runs/test-1 \
  [--decisions 3] [--advance SECONDS] [--through-exit] [--json]
```

It waits for admission, then lets up to `--decisions` decisions settle. Between them it advances every fork node's time by the decision interval (or `--advance`, raised to the next due decision when shorter, so it never waits on the wall clock) with `evm_setNextBlockTimestamp` and `evm_mine`, and moves the coordinator's decision clock by the same offset; the author's interval is never edited. It stops early once an intent that increases exposure has executed, unless `--through-exit` keeps it deciding up to `--decisions`, so the strategy's own later intents run: the mint after an LP entry swap, fee collection, a recentre, its own close or sell. Size `--advance` so those fall within the decisions, e.g. past a hold time. After the entry, `--idle` consecutive holds (default 3) end the decisions early: a short run of holds is a strategy waiting on its own timer, a longer one means it has nothing left to do. It then requests teardown, waits until the deployment is closed, shuts the services down and releases the run's fund claims. Interrupts and failures still stop every recorded service.

A fork runs no protocol keeper, so a GMX order, deposit or withdrawal would stay pending. While the run waits on an accepted GMX request it settles it the way a keeper would, with nothing synthesized: it reads a keeper execution mined on the origin chain after the request was created, impersonates that keeper (which already holds the role at the fork origin) and submits its unchanged signed Chainlink Data Streams payloads to the fork's handler, at the reports' time. GMX's contracts then verify the signatures, report times, acceptable price and triggers. A request with no eligible report within its expiration stops the lifecycle with that reason; the source search is retained under `keepers/` in the workspace.

Keepers publish when somebody's request is pending, so on a quiet market they can be minutes apart, longer than GMX's 300-second request expiration. Because the fork runs behind the chain head, the run knows the coming publications and times the fork by them: `test config` starts a single-market GMX fork where usable publications stay under four minutes apart for fifteen minutes after admission, and each later advance (the `--advance` step, the start of teardown and its stall advances) is lengthened when needed so the decision lands 120 to 280 seconds before a usable publication: its request follows the decision's approvals, and its reports must come after the request and within GMX's 300-second expiration of it. This only chooses when the fork acts; the strategy's interval and every report are unchanged. The verdict is printed and retained as `test-verdict.json` in the new workspace; an existing `--output` is refused and keeps its earlier verdict:

| Field               | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema`            | `almanak-test-verdict-v1`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `workspace`         | The fork workspace directory, or null when the inputs were refused                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `admission`         | `ok`, and the `refusal` (an input error document, or the failed stage and its diagnostic, with the supervisor's failure kind as `error` when it recorded one), classified like a failure                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `decisions`         | Per decision: `time`, `result` (`hold` or `intent`) and the `intents` kinds; `settled: false` marks a decision the lifecycle stopped on before its work settled. From the `decision_evaluated` line the test run's coordinator logs (only `almanak test run` opts its coordinator in; production and hosted deployments never log it): `evaluated` (false when an unavailable price series held the decision before the strategy ran, with `price_error`), `observed` allocations, `price_series` (per declared series: `asset`, `complete`, `points`, `window`, `latest_at`, `age_seconds`; pool-sourced series also carry `timeframe`, `gaps` and `max_age_seconds`) and `availability` (chains not ready, with their fault). On a multi-chain deployment `chain` names the chain whose tick evaluated the decision; a row is given facts only when they are unambiguously its own. These are facts, not the strategy's reason for a hold |
| `transactions`      | Per executed transaction: `action`, `allocation`, `status`, `gas_used`, `via_roles`, `transaction`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `entered`, `closed` | Whether an entry executed and the deployment closed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `evidence_clean`    | The final journal holds no pending or reserved attempt, undelivered outcome, accounting issue, integrity error or chain fault                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `executed`          | Intent kinds that executed completely, in first-execution order; teardown's are not included                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `after_entry`       | The `executed` kinds that ran after the first entry: what `--through-exit` exercised beyond it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `keepers`           | Per request a fork keeper settled: its `request` key, `kind`, `chain`, the replayed `keeper` and its `source_transaction` and `source_block` on the origin chain, the `report_times`, the fork `transaction` and its `status` (`mined` or `reverted`, with the `revert` reason), and the fork time it `advanced_seconds`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `failures`          | Stage and message of anything that stopped the lifecycle early, each with its `kind`, `class` and `transient`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `failure_class`     | The `class` of the admission refusal, else of the first failure; null when nothing failed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `passed`            | Admission passed, the deployment closed, the evidence is clean and nothing failed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `duration_seconds`  | Wall time of the whole command                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

The exit code is 0 exactly when `passed` is true.

Each refusal and failure carries a structured `kind`, taken from its source rather than its message: the error kind of a refused input, the supervisor's failure kind, the error kind of the refusal a stopped coordinator logged, the kind of a fork keeper's data error, or else the stage or condition that ended the lifecycle (`decision_timeout`, `admission_timeout`, `coordinator_stopped`, `keeper`, …). Its `class` is `infra` for the kinds whose cause lies outside the strategy (`rpc`, `http`, `transport`, `unavailable`, `gateway_unavailable`, `timeout`, `rate_limit`, `fork_unavailable`, `unsupported_on_fork` and `admission_timeout`) and `strategy` for every other kind, including one not listed, and for a contract revert, which keeps the `rpc` kind but is marked `reverted` by the gateway. `transient` is true only for an `infra` failure its source marked transient, one the same run can pass unchanged; `unsupported_on_fork` never is. A caller can therefore re-run once only when `failure_class` is `infra` and that failure is `transient`, without reading the message.

A local fork can only execute EVM bytecode. Contracts a chain's node implements natively, such as Base's B20 stock tokens whose code is the single byte `0xef`, resolve on mainnet but fail every call on the fork. `check --fork --online` refuses them in its `fork_compatibility` stage with code `unsupported_on_fork`. Fork setup checks the fork nodes before any funding, and again when it reuses a funded fixture, and stops `failed` with the failure `unsupported_on_fork`, naming every such contract. No retry or resume can pass it, so status offers `shutdown` instead of `resume`, `almanak run` exits 1 and the verdict's refusal carries `"error": "unsupported_on_fork"`. This is a limitation of local fork simulation, not of the strategy, which can still be exported and deployed.

A fork node starts at the fork block's own base fee without Anvil's 1 gwei minimum tip, so its gas quote follows the forked chain's market and a mainnet-calibrated `strategy.gas` fits. On chains whose fee history reports no tips (BNB), the node starts at the upstream gas price instead.

## Production

```
uv run --locked almanak wallet create --account treasury --output /private/operator
uv run --locked almanak check ./my-strategy --production --online \
  --gateway-config /private/operator/production.yaml --env-file /private/providers.env
uv run --locked almanak run ./my-strategy --production \
  --gateway-config /private/operator/production.yaml \
  --env-file /private/providers.env --output /private/runs/live
```

For Safe/Roles accounts the production online check also reports a `permissions` block: per Safe, whether its authority is installed and, when it is not, the Safe Transaction Builder batch the owner executes. It is generated exactly as admission generates it, so the owner can install permissions before the run exists. Admission still generates and verifies the installed authority.

The author file carries the economic gas policy under `strategy.gas`. Resolution retains this policy in every mode. Identified forks enforce the same economic bounds through explicit operator `fork.gas` ceilings and generated fixture payers. Missing or incompatible ceilings are refused before fixture setup. Fork runs on Base and Optimism refuse authored gas caps because their L1 data and operator fees have no verified bound on a fork; gasless forks are unaffected. Production runs on those chains accept authored caps: the gateway bounds the L1 data and operator fees at quote time within the payer ceilings and reconciles the exact charges from each receipt. Fee-only replacement remains opt-in and limited to its supported Ethereum execution path. The operator file carries the production authority: execution networks, independent payer ceilings that the authored policy must fit, the funded account bindings and the signing references. The online check verifies policy compatibility through the read-only gateway and never opens signing files. EOA funds are their own submitters. Safe funds use delegated Roles operators; owners remain distinct and are never a fallback signer.

Encrypted keystores must match the named submitters. Interactive passwords are passed ephemerally and are not saved in manifests. An explicit private `password_file` in the operator signing section is an operator choice. A gateway restart requests a password when required. Credentials must be operator-owned private files outside sandbox readable paths.

A hosted operator may replace `--key-file` with `--signer-file`, a private JSON binding whose `kind` names an installed signer plugin (entry-point group `almanak.signers`). The gateway then holds no key material: every envelope a remote signer returns is decoded and checked against the requested transaction and the bound submitter before it is broadcast or retained. The two flags are exclusive and a remote signer takes no keystore password.

Missing Safe permissions are exported for owner review and installation. The supervisor reports action required and does not install them. The Safe must be a canonical Safe 1.4.1 (L1 or SafeL2) whose only module is its Roles 2.1.0 proxy, with no guard and either no fallback handler or Safe's CompatibilityFallbackHandler 1.4.1. When the operator's role was assigned in the Roles setup transaction, the exported bundle does not assign it again. After owner action, explicit resume verifies installed authority before admitting execution. Installed authority is read from Roles storage at a pinned block: every grant the strategy needs must be installed exactly, with the operator in the role and no default role. Grants beyond the strategy's own, other members of its role and other roles of the operator are not detected; Safes are dedicated to one strategy. The authority check before every plan, signature and broadcast replays the previous check's block-pinned reads in batches at its own observation block. Production advances on canonical mined receipts and audits recorded canonicality before further work. Invalidated history halts with pending evidence retained.

## Reporting and advanced commands

Text is the default on terminals; redirected output is JSON. Override with `--format text`, `--format json` or `--json`. Offline reports never obtain credentials or contact a provider. They distinguish retained status from a fresh online audit and preserve fund/payer scope, unavailable values and simulation assumptions. Custom dashboard failure remains visible without hiding the report.

Explicit process management remains under `service gateway`, `service fork`, `service start`, `service resume`, `service status` and `service teardown`. `permissions generate|verify` uses an authenticated gateway.

### Protocol observations

`almanak ax` reads one protocol observation at one pinned block. Every subcommand takes `--block` (default `finalized`), one transport and its own request arguments, all after the subcommand:

| Subcommand         | `--connector`                                                                | Request                                                                                                                                 |
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `pool`             | `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome`, `curve` | `--chain` and `--pool`, or `--config`; optional `--depth-bps` for the first three                                                       |
| `pool-price`       | `uniswap_v4`                                                                 | `--chain`, `--currency0`, `--currency1`, `--fee`, `--tick-spacing` and optional `--base`, or `--config`                                 |
| `lending-reserve`  | `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`              | `--chain` and the connector's selector flags, or `--config`                                                                             |
| `lending-position` | same as `lending-reserve`                                                    | the same selection, plus `--account`                                                                                                    |
| `perp-position`    | `gmx_v2`                                                                     | `--config`, `--account`                                                                                                                 |
| `perp-markets`     | `gmx_v2`                                                                     | `--chain` and `--query`, or `--config`                                                                                                  |
| `balance`          | none                                                                         | `--asset` (CAIP-19 asset type), `--account` (repeatable)                                                                                |
| `resolve`          | none                                                                         | `--chain`, `--query` (symbol or address); optional `--limit`                                                                            |
| `dex-pools`        | none                                                                         | `--chain`, `--token`; optional `--quote`, `--connector`, `--limit`, `--offset`, `--depth-bps`                                           |
| `lending-markets`  | `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`              | `--chain`; optional `--asset` (required for `morpho_blue`), `--collateral`, `--include-unlisted` (`morpho_blue`), `--limit`, `--offset` |

`--config` names a JSON request file whose fields are the connector's request model; for the lending subcommands it is the market selection an author writes under `markets:`. `--block` accepts a decimal block number, `finalized` or `latest`; any other value is refused before a transport opens. The reader first resolves the selector to one header, and every later read of the observation names that header's hash with `requireCanonical`, so the output's `block` records the concrete number and hash all values came from. A `latest` observation can be reorganized away; a later read against a block that is no longer canonical fails instead of mixing blocks.

Each observation reads through exactly one transport: `--env-file` resolves the provider credentials in the calling process, or `--gateway-url` with `--gateway-auth-file` reads through `service read-gateway`, a read-only observation gateway that keeps the credentials in its own process and serves only whitelisted JSON-RPC reads and the discovery lookups below over authenticated loopback. Hosted sessions that must not receive a provider key use the gateway transport. Without either flag, `ax` reads through the gateway named by `ALMANAK_AX_GATEWAY_URL` and `ALMANAK_AX_GATEWAY_AUTH_FILE`, which the hosted worker sets; a flag always wins, and with neither the command is refused. The inline `pool-price` key names a pool on the reviewed deployment for the chain, hookless unless `--hooks` names a reviewed hook (whose dynamic-fee profiles take `--fee 8388608`). Use each command's `--help` for the complete argument list.

`ax lending-reserve` resolves a lending market exactly as the author loader does and reports the price sources the protocol uses at the pinned block, with a paste-ready `config` block:

```
almanak ax lending-reserve --connector aave_v3 --chain eip155:1 \
  --asset eip155:1/erc20:0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 \
  --block finalized --env-file .env
```

Selector flags use the author field names: `--asset` and optional `--pool` for Aave V3 and SparkLend, `--spoke` and `--reserve-id` for Aave V4, `--comet` and `--asset` for Compound V3, `--market-id`, `--leg` and optional `--core` for Morpho Blue. The result carries `resolution`, `price_sources`, `config` and the reserve `observation`. For Aave V3 and SparkLend, `config.allocation` holds the `oracle` and `price_source` an allocation on that market states. For Aave V4, `config.debt` holds the `oracle` and `debt_price_source` of a debt on that reserve, and `config.collateral_price_source` is the entry for that reserve in a debt's `collateral_price_sources`. Compound V3 and Morpho Blue resolve their feeds and oracle themselves, so their `config.market` adds them only as optional assertions. A deployment the connector has not reviewed is refused. A reserve state read the protocol refuses at that block leaves `observation` null with `observation_error`, and the price sources are still reported. `ax balance` reads every repeated `--account` at the same pinned block.

`ax pool` reads a Uniswap V3, PancakeSwap V3 or Aerodrome Slipstream pool from only its chain and address, given inline or as a `{"chain", "pool"}` file. Both forms validate as the same request:

```
almanak ax pool --connector uniswap_v3 --chain eip155:4663 --pool 0x... \
  --block latest --depth-bps 200 --gateway-url "$URL" --gateway-auth-file "$TOKEN_FILE"
```

The connector's resolver reads the pair, fee and tick spacing from the pool and refuses a pool whose factory is not the connector's reviewed deployment on that chain, naming the factory the pool reports. The result carries `support` (`verified`, or `candidate` for a chain declared only for qualification), the resolved identities, the pinned observation and the spot price of currency0 in currency1, which is `null` when the pool has no active liquidity. With `--depth-bps`, `depth.to_lower` is the currency1 the pool pays out before its price falls by that many basis points and `depth.to_upper` the currency0 it pays out before its price rises by as many; both exclude swap fees and are raw integer units. The walk reads every initialized tick inside the band, and a band wider than 5000 basis points or holding more than 2048 initialized ticks is refused rather than truncated.

For an Aerodrome classic or Curve pool, `ax pool` reports the connector's resolution (the pool's coins, decimals and, for Curve, its family and revision, verified against the reviewed factory or registry) and the pool's prices. `--depth-bps` is refused for them. Every price, here and for the V3-style pools, is whole units of `quote` per whole unit of `base`, fees excluded, and names its `kind`:

| Pool                        | Field                               | `kind`                                                                                                                                                       |
| --------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| V3-style, Aerodrome classic | `price`: currency0 in currency1     | `spot`, the marginal price at the pinned state (Aerodrome from its stored reserves, volatile or stable)                                                      |
| Curve StableSwap            | `prices`: each later coin in coin 0 | `spot`, from the invariant as the pool's `get_p` computes it                                                                                                 |
| Curve TwoCrypto, TriCrypto  | `prices`: each later coin in coin 0 | `last_spot` (`last_prices`, the marginal price after the pool's last swap or liquidity change, not an execution price) and `oracle` (`price_oracle`, an EMA) |

An Aerodrome classic or Curve StableSwap pool without liquidity on a priced side reports `null`. An empty Curve TwoCrypto or TriCrypto pool is refused: `ax pool` exits with `invalid_config`.

#### Discovery

`ax resolve` and `ax dex-pools` find candidates an author does not yet have. CoinGecko on-chain names the candidates and the chain verifies them; the vendor's liquidity and volume are reported as its figures, in USD cents, and are absent when it omits them. They need `COINGECKO_API_KEY` (a CoinGecko pro key) in the env file of the calling process or of the read gateway.

```
almanak ax resolve --chain eip155:8453 --query cbBTC
almanak ax dex-pools --chain eip155:8453 \
  --token eip155:8453/erc20:0x4200000000000000000000000000000000000006 \
  --quote eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 --depth-bps 100
```

`ax resolve` takes a symbol or an ERC-20 address. A symbol is matched against the tokens in the pools the vendor's search returns, case-insensitively, by the symbol the vendor lists or the token's own `symbol()`, and the candidates are ranked by vendor liquidity. An address the vendor does not list is still reported, with its vendor fields null. Every reported candidate is read on chain: `decimals()` verifies the contract and `symbol()` is reported as `onchain_symbol`, null when the token does not answer it as a string. `source.complete` is false, with the reason in `source.limitation`, when the vendor has not indexed part of the lookup or the search filled the two pages (40 pools) read, since a candidate may then be missing. `recommended` names the leading candidate only when no lookup went unindexed, it is verified, its `onchain_symbol` matches the symbol the vendor lists, and it is either the only candidate or its liquidity is measured at least ten times the next one's measured liquidity, and its own is nonzero; otherwise it is null and the author chooses. A search cut at 40 pools still recommends: liquidity is each token's total across all its pools, and a token large enough to rival the leader has pools among the first 40. A smaller same-symbol token can still be missed; `source.limitation` reports the cut. A symbol is never identity: the author states the CAIP-19 asset. Native assets are `slip44` identities and are not looked up.

`ax dex-pools` lists the pools the vendor reports for an ERC-20 `--token`, optionally only those pairing it with an ERC-20 `--quote`, in the vendor's order (24-hour volume with liquidity), not re-ranked, so an offset continues the vendor's list. The vendor offers no snapshot or cursor, so its order can shift between calls as volumes move. `--limit` (default 10, at most 20) and `--offset` page through them; `page.matched` counts the matches in the pages read and `page.has_more` says whether a later offset returns more. The vendor's venue label only selects a connector; a label the SDK does not know is matched by the pool's own `factory()` against each connector's reviewed factory, before `--connector` filters, so a factory-matched pool is kept. Every reported pool is resolved by its connector, which refuses a pool from an unreviewed deployment and names why. The vendor's pair is only a filter: a pool whose resolved currencies do not include `--token` (and `--quote`, when given) is refused with `vendor_mismatch`. Each pool reports a `status`:

| Status        | Meaning                                                                                                                            |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `verified`    | The connector resolved the pool at the pinned block; `market` is a paste-ready `markets:` entry                                    |
| `refused`     | The connector refused the pool, with its reason, or the pool it resolved does not trade the requested pair (`vendor_mismatch`)     |
| `unverified`  | Uniswap V4 pools whose key neither the vendor's pair and fee nor the pool's `Initialize` event states, including a malformed event |
| `unsupported` | No SDK connector for the venue: neither its label nor a reviewed factory names one                                                 |

A Uniswap V4 key is first derived from the vendor pair and the fee in its pool name as a hookless pool with the standard tick spacing for that fee; when its hash differs from the vendor's pool ID, the reviewed PoolManager's `Initialize` event for that ID states the exact key. The connector then admits the key exactly as it does for a strategy: a hooked pool is verified only when its hook has a reviewed profile, the profile's permission bits and fee mode, and code equal to its pin, and the row adds a `hook` block with the `profile`, hook `address`, `pin: match` and the current `terms` (a Fables or StablePairHook LP fee per direction, or a Pons hook fee and creator tax in bps). Any other hook is refused with the connector's reason. A pool holding the chain's native coin (ETH, POL, BNB or AVAX) keeps the native coin in its PoolKey and is verified with the fund holding its wrapped token (WETH, WPOL, WBNB or WAVAX): the reviewed Universal Router unwraps the input or wraps the output within the swap, so a `--token` of the wrapped token matches it. The row's `markets:` entry names the native currency; keep it native, since a selection naming the wrapped token is a different pool. Native pools on a chain without a reviewed router route, and native/wrapped-native pools, are refused with the connector's reason. Vendor USD figures are cents, and absent when missing, malformed or too large to state. `source.complete` is true only when the vendor's list ended within the pages read. One call reads at most five vendor pages, the token's 100 busiest pools, and the vendor does not index every pool; an incomplete list says so in `source.limitation`, and a pool missing from it may still exist. A token the vendor has not indexed is reported the same way, never as a complete empty list. A known pool, for example one a reference strategy names, is checked directly with `ax pool` or `ax pool-price`.

`ax lending-markets` lists a lending protocol's markets on a chain from the protocol's own records at the pinned block, or, for Morpho Blue where Morpho's API indexes the chain, from that API. None of them needs a key:

```
almanak ax lending-markets --connector aave_v3 --chain eip155:43114
almanak ax lending-markets --connector morpho_blue --chain eip155:8453 \
  --asset eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
```

| Connector              | Record                                                                                                                                                                                                                                                                 | Order and filters                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `aave_v3`, `sparklend` | the reviewed Pool's `getReservesList()` (`source.record` `pool_reserves`)                                                                                                                                                                                              | the Pool's order; `--asset` keeps that reserve                                                                                                                                                                                                                                                                                                                                                                                                         |
| `aave_v4`              | the reserves of each Spoke the chain's Hubs serve (`getSpokeAddress`, `getReserve`; `hub_spokes`)                                                                                                                                                                      | Hub, Spoke and reserve order; `--asset` keeps reserves of that asset; a Spoke that reverts `getReserveCount()` holds no reserves and is counted in `source.without_reserves`; one whose count cannot be read for any other reason (transport, throttling, an undecodable answer) is named in `source.unreadable`, as is a Hub whose Spokes cannot be enumerated (`hub`) and a reserve that cannot be read (`spoke`, `reserve_id`); the rest still list |
| `compound_v3`          | the Comets the chain's Configurator recorded in `CometDeployed` events (`comet_deployments`)                                                                                                                                                                           | deployment order; `--asset` keeps Comets whose base or collateral it is, `--collateral` those accepting it                                                                                                                                                                                                                                                                                                                                             |
| `morpho_blue`          | Morpho's API on Ethereum, Optimism, Polygon, Robinhood, Base and Arbitrum (`source.record` `morpho_api`, with `source.skipped` counting rows it answered in another shape); elsewhere the markets the reviewed core recorded in `CreateMarket` events (`core_markets`) | `--asset` (the loan asset) is required; supplied markets only, most supplied in the loan asset first; `--collateral` narrows; from the API, only markets Morpho lists unless `--include-unlisted` (`source.listed_only`, `source.curation` `morpho_api`); from the core's events, every supplied market (`listed_only` false, `curation` null)                                                                                                         |

A record only names a candidate. Each reported market is resolved by its connector as `lending-reserve` resolves it, and reports `status` `verified` with the same `config` block `lending-reserve` returns, or `refused` with the resolver's reason. A Compound V3 selection names the asked-for `--asset`, base or collateral, or else the base asset; a Comet whose assets cannot be read is named in `source.unreadable`. A Morpho selection is the market's loan leg; its collateral leg uses the same `market_id`. From the API, a Morpho row reports only its `market_id` and the asked-for `loan_asset` as the market's own; Morpho's view is under `vendor`: `collateral_asset`, `lltv`, `supplied` and `borrowed` in the loan asset, `supplied_usd` and `borrowed_usd`, `supply_apy`, Morpho's `listed` curation and its `warnings` (for example `not_whitelisted` or `unrecognized_collateral_asset`). They come from Morpho's current index, not the pinned block, are absent where it omits them, and are never strategy inputs; a verified row's `config` is the resolver's reading at the pinned block, and a market whose own loan or collateral asset contradicts the row is refused with `vendor_mismatch`; a row naming no collateral claims an idle market, whose collateral is the zero address. Anyone can create a Morpho market, and an unlisted one can show supply looped against worthless collateral that still passes the resolver's revision checks, so the API listing keeps to Morpho's listed markets by default; `--include-unlisted` adds the rest, each with Morpho's warnings. The core's events carry no curation, so a listing from them is not filtered: its `source` reports `listed_only` false and `curation` null, and `--include-unlisted` changes nothing there. From the core's events, `supplied` and `borrowed` are the totals the core last recorded, before interest accrued since; unsupplied markets are counted in `source.without_supply`, and a listing that would read more than 5,000 markets is refused. A provider that caps `eth_getLogs` ranges refuses a core's whole history in one request; ours does on Avalanche, where the listing then fails with that reason and `lending-reserve` still verifies a known market ID. Every reported asset is an object of its CAIP-19 `asset` and its on-chain `symbol()`, null where a token does not answer one as a string; the symbol is a label, the asset is the identity. A resolver checks the deployment's revisions, not whether a reserve takes deposits or loans now, so Aave V3 and SparkLend rows also carry the reserve's own `reserve` flags (`active`, `frozen`, `paused`, `borrowing_enabled`, `ltv_bps`, `liquidation_threshold_bps`) and Aave V4 rows its `frozen`, `paused` and `borrowable`; a frozen reserve, or one with no loan-to-value, cannot back a borrow even when `verified`. A flag read that fails reports `reserve` as null. `--limit` (default 10, at most 20) and `--offset` (at most 500) page through the matches, with `page.matched` and `page.has_more`. Only the page's markets are resolved. Without a filter, an Aave V4 listing reads only the page's reserves and a Compound V3 listing only the page's Comets: `matched` counts every one the record names, and a reserve or Comet that cannot be read is named in `source.unreadable` only when it falls on the page. With `--asset` or `--collateral`, each is read to count the matches, and one that cannot be read is named and not matched. Each event history is asked for from block zero in one request; a provider that refuses that range or result size fails the listing.
# Additional Guides

# Almanak Code

Almanak Code is Almanak's AI coding assistant. Every published wheel carries the binary for its own platform, so it is available as soon as the SDK is installed.

## Running it

Running `almanak` with no command starts the assistant:

```
almanak
```

Any command this CLI does not define is passed to it unchanged, so its own options work directly:

```
almanak code -p "supply WETH to Aave on Ethereum"
```

A command the SDK does define always belongs to the SDK, and so does anything starting with an option:

```
almanak check strategies/aave_v4_weth_supply   # SDK
almanak --help                                 # SDK help
almanak --unknown-option                       # SDK error, exit 2
```

## The Python LSP

The assistant drives `pyright-langserver` from `PATH` and falls back to `npx` when it is absent. To install it alongside the SDK:

```
uv add "almanak[code]"
```

## Platform coverage

Wheels are published for macOS on arm64 and x86_64, Linux on arm64 and x86_64, and Windows on x86_64. Installing from the source distribution, or on any other platform, gives a working SDK with no assistant; `almanak` then reports `almanak_code_unavailable` and exits 1.

The pinned release is recorded in `.almanak-code-version`. Nothing is downloaded at install time or at run time.

## The SDK v3 authoring lane

Forwarding sets `ALMANAK_SDK_GENERATION=v3` and `ALMANAK_SDK_V3_PYTHON` to the running interpreter unless they are already set, so the assistant works in its SDK v3 lane: its prompts, sub-agents and strategy workflow target a `strategy.py` plus one `config.yaml`, validated with `almanak check --json`, and its `sdk-docs`/`sdk-src` references resolve to this installed package. Worked examples are listed with `almanak support --references`.

Inside an assistant session the SDK parser always wins, so a command line this CLI does not define, such as a V2 `almanak strat new`, reports the commands it does define rather than reopening the assistant.

`almanak ax` exists in both generations with different meanings: V2's `ax` runs DeFi actions such as swaps, while this SDK's `ax` is read-only protocol observations (`pool`, `pool-price`, `lending-reserve`, `lending-position`, `perp-position`, `perp-markets`, `balance`), read through `--env-file` or a `service read-gateway` sidecar. V2 `ax` command lines, such as `almanak ax swap`, are rejected by the SDK parser.

# Daily SDK QA

The bench runs the actual reference strategies through the existing fork harness and backtest CLI. It validates coverage against the trusted connector support contracts before starting any provider work. The [design contract](https://sdk.docs.almanak.co/internal/blueprints/18-daily-qa-bench.html) owns its scope.

The accepted [chain and connector expansion](https://sdk.docs.almanak.co/internal/blueprints/19-chain-connector-expansion.html) has a separate [96-cell delivery inventory](https://sdk.docs.almanak.co/internal/blueprints/19-chain-connector-inventory.json). It records candidates and missing rollout evidence, not additional supported capabilities. The current QA plan still derives support from trusted connector declarations. New pairs require actual reference outcomes and prepared offline backtests before support promotion. The [composed borrowing/trading report](https://sdk.docs.almanak.co/internal/reports/2026-09-07-composed-borrowing-and-trading.html) retains the current shared-inventory, process-recovery and historical evidence.

## Run locally

Install the locked environment and hook, then inspect the complete matrix:

```
uv sync --locked
uv run --locked pre-commit install
uv run --locked python -m scripts.qa_bench --plan --output artifacts/qa-plan
```

Run all cases with explicit read-only provider bindings:

```
uv run --locked python -m scripts.qa_bench \
  --env-file .env --output artifacts/qa-daily
```

Use a new output directory for each invocation. Existing output is never reused as evidence for a new run. Plan mode performs no provider calls. Linux needs Anvil and working Bubblewrap/unprivileged namespaces. macOS uses sandbox-exec; Full QA runs locally with retained evidence. Hosted CI runs lint, types, the offline suite with evidence-dependent modules skipped, the CRAP gate over that reduced coverage scoped to the functions a pull request changed, and CLI smoke checks on Linux and macOS; it does not run the full QA suite. The manual `Fork Qualification` workflow (`.github/workflows/fork_qualification.yml`) runs the discovered fork lifecycle and, where the checked-out connectors declare production EOA support, the production rehearsal for named references on hosted runners against the repository's provider key; it is the fork qualification a reference listed in `tests/strategy_fork/rehearsal.py::UNREHEARSED` needs before it joins the default rehearsal selection. Each run retains the case evidence as a workflow artifact and spends no funds. GMX references cannot pass there: their keeper drivers settle requests with authentic signed reports from local evidence captures that hosted runners do not have. They qualify by running the workflow's pytest command locally with `--fork-env=.env`. The manual `Cell QA Matrix` workflow runs one connector/chain cell's QA cases on hosted runners in parallel; see [Hosted cell matrix](#hosted-cell-matrix) for what it does and does not establish.

For a focused diagnostic run, select exact case IDs from the plan:

```
uv run --locked python -m scripts.qa_bench \
  --env-file .env --output artifacts/qa-focused \
  --case backtest/pinned/uniswap_v4_trading/economic
```

`--lane` selects offline, pytest, fork, faults, backtest or blocked. Unselected cases remain visible as skipped, so a focused run is never displayed as a passing full bench. The exit code for an explicit selection describes its selected cases. The full run exits nonzero if any required case did not pass. Unexpected pytest skips and empty test collections fail the corresponding case.

## Inputs and coverage

The current plan validates 82 connector/chain/profile/model/action obligations in 93 cases, including eight explicit blockers for unregistered rollout references. The executable cases include seven references through both Safe/Roles operator variants and EOA on pinned and newly resolved forks; twelve backtest reference/model combinations with pinned, recent and short native-cadence variants; the full offline gate; focused fork faults and multi-chain outages; support refusals; and supplemental prepared-model action checks. Counts change with declared support and reviewed proof mappings, not by hiding failed cases.

The catalog in `tests/qa/catalog.py` records exercised actions and model revisions. Backtest mode is an explicit field checked against the declared model, not inferred from its revision label. Required support comes from the existing connector composition. Adding a supported chain, profile, action or model without a proof fails plan validation. Every discovered strategy needs a catalog mapping. Supplemental model tests are labeled component proofs; they are not additional strategy lifecycles.

An unregistered candidate reference has an explicit blocked case and contributes no supported-action claims. The case remains in the full result denominator; omitting it or clearing its blocker fails catalog validation. Candidate blockers prevent a passing full bench even when all executable cases pass. Promoting its chain support requires replacing the blocker with reviewed proof mappings.

Author economic choices and named markets remain in each strategy's `config.yaml`. Backtests use a separate backtest configuration. Every reference keeps providers, account bindings and fork fixtures out of its author files: QA derives one operator file per case from `examples/operators/`, varying the pinned or fresh anchor, the Safe or EOA profile and the shared or distinct operator there, and drives its fork lifecycle through the public `almanak run`, `pause`, `resume`, `teardown` and `shutdown` commands. QA writes and validates full resolved configurations, preserving all changes as evidence:

- Fresh forks resolve one block identity per chain before case execution.
- Recent historical windows preserve the configured duration and cleanup period. The explicit source lag in `tests/qa/policy.json` is currently two hours on each chain, measured after the cleanup endpoint. Missing history is refused; the bench does not shorten the window or choose another provider.
- Native-cadence probes use a ten-minute window and ten-minute cleanup, with the author interval and explicit 120-second hold / 60-second collection parameters where those parameters apply. The ordinary pinned/recent variants retain their authored holding parameters and explicit backtest cadence overrides.
- Detailed Aave history uses an explicit debt-free author variant and an hourly override in the long runs. Its native probe has no interval override.
- The EOA rehearsal harness supplies the explicit Ethereum test gas policy in `tests/strategy_fork/rehearsal-policy.json` when a reference has no production policy. Provider selection still comes from that reference. Generated production configurations run only against identified local forks with Foundry test keys; this does not change author production authority.

Prepared backtests use a new dataset directory for cold preparation, then repeat preparation and run execution/reproduction with OS network isolation. Local IPC and loopback gateway calls remain available. Detailed history captures online reads and reproduces them in the same network isolation. Successful cases must show closure, delivered outcomes and the expected economic actions on each chain. Uniswap prepared runs also compare integer calculations with independent Quoter reads at the same sampled blocks, in both directions and several sizes.

Existing frozen fixture, recovery, integer boundary, vendor-source and model regressions run in the full offline gate. The online fault lane runs `tests/fork` and the multi-chain outage tests, including Aave liquidation/contract comparisons.

## Results and limits

`public/results.json` is the versioned complete matrix. `summary.md`, `index.html` and `junit.xml` present the same outcomes. `private/` holds working files and credentials generated by the harness. Both directories remain local. Evidence export preserves dataset bytes or refuses to export a file containing known credential material. Logs are redacted. A refused required artifact blocks the case. The source archive and hashes retain the tested implementation, including uncommitted changes; source changes during execution invalidate passing results.

`public/proof-ledger.json` links every declared connector, chain, action, account profile, model and protocol variant obligation to its scheduled case IDs. An obligation passes only when every mapped case passes. Failed, skipped, unstarted or missing cases remain outstanding, including separate fresh-input and recovery cases. The ledger preserves the tested source identity. It does not turn an undeclared candidate into supported scope or replace the 96-cell rollout inventory.

Each case has isolated journals, gateways, forks and retained evidence. Provider cases run serially initially; `--workers` controls only the offline pytest worker count and accepts `auto` (the default) or an explicit positive integer. The default runs the combined coverage/CRAP gate with `-n auto`. The fault lane uses one pytest worker. The runner does not automatically retry failures. Existing gateway collection limits retain their own explicit bounded provider retry behavior. This is deliberately conservative until measured complete daily runs establish useful concurrency and timeout settings.

Per-command records measure cold preparation, cached preparation, execution and reproduction wall time independently. Quoter comparisons and source resolution remain separate activities. The SDK's retained dataset and progress reports provide provider activity, model and cadence detail. No throughput or ETA claim is inferred from test counts.

The case timeout defaults to one hour. This is an operational bound, not a measured runtime target. A backtest case's execute and reproduce steps share that bound; dataset preparation keeps its own thirty minute bound. Timeouts, missing providers, unstarted cases and source-resolution failures remain visible. Unknown failures are labeled without guessing that the SDK or provider caused them.

Pass `--previous path/to/results.json` to write `changes.json`, comparing case status/category with the previous run. Unchanged cases are absent from this file. Supply the preceding local result explicitly. The bench sends no Slack/email messages. An absent prior artifact is reported as a fresh comparison baseline.

## Hosted cell matrix

The manual `Cell QA Matrix` workflow (`.github/workflows/cell_qa_matrix.yml`) runs one connector/chain cell's QA matrix on hosted runners, one job per case, through the same `scripts.qa_bench --case` entry point as a local run. It spends no funds and installs no production permissions: fixture accounts are public Foundry test keys on identified Anvil forks of the repository's read-only Alchemy provider. Dispatch it with a connector ID and a CAIP-2 chain, or with explicit case IDs:

```
gh workflow run cell_qa_matrix.yml --ref <branch> \
  -f connector=uniswap_v3 -f chain=eip155:56
```

Selection. `scripts/ci/cell_qa_matrix.py` selects every catalog case whose obligations name the connector on the chain, which are the cases the proof ledger maps to the cell. The `fork` and `backtest` lanes run by default and the `pytest` lane can be added. The `offline`, `faults` and `blocked` lanes are refused: the offline gate needs the retained evidence tree, `fork/faults` runs every connector's fault suite, and a candidate blocker cannot pass. A selected cell case outside the chosen lanes stays listed as not run. The reverse-fault entries are the reversal tests in `tests/fork/test_*reversal*.py` that are parametrized by one of the cell's references, or whose module declares it as `REFERENCE`. Each one runs as its own pytest job. When no such test exists, the run reports that gap and does not pass. The `nodes` input adds explicit `tests/fork` or `tests/strategy_fork` node IDs, for example a cell's recovery-foundation nodes. An unknown case, an empty cell and more than 256 entries are refused.

Execution. Each job runs on `ubuntu-latest-l` with Bubblewrap and Anvil and installs the locked environment on CPython 3.12.13. At most eight jobs run at once. Every job is a separate deployment with its own gateway, so the per-deployment provider caps apply to each job, and the Alchemy plan limit is the only ceiling they share. Nothing is retried.

Source and input identity. Every job checks out the run's commit. Before and after its entry, each job records the commit, the bench's tree hash, the lock hash, the Python build and the tool versions. The aggregate job recomputes the tree hash from a clean checkout of the same commit. Any mismatch, or any change during a job, blocks that entry. The plan job resolves the fresh fork origins and the input time once and passes them to every job through `qa_bench --frozen-inputs`. All fresh forks in one run therefore start from the same block per chain, and all recent historical windows end at the same instant, as in one local run.

Time budgets. Fork and pytest-lane cases keep the bench's one-hour case bound, in a 90-minute job. A backtest case gets its cold-preparation budget from `tests/qa/policy.json` plus 7200 seconds. The policy budget is 1800 seconds by default and 7200 seconds for the five composed references it lists; detailed historical cases prepare nothing. The extra 7200 seconds cover cached preparation, execution, reproduction and model comparisons, and match the bound the local runbook recommends for slow windows. The job timeout adds 30 minutes for setup and upload. The largest case, a 7200-second native preparation, gets 14400 seconds in a 270-minute job, inside the six-hour hosted job limit. The planner refuses any case whose bound cannot fit. The preparation budgets are unchanged. Each job prepares its dataset cold from the provider, exactly as a local case does, so no retained dataset is needed or reused. These are operational bounds, not measured runtimes, and a timeout remains a blocked case.

Evidence. Only the bench's sanitized `public/` tree leaves the runner. The bench's export boundary redacts logs, drops credential files and refuses any other file that contains a known secret. `private/` never leaves the runner. Reversal-node files pass through the same export. Before upload, each job scans file names and bytes, including gzip members, for the provider keys. The key values come from the environment only. On a match, the job uploads nothing but its record, and the entry is blocked as unsafe evidence. The per-case source snapshot is dropped from the job artifacts. The aggregate archives one `source.tar.gz` for the verified commit. All artifacts are retained for 90 days.

Results. The `aggregate` job writes `results.json`, marked `"execution": "hosted_cell_matrix"`, and a job summary. The summary has one row per planned entry, the cell's cases that were not run and a cell-scoped proof ledger. The workflow fails unless every planned entry passed on one source. A skipped, missing, timed-out, unsafe or mixed-identity entry keeps the result `incomplete_or_failed`.

### What a hosted cell run establishes

It can satisfy the first item of the [per-cell package](https://sdk.docs.almanak.co/internal/reports/2026-09-16-scoped-promotion-amendment.html): the cell's fork and backtest cases and its reverse-fault node, passed on one commit. That commit must be the frozen integrated source, or the proof must survive the amendment's path-level renewal check. It is not the local retained-evidence gate and does not establish:

- Pytest-lane component proofs that read retained evidence, such as `model-actions/uniswap_v4/eip155:56`. They cannot pass on hosted runners; run them locally with the evidence tree present.
- The full offline suite with retained evidence, combined coverage, CRAP at most 30, Ruff, formatting and mypy (package item 2). Hosted CI's offline suite skips the evidence modules and is not a substitute.
- The recovery-foundation nodes (item 3), unless they were passed as `nodes` and do not need retained evidence. Otherwise they remain local work.
- Append-only sanitized evidence archival with checksum verification (item 4). Download the artifacts, verify their digests and seal them locally. The workflow writes nothing to the evidence bucket.
- GMX cells. Their keeper drivers settle requests with signed reports from local captures that hosted runners do not have, so those cases fail here.

## Local evidence and scheduling

Generated captures under `docs/internal/reports/evidence/` and bench output under `artifacts/` are ignored by Git and remain local. Full offline QA requires the retained captures at their original paths. A fresh clone needs a local copy of those inputs; missing evidence must not be converted into passing or skipped required cases. See the [evidence policy](https://sdk.docs.almanak.co/internal/reports/index.html).

Hosted daily QA and its GitHub watchdog have been removed. There is no hosted daily coverage or automatic replacement local schedule. Operators can invoke the commands above with explicit provider bindings and retained results. Hosted CI skips evidence-dependent tests and reports that scope in its job summary.
# CLI Reference

# almanak ax

read-only protocol observations at a pinned block

```
usage: almanak ax [-h]
                  {pool-price,lending-reserve,lending-position,perp-position,perp-markets,pool,balance,resolve,dex-pools,lending-markets}
                  ...
```

## almanak ax pool-price

Uniswap V4 pool spot price

```
usage: almanak ax pool-price [-h] --connector {uniswap_v4}
                             (--config CONFIG | --chain CHAIN)
                             [--currency0 CURRENCY0] [--currency1 CURRENCY1]
                             [--fee FEE] [--tick-spacing TICK_SPACING]
                             [--hooks HOOKS] [--base BASE] [--block BLOCK]
                             [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                             [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                       | Description                                                                                                  |
| --------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `uniswap_v4`; required | canonical connector ID                                                                                       |
| `--config`            | Path                          | JSON request file with the full pool source                                                                  |
| `--chain`             |                               | strict CAIP-2 chain, given with the pool key flags                                                           |
| `--currency0`         |                               | CAIP-19 currency0 of the pool key                                                                            |
| `--currency1`         |                               | CAIP-19 currency1 of the pool key                                                                            |
| `--fee`               | int                           | pool fee in hundredths of a basis point                                                                      |
| `--tick-spacing`      | int                           | pool tick spacing                                                                                            |
| `--hooks`             |                               | reviewed hook address of the pool key; omit for a hookless pool (a dynamic-fee hook takes --fee 8388608)     |
| `--base`              |                               | CAIP-19 currency to price; defaults to currency0                                                             |
| `--block`             | block; default `finalized`    | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                          | private provider credentials read by this process                                                            |
| `--gateway-url`       |                               | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                          | authentication token file for --gateway-url                                                                  |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax lending-reserve

lending reserve state, its price sources and a config-ready market entry

```
usage: almanak ax lending-reserve [-h] --connector
                                  {aave_v3,sparklend,aave_v4,compound_v3,morpho_blue}
                                  (--config CONFIG | --chain CHAIN)
                                  [--asset ASSET] [--pool POOL]
                                  [--spoke SPOKE] [--reserve-id RESERVE_ID]
                                  [--comet COMET] [--market-id MARKET_ID]
                                  [--leg {loan,collateral}] [--core CORE]
                                  [--block BLOCK]
                                  [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                  [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                                                                          | Description                                                                                                  |
| --------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`; required | canonical connector ID                                                                                       |
| `--config`            | Path                                                                             | JSON market selection, as written under `markets:`                                                           |
| `--chain`             |                                                                                  | strict CAIP-2 chain, given with the selector flags                                                           |
| `--asset`             |                                                                                  | CAIP-19 reserve asset (aave_v3, sparklend, compound_v3)                                                      |
| `--pool`              |                                                                                  | Aave V3 or SparkLend Pool; omit for the reviewed Pool                                                        |
| `--spoke`             |                                                                                  | Aave V4 Spoke address                                                                                        |
| `--reserve-id`        | int                                                                              | Aave V4 reserve ID on the Spoke                                                                              |
| `--comet`             |                                                                                  | Compound V3 Comet address                                                                                    |
| `--market-id`         |                                                                                  | Morpho Blue market ID                                                                                        |
| `--leg`               | one of `loan`, `collateral`                                                      | Morpho Blue market side                                                                                      |
| `--core`              |                                                                                  | Morpho Blue core; omit for the reviewed deployment                                                           |
| `--block`             | block; default `finalized`                                                       | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                             | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                  | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                             | authentication token file for --gateway-url                                                                  |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax lending-position

account position in one lending reserve

```
usage: almanak ax lending-position [-h] --connector
                                   {aave_v3,sparklend,aave_v4,compound_v3,morpho_blue}
                                   (--config CONFIG | --chain CHAIN)
                                   [--asset ASSET] [--pool POOL]
                                   [--spoke SPOKE] [--reserve-id RESERVE_ID]
                                   [--comet COMET] [--market-id MARKET_ID]
                                   [--leg {loan,collateral}] [--core CORE]
                                   [--block BLOCK]
                                   [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                   [--gateway-auth-file GATEWAY_AUTH_FILE]
                                   --account ACCOUNT
```

| Option                | Details                                                                          | Description                                                                                                  |
| --------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`; required | canonical connector ID                                                                                       |
| `--config`            | Path                                                                             | JSON market selection, as written under `markets:`                                                           |
| `--chain`             |                                                                                  | strict CAIP-2 chain, given with the selector flags                                                           |
| `--asset`             |                                                                                  | CAIP-19 reserve asset (aave_v3, sparklend, compound_v3)                                                      |
| `--pool`              |                                                                                  | Aave V3 or SparkLend Pool; omit for the reviewed Pool                                                        |
| `--spoke`             |                                                                                  | Aave V4 Spoke address                                                                                        |
| `--reserve-id`        | int                                                                              | Aave V4 reserve ID on the Spoke                                                                              |
| `--comet`             |                                                                                  | Compound V3 Comet address                                                                                    |
| `--market-id`         |                                                                                  | Morpho Blue market ID                                                                                        |
| `--leg`               | one of `loan`, `collateral`                                                      | Morpho Blue market side                                                                                      |
| `--core`              |                                                                                  | Morpho Blue core; omit for the reviewed deployment                                                           |
| `--block`             | block; default `finalized`                                                       | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                             | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                  | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                             | authentication token file for --gateway-url                                                                  |
| `--account`           | required                                                                         | EVM account address                                                                                          |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax perp-position

GMX V2 account position

```
usage: almanak ax perp-position [-h] --connector {gmx_v2} --config CONFIG
                                [--block BLOCK]
                                [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                [--gateway-auth-file GATEWAY_AUTH_FILE]
                                --account ACCOUNT
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `gmx_v2`; required  | canonical connector ID                                                                                       |
| `--config`            | Path; required             | JSON request file for the connector                                                                          |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |
| `--account`           | required                   | EVM account address                                                                                          |

At most one of `--env-file`, `--gateway-url`.

## almanak ax perp-markets

GMX V2 market lookup

```
usage: almanak ax perp-markets [-h] --connector {gmx_v2}
                               (--config CONFIG | --chain CHAIN)
                               [--query QUERY] [--block BLOCK]
                               [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                               [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `gmx_v2`; required  | canonical connector ID                                                                                       |
| `--config`            | Path                       | JSON request file:                                                                                           |
| `--chain`             |                            | strict CAIP-2 chain, given with --query                                                                      |
| `--query`             |                            | GM share address or a label such as "BTC/USD"                                                                |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax pool

V3-style pool state and optional liquidity depth, or an Aerodrome or Curve pool's resolution

```
usage: almanak ax pool [-h] --connector
                       {uniswap_v3,pancakeswap_v3,aerodrome_slipstream,aerodrome,curve}
                       (--config CONFIG | --chain CHAIN) [--pool POOL]
                       [--block BLOCK]
                       [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                       [--gateway-auth-file GATEWAY_AUTH_FILE]
                       [--depth-bps DEPTH_BPS]
```

| Option                | Details                                                                                       | Description                                                                                                  |
| --------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome`, `curve`; required | canonical connector ID                                                                                       |
| `--config`            | Path                                                                                          | JSON request file:                                                                                           |
| `--chain`             |                                                                                               | strict CAIP-2 chain of the pool, given with --pool                                                           |
| `--pool`              |                                                                                               | pool contract address, given with --chain                                                                    |
| `--block`             | block; default `finalized`                                                                    | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                                          | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                               | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                                          | authentication token file for --gateway-url                                                                  |
| `--depth-bps`         | depth_bps                                                                                     | also report pool output within this price move, 1 to 5000 basis points                                       |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax balance

native or ERC-20 balance

```
usage: almanak ax balance [-h] --asset ASSET --account ACCOUNT [--block BLOCK]
                          [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                          [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--asset`             | required                   | CAIP-19 asset type                                                                                           |
| `--account`           | required                   | EVM account address; repeat to read several accounts at the same block                                       |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

## almanak ax resolve

ERC-20 token candidates for a symbol or address, verified at a pinned block

```
usage: almanak ax resolve [-h] --chain CHAIN --query QUERY [--limit LIMIT]
                          [--block BLOCK]
                          [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                          [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--chain`             | required                   | strict CAIP-2 chain                                                                                          |
| `--query`             | required                   | token symbol or contract address                                                                             |
| `--limit`             | parse; default `10`        | candidates to report (1 to 30)                                                                               |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

## almanak ax dex-pools

pools that trade a token, verified through each connector's resolver

```
usage: almanak ax dex-pools [-h] --chain CHAIN --token TOKEN [--quote QUOTE]
                            [--connector {uniswap_v4,uniswap_v3,pancakeswap_v3,aerodrome_slipstream,aerodrome,curve}]
                            [--limit LIMIT] [--offset OFFSET]
                            [--depth-bps DEPTH_BPS] [--block BLOCK]
                            [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                            [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                                                                                           | Description                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--chain`             | required                                                                                          | strict CAIP-2 chain                                                                                          |
| `--token`             | required                                                                                          | CAIP-19 ERC-20 asset the pools trade                                                                         |
| `--quote`             |                                                                                                   | CAIP-19 ERC-20 asset; only pools pairing --token with it                                                     |
| `--connector`         | one of `uniswap_v4`, `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome`, `curve` | only pools of this connector                                                                                 |
| `--limit`             | parse; default `10`                                                                               | pools to report (1 to 20)                                                                                    |
| `--offset`            | parse                                                                                             | matching pools to skip, for paging                                                                           |
| `--depth-bps`         | depth_bps                                                                                         | also report each verified V3-style pool's depth within this price move                                       |
| `--block`             | block; default `finalized`                                                                        | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                                              | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                                   | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                                              | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

## almanak ax lending-markets

a lending protocol's markets on a chain, verified through its connector's resolver

```
usage: almanak ax lending-markets [-h] --connector
                                  {aave_v4,aave_v3,sparklend,compound_v3,morpho_blue}
                                  --chain CHAIN [--asset ASSET]
                                  [--collateral COLLATERAL] [--limit LIMIT]
                                  [--include-unlisted] [--offset OFFSET]
                                  [--block BLOCK]
                                  [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                  [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                                                                          | Description                                                                                                                                     |
| --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `--connector`         | one of `aave_v4`, `aave_v3`, `sparklend`, `compound_v3`, `morpho_blue`; required |                                                                                                                                                 |
| `--chain`             | required                                                                         | strict CAIP-2 chain                                                                                                                             |
| `--asset`             |                                                                                  | CAIP-19 ERC-20 asset: an Aave or SparkLend reserve asset, a Compound base or collateral asset, the Morpho loan asset (required for morpho_blue) |
| `--collateral`        |                                                                                  | CAIP-19 ERC-20 asset; only Compound or Morpho markets accepting it                                                                              |
| `--limit`             | parse; default `10`                                                              | markets to report (1 to 20)                                                                                                                     |
| `--include-unlisted`  |                                                                                  | morpho_blue: also list markets Morpho's API does not mark listed; they keep its warnings                                                        |
| `--offset`            | parse                                                                            | matching markets to skip, for paging                                                                                                            |
| `--block`             | block; default `finalized`                                                       | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash                                    |
| `--env-file`          | Path                                                                             | private provider credentials read by this process                                                                                               |
| `--gateway-url`       |                                                                                  | read-only observation gateway on loopback (service read-gateway)                                                                                |
| `--gateway-auth-file` | Path                                                                             | authentication token file for --gateway-url                                                                                                     |

At most one of `--env-file`, `--gateway-url`.

# almanak backtest

prepare, run and reproduce historical simulations

```
usage: almanak backtest [-h] {run,prepare,resume,reproduce} ...
```

## almanak backtest run

```
usage: almanak backtest run [-h] [--config CONFIG] [--strategy STRATEGY]
                            --backtest-config BACKTEST_CONFIG
                            [--gateway-config GATEWAY_CONFIG] --output OUTPUT
                            [--env-file ENV_FILE] [--dataset DATASET]
                            [--progress {auto,text,json,none}]
                            [--progress-file PROGRESS_FILE]
                            [--timing-history TIMING_HISTORY]
                            [--timing-scope TIMING_SCOPE]
                            [--format {text,json}] [--json]
                            [directory]
```

| Option              | Details                                               | Description                                                          |
| ------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `directory`         | Path; default `.`                                     |                                                                      |
| `--config`          | Path                                                  |                                                                      |
| `--strategy`        | Path                                                  |                                                                      |
| `--backtest-config` | Path; required                                        |                                                                      |
| `--gateway-config`  | Path                                                  | operator provider, account and execution settings                    |
| `--output`          | Path; required                                        |                                                                      |
| `--env-file`        | Path                                                  |                                                                      |
| `--dataset`         | Path                                                  |                                                                      |
| `--progress`        | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`   | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history`  | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`    |                                                       | comparable worker environment; defaults to this host                 |
| `--format`          | one of `text`, `json`                                 |                                                                      |
| `--json`            |                                                       |                                                                      |

## almanak backtest prepare

```
usage: almanak backtest prepare [-h] [--config CONFIG] [--strategy STRATEGY]
                                --backtest-config BACKTEST_CONFIG
                                [--gateway-config GATEWAY_CONFIG] --output
                                OUTPUT [--env-file ENV_FILE]
                                [--progress {auto,text,json,none}]
                                [--progress-file PROGRESS_FILE]
                                [--timing-history TIMING_HISTORY]
                                [--timing-scope TIMING_SCOPE]
                                [--format {text,json}] [--json]
                                [directory]
```

| Option              | Details                                               | Description                                                          |
| ------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `directory`         | Path; default `.`                                     |                                                                      |
| `--config`          | Path                                                  |                                                                      |
| `--strategy`        | Path                                                  |                                                                      |
| `--backtest-config` | Path; required                                        |                                                                      |
| `--gateway-config`  | Path                                                  | operator provider, account and execution settings                    |
| `--output`          | Path; required                                        |                                                                      |
| `--env-file`        | Path                                                  |                                                                      |
| `--progress`        | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`   | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history`  | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`    |                                                       | comparable worker environment; defaults to this host                 |
| `--format`          | one of `text`, `json`                                 |                                                                      |
| `--json`            |                                                       |                                                                      |

## almanak backtest resume

```
usage: almanak backtest resume [-h] --run RUN [--env-file ENV_FILE]
                               [--progress {auto,text,json,none}]
                               [--progress-file PROGRESS_FILE]
                               [--timing-history TIMING_HISTORY]
                               [--timing-scope TIMING_SCOPE]
                               [--format {text,json}] [--json]
```

| Option             | Details                                               | Description                                                          |
| ------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `--run`            | Path; required                                        |                                                                      |
| `--env-file`       | Path                                                  |                                                                      |
| `--progress`       | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`  | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history` | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`   |                                                       | comparable worker environment; defaults to this host                 |
| `--format`         | one of `text`, `json`                                 |                                                                      |
| `--json`           |                                                       |                                                                      |

## almanak backtest reproduce

```
usage: almanak backtest reproduce [-h] --run RUN
                                  [--progress {auto,text,json,none}]
                                  [--progress-file PROGRESS_FILE]
                                  [--timing-history TIMING_HISTORY]
                                  [--timing-scope TIMING_SCOPE]
                                  [--format {text,json}] [--json]
```

| Option             | Details                                               | Description                                                          |
| ------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `--run`            | Path; required                                        |                                                                      |
| `--progress`       | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`  | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history` | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`   |                                                       | comparable worker environment; defaults to this host                 |
| `--format`         | one of `text`, `json`                                 |                                                                      |
| `--json`           |                                                       |                                                                      |

# almanak check

validate author inputs and selected-mode readiness

```
usage: almanak check [-h] [--config CONFIG] [--strategy STRATEGY]
                     [--fork | --production] [--gateway-config GATEWAY_CONFIG]
                     [--env-file ENV_FILE] [--online] [--normalized]
                     [--backtest-config BACKTEST_CONFIG]
                     [--format {text,json}] [--json]
                     [directory]
```

| Option              | Details               | Description                                                    |
| ------------------- | --------------------- | -------------------------------------------------------------- |
| `directory`         | Path; default `.`     |                                                                |
| `--config`          | Path                  |                                                                |
| `--strategy`        | Path                  |                                                                |
| `--fork`            |                       |                                                                |
| `--production`      |                       |                                                                |
| `--gateway-config`  | Path                  | operator provider, account and execution settings              |
| `--env-file`        | Path                  | private provider credentials read only by the operator gateway |
| `--online`          |                       | resolve current facts through a read-only gateway              |
| `--normalized`      |                       |                                                                |
| `--backtest-config` | Path                  |                                                                |
| `--format`          | one of `text`, `json` |                                                                |
| `--json`            |                       |                                                                |

At most one of `--fork`, `--production`.

# almanak lifecycle commands

Retained-run commands that share the `--run` workspace argument.

## almanak status

show retained run state and service availability

```
usage: almanak status [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak pause

pause the coordinator; positions and services remain

```
usage: almanak pause [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak resume

resume captured execution through its original services

```
usage: almanak resume [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak teardown

request and execute economic closure

```
usage: almanak teardown [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak shutdown

stop local services, preserving recorded evidence

```
usage: almanak shutdown [-h] --run RUN [--format {text,json}] [--json]
                        [--force]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |
| `--force`  |                       |             |

# almanak permissions

generate or verify strategy-derived permissions

```
usage: almanak permissions [-h] {generate,verify} ...
```

## almanak permissions generate

```
usage: almanak permissions generate [-h] --gateway-url GATEWAY_URL
                                    --gateway-auth-file GATEWAY_AUTH_FILE
                                    --strategy STRATEGY --config CONFIG
                                    --output OUTPUT
```

| Option                | Details        | Description |
| --------------------- | -------------- | ----------- |
| `--gateway-url`       | required       |             |
| `--gateway-auth-file` | Path; required |             |
| `--strategy`          | Path; required |             |
| `--config`            | Path; required |             |
| `--output`            | Path; required |             |

## almanak permissions verify

```
usage: almanak permissions verify [-h] --gateway-url GATEWAY_URL
                                  --gateway-auth-file GATEWAY_AUTH_FILE
                                  --manifest MANIFEST
```

| Option                | Details        | Description |
| --------------------- | -------------- | ----------- |
| `--gateway-url`       | required       |             |
| `--gateway-auth-file` | Path; required |             |
| `--manifest`          | Path; required |             |

# almanak report

render recorded evidence without network access

```
usage: almanak report [-h] --run RUN [--output OUTPUT]
                      [--format {text,json,html}] [--json]
```

| Option     | Details                       | Description |
| ---------- | ----------------------------- | ----------- |
| `--run`    | Path; required                |             |
| `--output` | Path                          |             |
| `--format` | one of `text`, `json`, `html` |             |
| `--json`   |                               |             |

# almanak run

start a supervised fork or production workspace

```
usage: almanak run [-h] [--config CONFIG] [--strategy STRATEGY]
                   (--fork | --production) --gateway-config GATEWAY_CONFIG
                   --env-file ENV_FILE --output OUTPUT [--format {text,json}]
                   [--json]
                   [directory]
```

| Option             | Details               | Description                                                    |
| ------------------ | --------------------- | -------------------------------------------------------------- |
| `directory`        | Path; default `.`     |                                                                |
| `--config`         | Path                  |                                                                |
| `--strategy`       | Path                  |                                                                |
| `--fork`           |                       |                                                                |
| `--production`     |                       |                                                                |
| `--gateway-config` | Path; required        | operator provider, account and execution settings              |
| `--env-file`       | Path; required        | private provider credentials read only by the operator gateway |
| `--output`         | Path; required        |                                                                |
| `--format`         | one of `text`, `json` |                                                                |
| `--json`           |                       |                                                                |

Exactly one of `--fork`, `--production`.

# almanak service

advanced explicit process operation

```
usage: almanak service [-h]
                       {gateway,read-gateway,start,resume,status,teardown} ...
```

## almanak service gateway

```
usage: almanak service gateway [-h]
                               (--config CONFIG | --gateway-config GATEWAY_CONFIG)
                               [--read-context READ_CONTEXT | --preparation-file PREPARATION_FILE]
                               [--database DATABASE]
                               (--fork-url FORK_URL | --fork-routes FORK_ROUTES | --production | --fork | --backtest)
                               [--fork-key-file FORK_KEY_FILE]
                               [--env-file ENV_FILE] [--accounts ACCOUNTS]
                               [--key-file KEY_FILE]
                               [--password-file PASSWORD_FILE]
                               --auth-token-file AUTH_TOKEN_FILE
                               (--port PORT | --listen-fd LISTEN_FD)
                               [--lease-seconds LEASE_SECONDS]
```

| Option               | Details             | Description                                                         |
| -------------------- | ------------------- | ------------------------------------------------------------------- |
| `--config`           | Path                |                                                                     |
| `--gateway-config`   | Path                | operator provider, account and execution settings                   |
| `--read-context`     | Path                | generated provisional read context                                  |
| `--preparation-file` | Path                | generated frozen deployment preparation                             |
| `--database`         | Path                |                                                                     |
| `--fork-url`         |                     |                                                                     |
| `--fork-routes`      | Path                |                                                                     |
| `--production`       |                     |                                                                     |
| `--fork`             |                     | read-only fork origin preparation                                   |
| `--backtest`         |                     | read-only historical preparation                                    |
| `--fork-key-file`    | Path                |                                                                     |
| `--env-file`         | Path                |                                                                     |
| `--accounts`         | Path                | operator-generated account bindings                                 |
| `--key-file`         | Path                |                                                                     |
| `--password-file`    | Path                |                                                                     |
| `--auth-token-file`  | Path; required      |                                                                     |
| `--port`             | int                 |                                                                     |
| `--listen-fd`        | int                 | inherited loopback socket, already bound by the supervising process |
| `--lease-seconds`    | float; default `30` |                                                                     |

Exactly one of `--config`, `--gateway-config`.

At most one of `--read-context`, `--preparation-file`.

Exactly one of `--fork-url`, `--fork-routes`, `--production`, `--fork`, `--backtest`.

Exactly one of `--port`, `--listen-fd`.

## almanak service read-gateway

read-only observation gateway holding the provider credentials

```
usage: almanak service read-gateway [-h] --env-file ENV_FILE --auth-token-file
                                    AUTH_TOKEN_FILE --port PORT
```

| Option              | Details        | Description                                            |
| ------------------- | -------------- | ------------------------------------------------------ |
| `--env-file`        | Path; required | private provider credentials read only by this gateway |
| `--auth-token-file` | Path; required |                                                        |
| `--port`            | int; required  |                                                        |

## almanak service start

```
usage: almanak service start [-h] --gateway-url GATEWAY_URL
                             --gateway-auth-file GATEWAY_AUTH_FILE --config
                             CONFIG [--export EXPORT]
                             [--clock-offset-file CLOCK_OFFSET_FILE]
                             [--decision-facts] --strategy STRATEGY
```

| Option                | Details        | Description                                                           |
| --------------------- | -------------- | --------------------------------------------------------------------- |
| `--gateway-url`       | required       |                                                                       |
| `--gateway-auth-file` | Path; required |                                                                       |
| `--config`            | Path; required |                                                                       |
| `--export`            | Path           | retain read-only dashboard input snapshots                            |
| `--clock-offset-file` | Path           | fork test clock: whole seconds added to wall time for decision timing |
| `--decision-facts`    |                | fork test run: log what each decision saw for the test verdict        |
| `--strategy`          | Path; required |                                                                       |

## almanak service resume

```
usage: almanak service resume [-h] --gateway-url GATEWAY_URL
                              --gateway-auth-file GATEWAY_AUTH_FILE
                              [--config CONFIG] [--export EXPORT]
                              [--clock-offset-file CLOCK_OFFSET_FILE]
                              [--decision-facts]
```

| Option                | Details        | Description                                                           |
| --------------------- | -------------- | --------------------------------------------------------------------- |
| `--gateway-url`       | required       |                                                                       |
| `--gateway-auth-file` | Path; required |                                                                       |
| `--config`            | Path           |                                                                       |
| `--export`            | Path           | retain read-only dashboard input snapshots                            |
| `--clock-offset-file` | Path           | fork test clock: whole seconds added to wall time for decision timing |
| `--decision-facts`    |                | fork test run: log what each decision saw for the test verdict        |

## almanak service status

```
usage: almanak service status [-h] [--gateway-url GATEWAY_URL]
                              [--gateway-auth-file GATEWAY_AUTH_FILE]
                              [--database DATABASE]
```

| Option                | Details | Description |
| --------------------- | ------- | ----------- |
| `--gateway-url`       |         |             |
| `--gateway-auth-file` | Path    |             |
| `--database`          | Path    |             |

## almanak service teardown

```
usage: almanak service teardown [-h] --gateway-url GATEWAY_URL
                                --gateway-auth-file GATEWAY_AUTH_FILE
```

| Option                | Details        | Description |
| --------------------- | -------------- | ----------- |
| `--gateway-url`       | required       |             |
| `--gateway-auth-file` | Path; required |             |

# almanak support

show declared connector and chain support

```
usage: almanak support [-h] [--connector CONNECTOR] [--chain CHAIN]
                       [--schema {strategy,backtest,dashboard} | --references]
                       [--format {text,json}] [--json]
```

| Option         | Details                                    | Description                                                                                                                 |
| -------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `--connector`  |                                            | filter by an installed canonical connector ID                                                                               |
| `--chain`      |                                            | filter by a strict CAIP-2 chain identity                                                                                    |
| `--schema`     | one of `strategy`, `backtest`, `dashboard` |                                                                                                                             |
| `--references` |                                            | list the installed read-only reference strategies with their connector, chain and path, filtered by --connector and --chain |
| `--format`     | one of `text`, `json`                      |                                                                                                                             |
| `--json`       |                                            |                                                                                                                             |

At most one of `--schema`, `--references`.

# almanak test

fork test tooling: operator file and one lifecycle

```
usage: almanak test [-h] {run,config} ...
```

## almanak test run

one supervised fork lifecycle: admission, decisions, teardown, verdict

```
usage: almanak test run [-h] [--config CONFIG] [--strategy STRATEGY]
                        --gateway-config GATEWAY_CONFIG --env-file ENV_FILE
                        --output OUTPUT [--decisions DECISIONS]
                        [--advance ADVANCE] [--through-exit] [--idle IDLE]
                        [--format {text,json}] [--json]
                        [directory]
```

| Option             | Details               | Description                                                                                                                                                           |
| ------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `directory`        | Path; default `.`     |                                                                                                                                                                       |
| `--config`         | Path                  |                                                                                                                                                                       |
| `--strategy`       | Path                  |                                                                                                                                                                       |
| `--gateway-config` | Path; required        | operator provider, account and execution settings                                                                                                                     |
| `--env-file`       | Path; required        | private provider credentials read only by the operator gateway                                                                                                        |
| `--output`         | Path; required        | new fork workspace directory                                                                                                                                          |
| `--decisions`      | positive; default `3` | most decisions before teardown                                                                                                                                        |
| `--advance`        | positive              | seconds of fork time between decisions (default: the decision interval; a shorter step is raised to reach the next due decision)                                      |
| `--through-exit`   |                       | keep deciding after the first entry, up to --decisions, so the strategy's own later intents and exit execute before teardown                                          |
| `--idle`           | positive; default `3` | with --through-exit, consecutive holds after the entry that end the decisions early (default 3); raise it, or --advance, when the strategy waits longer between steps |
| `--format`         | one of `text`, `json` |                                                                                                                                                                       |
| `--json`           |                       |                                                                                                                                                                       |

## almanak test config

generate a fork operator file from the author configuration

```
usage: almanak test config [-h] [--config CONFIG] [--strategy STRATEGY]
                           --env-file ENV_FILE [--block BLOCK]
                           [--output OUTPUT] [--force] [--format {text,json}]
                           [--json]
                           [directory]
```

| Option       | Details               | Description                                    |
| ------------ | --------------------- | ---------------------------------------------- |
| `directory`  | Path; default `.`     |                                                |
| `--config`   | Path                  |                                                |
| `--strategy` | Path                  |                                                |
| `--env-file` | Path; required        | private provider credentials                   |
| `--block`    | whole                 | anchor block (default: latest; one chain only) |
| `--output`   | Path                  | write the operator file here                   |
| `--force`    |                       | replace an existing --output                   |
| `--format`   | one of `text`, `json` |                                                |
| `--json`     |                       |                                                |

# almanak wallet

provision encrypted operator credentials

```
usage: almanak wallet [-h] {create} ...
```

## almanak wallet create

```
usage: almanak wallet create [-h] --output OUTPUT --account ACCOUNT
```

| Option      | Details        | Description |
| ----------- | -------------- | ----------- |
| `--output`  | Path; required |             |
| `--account` | required       |             |

# almanak

Define, check, run and explain a strategy. A bare almanak, and any command this CLI does not define, run the bundled Almanak Code assistant when the installed wheel carries a binary for this platform.

```
usage: almanak [-h]
               {check,run,status,pause,resume,teardown,shutdown,test,report,support,backtest,service,permissions,wallet,ax}
               ...
```

| Command               | Description                                             | Page                                                                            |
| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `almanak check`       | validate author inputs and selected-mode readiness      | [almanak-check](https://sdk.docs.almanak.co/cli/almanak-check.html)             |
| `almanak run`         | start a supervised fork or production workspace         | [almanak-run](https://sdk.docs.almanak.co/cli/almanak-run.html)                 |
| `almanak status`      | show retained run state and service availability        | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak pause`       | pause the coordinator; positions and services remain    | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak resume`      | resume captured execution through its original services | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak teardown`    | request and execute economic closure                    | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak shutdown`    | stop local services, preserving recorded evidence       | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak test`        | fork test tooling: operator file and one lifecycle      | [almanak-test](https://sdk.docs.almanak.co/cli/almanak-test.html)               |
| `almanak report`      | render recorded evidence without network access         | [almanak-report](https://sdk.docs.almanak.co/cli/almanak-report.html)           |
| `almanak support`     | show declared connector and chain support               | [almanak-support](https://sdk.docs.almanak.co/cli/almanak-support.html)         |
| `almanak backtest`    | prepare, run and reproduce historical simulations       | [almanak-backtest](https://sdk.docs.almanak.co/cli/almanak-backtest.html)       |
| `almanak service`     | advanced explicit process operation                     | [almanak-service](https://sdk.docs.almanak.co/cli/almanak-service.html)         |
| `almanak permissions` | generate or verify strategy-derived permissions         | [almanak-permissions](https://sdk.docs.almanak.co/cli/almanak-permissions.html) |
| `almanak wallet`      | provision encrypted operator credentials                | [almanak-wallet](https://sdk.docs.almanak.co/cli/almanak-wallet.html)           |
| `almanak ax`          | read-only protocol observations at a pinned block       | [almanak-ax](https://sdk.docs.almanak.co/cli/almanak-ax.html)                   |
# API Reference

# Execution API

For author code use the [public authoring guide](https://sdk.docs.almanak.co/authoring.html); for commands use the [operating guide](https://sdk.docs.almanak.co/operating.html). This page describes trusted integration contracts.

## Author configuration and support

A strategy owns `strategy.py` and one configuration file. The current [reference strategy catalog](https://github.com/almanak-co/sdk-v3/blob/main/strategies/README.md) selects explicit markets, assets, capital, timing and gas policies in that file. Operator account bindings, credentials and generated deployment files are runtime artifacts.

`connector_for(config).support_for(ChainId(...))` returns an immutable `ConnectorChainSupport`. `supported_chains(RunMode.FORK)`, `RunMode.PRODUCTION` and `RunMode.BACKTEST` enumerate static support. Aave and Uniswap V4 advertise Ethereum and Avalanche for fork and production execution; only Ethereum has historical models. Aave historical actions exclude borrowing and repayment. Historical start times constrain an existing model; an omitted time grants no additional market or data coverage.

Support records expose modes, typed `EconomicAction` values, account profiles, restrictions and historical model identity. Configuration validation and gateway operation admission independently check them. A declaration does not establish provider availability, wallet funding or installed Safe authority.

Configured Aave reserves identify the Spoke, Hub, reserve ID and CAIP asset. Admission verifies their observed relationship, reserve metadata and supported implementation bytecode. Proxy verification normalizes only the verified immutable administrator operand. Uniswap configuration identifies protocol infrastructure and a full PoolKey. Admission verifies infrastructure revisions, periphery relationships and initialized pool state. Asset and pool selection do not consult sample token or market lists. Historical implementation code reads require a captured proxy pointer from a configured contract at the same canonical block; that evidence grants no calls or storage access to the implementation. A Uniswap V4 hook is supported only with a reviewed profile pinned by code (Fables, Uniswap StablePairHook and PonsV2MemeHook instances; Fables and StablePairHook pools also admit LP, Pons pools only trading); other hooks and protocol revisions fail. A reviewed hook whose code later differs refuses new exposure and still admits exits. A native-coin pool trades with the wrapped native held by the fund, wrapped and unwrapped by the reviewed router within the swap; its liquidity is minted and closed with the wrapped native, which the reviewed PositionManager unwraps and wraps within the same call. Simulation and receipt accounting require standard ERC-20 balance effects; unexplained effects prevent further exposure. Historical admission retains its captured model and revision constraints; compatibility for live execution does not establish backtest coverage.

Uniswap capital may identify either pool currency. `target.funding_currency` and `target.counter_currency` preserve that orientation through quotes, inventory, completion and pool-marked reporting. NFT exits retain the [documented broader Roles grant](https://sdk.docs.almanak.co/internal/blueprints/15-uniswap-v4-lifecycles.html#protocol-and-authority).

## Production binding

Every reference binds through the operator configuration. `almanak run --production --gateway-config` reads the public author file with `configuration.unresolved.read_inputs`, resolves named markets through the read-only gateway and constructs authoritative parameters with the operator's account bindings before admission. The author retains economic gas preferences; production networks, payer ceilings, account profiles and credentials belong to the operator file. The SDK exposes no legacy author-loader or complete-config production fallback. Archived roots under `scripts/legacy_configuration/` serve only baseline and component evidence tooling.

Each binding has a name and an EOA or Safe/Roles profile. Its fund, submitter and policy retain full chain and environment scope. The prepared deployment freezes the author digest, resolved facts, account identities, strategy hooks and compatible author/operator gas policy before the execution gateway can start. The supervised run generates and verifies these deployment artifacts. See the [operating guide](https://sdk.docs.almanak.co/operating.html#production).

Gas policies explicitly select `base_fee_basis`: `observed_block` in the Avalanche samples and `next_block` for Ethereum. The retained quote has the corresponding `observed_base_fee` or `next_base_fee`, plus its pinned block and priority fee. Requested missing data fails; there is no automatic fee-basis or provider fallback. The policy multiplier and hard caps apply to that observation.

The resulting configuration contains production authority only. Fork funding and local signing keys do not transfer into it. The [production operator guide](https://github.com/almanak-co/sdk-v3/blob/main/strategies/aave_v4_weth_supply/PRODUCTION.md) describes encrypted credentials, `production-check`, gateway launch and runtime control. `production-check` reports each network separately. A configured URL provider uses only its named environment variable; there is no fallback provider.

## Chain boundary and gateway routes

`Chain` combines a validated CAIP-2 identity and explicit environment authority. New forks retain the origin CAIP identity and use an `anvil:<instance>` environment. `ForkBinding` retains the pinned origin and separate local signing chain `eip155:31337`. Avalanche native fees use `eip155:43114/slip44:9005`; wrapping uses the verified WAVAX mapping.

`ChainAdapter` owns network guards, checkpoints, submission references, preparation, signing, broadcast, receipts, reconciliation and execution costs. `CheckpointReference` and `TransactionReference` carry a chain and an opaque validated `ChainReference`. EVM references require canonical hashes and EVM checkpoints require block heights. Other families preserve case and delegate semantic validation to their adapters. EVM nonce, ABI, gas and signed-transaction fields stay in the EVM implementation and connector execution types.

One authenticated loopback gateway contains explicitly composed adapter routes. `ChainRouter` creates a route when accessed, so one unavailable network does not prevent another route from operating. Multi-chain gateway requests to `observe`, `audit`, `plan`, `sign`, `receipt`, `broadcast`, `reconcile` and execution `update` must include the complete `chain` object. Omitted or mismatched routes are refused. `GatewayClient` and the runtime's `ScopedGateway` provide those routes.

Use `gateway --fork-routes /private/deployment/fork-routes.json` for a generated fork collection. Its entries contain `chain` and loopback `endpoint`. The `fork` command starts one node per configured target, writes one deployment and routing file, and installs only generated Safe authority on the identified fixtures. Production routes come from `production.networks`. Both environments share protocol, account wrapping, simulation, receipt and accounting code.

## Durable work and strategy observations

A multi-chain checkpoint contains `work`, one record per configured chain, and a round-robin `scheduler_cursor`. Each record retains initialization, decision, plan, pending attempt, fault and closure. One economic intent may be active on each chain. The checkpoint has one configuration, strategy memory, outcome order and delivery cursor. Decisions and outcome delivery remain serialized.

Single-chain checkpoints retain their scalar execution slot, including previously recorded Ethereum fork identities and signed bytes. The runtime never rewrites a pending attempt into another signing domain. Chain-scoped updates are projected and independently validated under the same durable journal lock; updates that alter another chain's execution facts are refused.

`DecisionInput.availability` exposes `unobserved`, `ready`, `unavailable`, `invalidated` or `closed`, with retained fault information. A decision receives fresh observations for its selected chain. It must skip unavailable allocations. `Decision.required_observations` declares `(chain, account, allocation)` scopes. The runtime and gateway reject dependencies absent from the decision evidence; the connector additionally verifies its economic dependencies. The new reference strategies declare the observations used by each economic decision. No missing observation is interpreted as zero. Cross-chain dependent decisions have no combined observation acquisition workflow in this increment and fail closed.

Committed receipt canonicality is audited before advancing a route. Unknown attempts reconcile using their retained signed identity before new affected work. A chain-local outage or invalidated history blocks that chain; independent healthy entries and exits continue. A shared database, gateway, coordinator or strategy failure stops the runtime. Creating a Safe deployment requires all its permissions to be generated and verified; independent progress begins after that admission. Gateway replacement uses the same database and credentials.

Teardown persists at deployment scope. Each available chain repays, exits and verifies its remaining obligations independently. Overall `closed` requires all chains closed. Invalidated history remains halted for operator investigation; automatic reorganization recovery is unsupported. Reports retain per-chain work, readiness, balances, debt, permissions and payer gas. They do not sum unlike assets or infer a consolidated valuation source.

# Authoring API

Public contracts imported by `strategy.py`. External I/O remains gateway-owned.

Public strategy authoring contracts. External I/O remains gateway-owned.

## AssetAmount

Bases: `Model`

Nonnegative base-unit amount with explicit asset identity.

## DecisionContext

Bases: `Model`

### price

```
price(asset)
```

The declared series for one asset; a strategy declares at most one per asset.

Source code in `src/almanak/strategy_api.py`

```
def price(self, asset):
    """The declared series for one asset; a strategy declares at most one per asset."""
    matches = [series for series in self.prices if series.binding.asset == asset]
    if len(matches) != 1:
        raise ValueError(f"{asset}: no single declared strategy price series")
    return matches[0]
```

## PoolPrice

Bases: `PoolSource`

The pool's price at the last block at or before each candle boundary: closes only.

## PoolSwapCandles

Bases: `PoolSource`

OHLCV candles of the pool's trades between consecutive candle boundaries.

A candle opens at the previous boundary's pool price and closes at this one's; its high and low also cover every trade's price, and its volume sums the absolute quote-token amounts of those trades. A trade's price is the pool's resulting spot price (V3-style and V4 Swap events, Aerodrome Sync events) or, for Curve, its execution price including fees (TokenExchange events between the asset and quote). A candle without trades is flat with zero volume.

## PriceBinding

Bases: `Model`

### lookback_seconds

```
lookback_seconds: int
```

History before a decision that its window needs, beyond the latest point.

## PriceSeries

Bases: `Model`

### complete

```
complete: bool
```

The full window, evenly spaced, with its latest point within the source age.

### gaps

```
gaps: tuple[tuple[UnixTime, UnixTime], ...]
```

Consecutive points further apart than the timeframe.

### values

```
values: tuple[Decimal, ...]
```

The closes, for candles.

## StrategyState

Bases: `Memory`

Subclass as State inside strategy.py; typed fields are persisted by the worker.

# Configuration API

Public author inputs parse once into `AuthorInputs`; resolution binds them to an operator configuration and retained market facts before parameters exist.

The public author root, with connector-owned inputs and gateway-resolved facts.

These inputs are never strategy parameters. Supported connector bindings share this loader, which has no legacy-format fallback.

## InventoryInput

Bases: `Model`

Owned cash a composed root declares; never spending authority.

## author_field

```
author_field(location) -> str
```

A discriminated union inserts its connector tag, which the author never wrote.

Source code in `src/almanak/configuration/unresolved.py`

```
def author_field(location) -> str:
    """A discriminated union inserts its connector tag, which the author never wrote."""
    return ".".join(str(part) for part in location if str(part) not in TAGS)
```

## diagnostic

```
diagnostic(
    source: Document, error, prefix=""
) -> ConfigurationError
```

Locate a validation or configuration failure in the author's own file.

Source code in `src/almanak/configuration/unresolved.py`

```
def diagnostic(source: Document, error, prefix="") -> ConfigurationError:
    """Locate a validation or configuration failure in the author's own file."""
    field = error.field if isinstance(error, ConfigurationError) else ""
    message = str(error)
    if isinstance(error, ValidationError):
        detail = error.errors(include_input=False, include_url=False)[0]
        field = author_field(detail["loc"])
        message = detail["msg"]
    field = ".".join(part for part in (prefix, field) if part)
    return located(source, ConfigurationError(message, field=field))
```

## inject_connectors

```
inject_connectors(document: Document) -> dict
```

Positions take the connector tag from their named market; the author never writes it.

Source code in `src/almanak/configuration/unresolved.py`

```
def inject_connectors(document: Document) -> dict:
    """Positions take the connector tag from their named market; the author never writes it."""
    value = json.loads(json.dumps(document.value))
    markets = value.get("markets")
    strategy = value.get("strategy")
    if not isinstance(markets, dict) or not isinstance(strategy, dict):
        return value
    for group in ("allocations", "debts"):
        positions = strategy.get(group)
        for name, position in positions.items() if isinstance(positions, dict) else ():
            if isinstance(position, dict):
                tag = position_connector(position, markets, f"strategy.{group}.{name}")
                if tag is not None:
                    position["connector"] = tag
    return value
```

## public_root

```
public_root(value) -> bool
```

Public roots carry no execution block and identify every market chain by CAIP-2 text.

Source code in `src/almanak/configuration/unresolved.py`

```
def public_root(value) -> bool:
    """Public roots carry no execution block and identify every market chain by CAIP-2 text."""
    if not isinstance(value, dict) or "execution" in value:
        return False
    strategy = value.get("strategy")
    if isinstance(strategy, dict) and "connector" in strategy:
        return False
    markets = value.get("markets")
    return (
        isinstance(markets, dict)
        and bool(markets)
        and all(isinstance(m, dict) and isinstance(m.get("chain"), str) for m in markets.values())
    )
```

## require_bindings

```
require_bindings(document: Document)
```

Every market names a canonical connector that has a public author binding.

Source code in `src/almanak/configuration/unresolved.py`

```
def require_bindings(document: Document):
    """Every market names a canonical connector that has a public author binding."""
    markets = document.value.get("markets", {})
    if not isinstance(markets, dict):
        raise ConfigurationError("named markets must be a mapping", field="markets")
    for name, market in markets.items():
        if not isinstance(market, dict) or "connector" not in market:
            continue
        try:
            binding_for(require_connector(market["connector"]))
        except ConfigurationError as error:
            raise error.at(
                file=document.file,
                field=f"markets.{name}.connector",
                line=document.locations.get(f"markets.{name}.connector"),
            ) from error
```

## require_chain_support

```
require_chain_support(inputs: AuthorInputs)
```

Offline checks refuse a market chain its connector never declared, in any mode.

Source code in `src/almanak/configuration/unresolved.py`

```
def require_chain_support(inputs: AuthorInputs):
    """Offline checks refuse a market chain its connector never declared, in any mode."""
    from almanak.connectors.composition import connector_from_wire

    for name, market in inputs.markets.items():
        connector = connector_from_wire({"connector": str(market.connector)})
        try:
            connector.support_for(market.chain)
        except ValueError as error:
            raise ConfigurationError(str(error), field=f"markets.{name}.chain") from error
```

## unknown_reference

```
unknown_reference(kind, value, choices, field)
```

Report local-name typos without selecting a replacement automatically.

Source code in `src/almanak/configuration/unresolved.py`

```
def unknown_reference(kind, value, choices, field):
    """Report local-name typos without selecting a replacement automatically."""
    names = tuple(name for name in choices if isinstance(name, str))
    matches = get_close_matches(value, names, n=1) if isinstance(value, str) else []
    hint = f" Did you mean {matches[0]!r}?" if matches else ""
    available = ", ".join(names)
    return ConfigurationError(
        f"Unknown named {kind} {value!r}.{hint} Configured {kind}s: {available}.",
        field=field,
        code="unknown_named_reference",
        remedy=(
            f"Choose one of the configured {kind} names; suggestions never change the selection."
        ),
    )
```

Construct complete parameters only from matching, replay-checked resolution.

## admit_new_positions

```
admit_new_positions(document: InputDocument, result)
```

Preflight for a new run only: connector refusals that need the resolved reads.

Retained parameters (resume, exit) and backtests never pass through here, so a check added later cannot strand a deployment whose parameters are already frozen.

Source code in `src/almanak/configuration/parameters.py`

```
def admit_new_positions(document: InputDocument, result):
    """Preflight for a new run only: connector refusals that need the resolved reads.

    Retained parameters (resume, exit) and backtests never pass through here, so a check
    added later cannot strand a deployment whose parameters are already frozen.
    """
    for anchor in result.anchors.checkpoints:
        markets = tuple(m for m in result.markets if m.resolved.block.chain == anchor.chain)
        evidence = tuple(row for market in markets for row in market.evidence)
        with RetainedResolutionReader(anchor.chain, evidence) as reader:
            check_resolved_positions(document, reader, {m.name: m.resolved for m in markets})
```

## assemble

```
assemble(
    document, groups, accounts, metadata, gas
) -> Model
```

Build one authoritative parameter instance across all named markets.

Source code in `src/almanak/configuration/parameters.py`

```
def assemble(document, groups, accounts, metadata, gas) -> Model:
    """Build one authoritative parameter instance across all named markets."""
    if len(groups) > 1:
        by_name = {
            item.name: item
            for parts in groups.values()
            for group in GROUPS
            for item in parts[group]
        }
        names = (
            *document.inputs.strategy.allocations,
            *document.inputs.strategy.debts,
        )
        positions = tuple(by_name[name] for name in names)
        composed_values = {
            "connector": None,
            "allocations": positions,
            "inventory": resolved_inventory(document, accounts, metadata),
        }
        return validate_parameters(document, None, composed_values, accounts, metadata, gas)
    if document.inputs.inventory:
        raise document.error(
            ConfigurationError(
                "inventory requires positions from more than one connector", field="inventory"
            )
        )
    ((identity, parts),) = groups.items()
    values: dict[str, object] = {"allocations": tuple(parts["allocations"])}
    if identity == ConnectorId("aave_v4"):
        values["debts"] = tuple(parts["debts"])
    return validate_parameters(document, identity, values, accounts, metadata, gas)
```

## check_resolved_positions

```
check_resolved_positions(document, reader, markets)
```

Connector checks that need the market's resolved reads, not only its frozen facts.

Source code in `src/almanak/configuration/parameters.py`

```
def check_resolved_positions(document, reader, markets):
    """Connector checks that need the market's resolved reads, not only its frozen facts."""
    for group in GROUPS:
        for name, selection in getattr(document.inputs.strategy, group).items():
            check = getattr(selection, "check_resolved", None)
            if check is None or selection.market not in markets:
                continue
            try:
                check(reader, markets[selection.market])
            except ConfigurationError as error:
                raise document.error(error, f"strategy.{group}.{name}") from error
```

## fork_rescope

```
fork_rescope(groups, fixture)
```

Positions move to the generated fixture chains; targets keep their origin identity.

Source code in `src/almanak/configuration/parameters.py`

```
def fork_rescope(groups, fixture):
    """Positions move to the generated fixture chains; targets keep their origin identity."""
    from almanak.connectors.composed.fork import remap_allocation

    chains = {b.origin.chain: b.chain for b in fixture.bindings}
    return {
        identity: {
            group: [remap_allocation(item, chains[item.chain]) for item in items]
            for group, items in parts.items()
        }
        for identity, parts in groups.items()
    }
```

## grouped_positions

```
grouped_positions(
    document, markets
) -> dict[ConnectorId, dict[str, list]]
```

Resolved positions grouped by the connector of their named market.

Source code in `src/almanak/configuration/parameters.py`

```
def grouped_positions(document, markets) -> dict[ConnectorId, dict[str, list]]:
    """Resolved positions grouped by the connector of their named market."""
    inputs = document.inputs
    groups: dict[ConnectorId, dict[str, list]] = {}
    for group in GROUPS:
        for name, selection in getattr(inputs.strategy, group).items():
            identity = ConnectorId(inputs.markets[selection.market].connector)
            resolved = resolve_position(document, name, selection, markets, group)
            groups.setdefault(identity, {g: [] for g in GROUPS})[group].append(resolved)
    return groups
```

## resolved_inventory

```
resolved_inventory(document, accounts, metadata)
```

Resolve shared owned cash once, using verified precision and bound fund scope.

Source code in `src/almanak/configuration/parameters.py`

```
def resolved_inventory(document, accounts, metadata):
    """Resolve shared owned cash once, using verified precision and bound fund scope."""
    result = []
    for index, item in enumerate(document.inputs.inventory):
        try:
            binding = accounts.binding(item.account)
            result.append(
                Balance(
                    key=WalletAsset(
                        account=item.account,
                        chain=binding.profile.fund.chain,
                        asset=item.balance.asset,
                    ),
                    amount=resolve_quantity(
                        item.balance.quantity, metadata[item.balance.asset].decimals
                    ),
                )
            )
        except (ConfigurationError, KeyError, ValidationError) as error:
            raise document.error(error, f"inventory.{index}") from error
    return tuple(result)
```

## resolved_metadata

```
resolved_metadata(document, markets, symbols=None)
```

Verified precision per asset, named by the author or else by the token itself.

Source code in `src/almanak/configuration/parameters.py`

```
def resolved_metadata(document, markets, symbols=None):
    """Verified precision per asset, named by the author or else by the token itself."""
    symbols = symbols or {}
    metadata: dict[AssetType, AssetMetadata] = {}
    for market in markets.values():
        for asset in market.assets:
            previous = metadata.setdefault(
                asset.identifier,
                AssetMetadata(decimals=asset.decimals, symbol=symbols.get(asset.identifier)),
            )
            if previous.decimals != asset.decimals:
                raise document.error(
                    ConfigurationError("resolved asset precision is inconsistent", field="markets")
                )
    for account in document.inputs.accounts.values():
        identity, native = native_metadata(account.chain)
        metadata[identity] = native
    for asset, expected in document.inputs.assets.items():
        field = f"assets.{asset}"
        if asset not in metadata:
            raise document.error(
                ConfigurationError("asset metadata has no verified resolution", field=field)
            )
        if expected.decimals is not None and metadata[asset].decimals != expected.decimals:
            raise document.error(
                ConfigurationError(
                    "asset precision assertion differs from verified metadata",
                    field=field + ".decimals",
                )
            )
        metadata[asset] = AssetMetadata(
            decimals=metadata[asset].decimals, symbol=expected.symbol or metadata[asset].symbol
        )
    return metadata
```

Backtest inputs resolve against those same parameters and explicit operator provider bindings.

Public backtest settings: syntax-checked inputs that resolve with verified parameters.

Historical provider bindings come from the operator configuration, never from the author's backtest file, so retained runs carry no provider identity.

## BacktestInputs

Bases: `Model`

### anchors

```
anchors(
    inputs: AuthorInputs,
) -> tuple[AnchorSelection, ...]
```

One anchor per account chain: the last block at the window start.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def anchors(self, inputs: AuthorInputs) -> tuple[AnchorSelection, ...]:
    """One anchor per account chain: the last block at the window start."""
    chains = dict.fromkeys(account.chain for account in inputs.accounts.values())
    return tuple(AnchorSelection(chain=chain, at=self.window.start) for chain in chains)
```

## aave_wire

```
aave_wire(
    document: BacktestDocument, parameters, history
) -> dict
```

Aave V4 keeps the flat fund identity and models no separate gas payer.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def aave_wire(document: BacktestDocument, parameters, history) -> dict:
    """Aave V4 keeps the flat fund identity and models no separate gas payer."""
    refuse_payers(document)
    funds = document.inputs.inventory.funds
    return single_wire(
        document,
        history,
        [fund_wire(document, name, fund, parameters) for name, fund in funds.items()],
    )
```

## bind_vendor_access

```
bind_vendor_access(
    document: BacktestDocument,
    wire: dict,
    available: tuple[VendorBinding, ...],
)
```

Bind only author-selected vendors to operator-owned access settings.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def bind_vendor_access(
    document: BacktestDocument, wire: dict, available: tuple[VendorBinding, ...]
):
    """Bind only author-selected vendors to operator-owned access settings."""
    selected = selected_vendors(document.inputs.data)
    if not selected:
        return
    bindings = {binding.name: binding for binding in available}
    for field, name, kind in selected:
        binding = bindings.get(name)
        if binding is None:
            raise document.error(
                ConfigurationError(
                    f"operator configuration has no vendor binding named {name}",
                    field=f"{field}.name",
                )
            )
        if binding.provider.kind != kind:
            raise document.error(
                ConfigurationError(
                    f"operator vendor {name} is {binding.provider.kind}, not {kind}",
                    field=f"{field}.kind",
                )
            )
    inject_vendor_access(wire.get("data"), bindings)
    for model in wire.get("models", {}).values():
        inject_vendor_access(model.get("data"), bindings)
```

## composed_wire

```
composed_wire(
    document: BacktestDocument, parameters, history
) -> dict
```

A composed run pairs one data policy, one cost policy and one model per connector.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def composed_wire(document: BacktestDocument, parameters, history) -> dict:
    """A composed run pairs one data policy, one cost policy and one model per connector."""
    funds, payers = profiled_inventory(document, parameters)
    return {
        **head_wire(document, history),
        "funds": funds,
        "payers": payers,
        "models": composed_models(document),
    }
```

## connector_wire

```
connector_wire(
    document: BacktestDocument, parameters, history
) -> dict
```

Every other connector carries a simulation profile and its own gas payers.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def connector_wire(document: BacktestDocument, parameters, history) -> dict:
    """Every other connector carries a simulation profile and its own gas payers."""
    funds, payers = profiled_inventory(document, parameters)
    return single_wire(document, history, funds, payers)
```

## payer_wire

```
payer_wire(document, name, payer, parameters) -> dict
```

A payer keeps the same deterministic identity as a hypothetical fund.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def payer_wire(document, name, payer, parameters) -> dict:
    """A payer keeps the same deterministic identity as a hypothetical fund."""
    field = f"inventory.payers.{name}"
    return {
        "name": name,
        "fund": simulation_binding(name, payer.chain).profile.fund.model_dump(mode="json"),
        "balances": balances_wire(document, field, payer.balances, parameters),
    }
```

## refuse_payers

```
refuse_payers(document: BacktestDocument)
```

Aave V4 simulates one account identity, so its inputs carry no payer.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def refuse_payers(document: BacktestDocument):
    """Aave V4 simulates one account identity, so its inputs carry no payer."""
    inventory = document.inputs.inventory
    located_fields = [f"inventory.payers.{name}.chain" for name in inventory.payers]
    located_fields += [
        f"inventory.funds.{name}.payer" for name, fund in inventory.funds.items() if fund.payer
    ]
    if located_fields:
        raise document.error(
            ConfigurationError(
                "this connector models no separate gas payer", field=located_fields[0]
            )
        )
```

## refuse_strategy_prices

```
refuse_strategy_prices(
    document: Document, value, prefix="data"
)
```

Only the strategy declares a pool price series; a backtest never repeats one.

A single strategy price (an Aave V4 economic input) may still be listed here, and must then equal the strategy's declaration.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def refuse_strategy_prices(document: Document, value, prefix="data"):
    """Only the strategy declares a pool price series; a backtest never repeats one.

    A single strategy price (an Aave V4 economic input) may still be listed here, and
    must then equal the strategy's declaration.
    """
    if not isinstance(value, dict):
        return
    prices = value.get("prices")
    for index, binding in enumerate(prices if isinstance(prices, list) else ()):
        if isinstance(binding, dict) and binding.get("purpose") == "strategy":
            source = binding.get("source")
            kind = source.get("kind") if isinstance(source, dict) else None
            if kind not in ("pool_state", "pool_swaps") and binding.get("window", 1) == 1:
                continue
            raise located(
                document,
                ConfigurationError(
                    "strategy price series are declared by the strategy's data_dependencies "
                    "hook, not the backtest configuration; remove this entry",
                    field=f"{prefix}.prices.{index}.purpose",
                ),
            )
    connectors = value.get("connectors")
    if isinstance(connectors, dict):
        for name, policy in connectors.items():
            refuse_strategy_prices(document, policy, f"{prefix}.connectors.{name}")
```

## resolve_backtest

```
resolve_backtest(
    document: BacktestDocument,
    parameters: Model,
    history: tuple[HistoryBinding, ...],
    vendors: tuple[VendorBinding, ...] = (),
)
```

The settings wire of whichever connector the resolved parameters name.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def resolve_backtest(
    document: BacktestDocument,
    parameters: Model,
    history: tuple[HistoryBinding, ...],
    vendors: tuple[VendorBinding, ...] = (),
):
    """The settings wire of whichever connector the resolved parameters name."""
    from almanak.connectors.composition import connector_for

    builders = {"aave_v4": aave_wire, COMPOSED: composed_wire}
    builder = builders.get(parameter_tag(parameters), connector_wire)
    wire = builder(document, parameters, history)
    bind_vendor_access(document, wire, vendors)
    settings_type = connector_for(parameters).backtest(document.inputs.mode).settings_type
    try:
        return settings_type.model_validate_json(json.dumps(wire))
    except ValidationError as error:
        raise document.error(error) from error
```

Runtime artifacts use the resolved parameter codecs. Archived author fixtures are private evidence inputs under `scripts/legacy_configuration/`; they are not an SDK configuration API.

# Connector support

Connectors advertise immutable, typed per-chain support for execution modes, intents, account profiles and historical coverage. The runtime and the gateway enforce the same declaration; the generated [support page](https://sdk.docs.almanak.co/support.html) renders it.

Static connector support, independent of credentials and current market state.

## candidate_uniswap_support

```
candidate_uniswap_support()
```

Public V4 entries plus unregistered chains for private qualification tests only.

Source code in `src/almanak/connectors/support.py`

```
def candidate_uniswap_support():
    """Public V4 entries plus unregistered chains for private qualification tests only."""
    supports = uniswap_support()
    template = next(support for support in supports if support.chain == ChainId("eip155:1"))
    return (
        *supports,
        *(
            uniswap_candidate_entry(template, chain, start)
            for chain, start in UNISWAP_CANDIDATE_STARTS
        ),
    )
```

## uniswap_candidate_entry

```
uniswap_candidate_entry(template, chain, start)
```

Build one validated candidate entry from a public V4 entry's models and actions.

Source code in `src/almanak/connectors/support.py`

```
def uniswap_candidate_entry(template, chain, start):
    """Build one validated candidate entry from a public V4 entry's models and actions."""
    return ConnectorChainSupport.model_validate(
        {
            **template.model_dump(),
            "chain": ChainId(chain),
            "modes": (RunMode.FORK, RunMode.BACKTEST),
            "historical": tuple(
                {**historical.model_dump(), "backtest_start_at": start}
                for historical in template.historical
            ),
            "restrictions": (
                "candidate fork and backtest verification only; not promoted",
                *template.restrictions,
            ),
        }
    )
```

# Intents

Economic requests carry explicitly bound connector targets. Lending and exchange intents share the same admission, execution and reconciliation path.

## Lending

Economic lending requests with explicitly bound connector target types.

## LendingTarget

Bases: `Model`, `ABC`

### asset

```
asset: AssetType
```

Underlying asset from the connector's complete market identity.

## Exchange and liquidity

Economic exchange and liquidity intents with concrete connector targets.

## DexTarget

Bases: `Model`, `ABC`

### assets

```
assets: tuple[AssetType, ...]
```

The connector's explicitly selected market currencies.

## ExitExchange

Bases: `DexIntent[Target]`

Close owned liquidity, return to configured funding currency and clear authority.

## PoolLiquidityBudget

Bases: `DexIntent[Target]`

Pool-wide quantities for markets whose coin count is part of the target.

## RevokeExchange

Bases: `DexIntent[Target]`

Request economic closure of spending authority; the connector chooses calls.

## TickBounds

Bases: `Model`

One concrete concentrated-liquidity range, chosen inside an authorized band.

# Reports

Read-only projections for terminal, JSON and portable HTML reports.

Read-only projections for terminal, JSON and portable HTML reports.

## apply_effects

```
apply_effects(wallets, positions, effects)
```

Apply one transition's effects; True when the retained shape drifts.

Source code in `src/almanak/reports.py`

```
def apply_effects(wallets, positions, effects):
    """Apply one transition's effects; True when the retained shape drifts."""
    if not isinstance(effects, dict):
        return True
    drift = apply_asset_deltas(wallets, effects.get("assets", []))
    return apply_position_changes(positions, effects.get("positions", [])) or drift
```

## initial_portfolio

```
initial_portfolio(manifest)
```

Initial portfolio from the manifest's frozen initial state.

Falls back to the funds inventory (the same mapping `Fund.inventory` applies) when the initial state does not carry a portfolio; positions then start empty. Returns None when neither source is retained.

Source code in `src/almanak/reports.py`

```
def initial_portfolio(manifest):
    """Initial portfolio from the manifest's frozen initial state.

    Falls back to the funds inventory (the same mapping ``Fund.inventory``
    applies) when the initial state does not carry a portfolio; positions then
    start empty. Returns None when neither source is retained.
    """
    if not isinstance(manifest, dict):
        return None
    initial = manifest.get("initial")
    if isinstance(initial, dict) and isinstance(initial.get("portfolio"), dict):
        return initial["portfolio"]
    settings = manifest.get("settings")
    funds = settings.get("funds") if isinstance(settings, dict) else None
    if not isinstance(funds, list):
        return None
    balances = []
    for fund in funds:
        if not isinstance(fund, dict):
            return None
        account = fund.get("fund")
        if not isinstance(account, dict):
            return None
        for amount in fund.get("balances", []):
            balances.append(
                {
                    "key": {
                        "account": fund.get("name"),
                        "chain": account.get("chain"),
                        "asset": amount.get("asset"),
                    },
                    "amount": amount.get("amount"),
                }
            )
    return {"balances": balances, "positions": []}
```

## liquidation_effects

```
liquidation_effects(event)
```

Liquidation facts of a recorded event; input-only events carry none.

Source code in `src/almanak/reports.py`

```
def liquidation_effects(event):
    """Liquidation facts of a recorded event; input-only events carry none."""
    result = event.get("transition", {}).get("result", {})
    if not isinstance(result, dict) or result.get("kind") != "liquidation":
        return ()
    return tuple(result.get("liquidations", ()))
```

## portfolio_differs

```
portfolio_differs(wallets, positions, portfolio)
```

Self-check: replayed state against the checkpoint's final portfolio.

Source code in `src/almanak/reports.py`

```
def portfolio_differs(wallets, positions, portfolio):
    """Self-check: replayed state against the checkpoint's final portfolio."""
    expected_wallets = {
        series_identity(balance["key"]): balance.get("amount")
        for balance in portfolio.get("balances", [])
        if isinstance(balance, dict) and "key" in balance
    }
    if {identity: entry["amount"] for identity, entry in wallets.items()} != expected_wallets:
        return True
    expected_positions = {
        series_identity(position["key"]): position.get("payload")
        for position in portfolio.get("positions", [])
        if isinstance(position, dict) and "key" in position
    }
    return {
        identity: position.get("payload") for identity, position in positions.items()
    } != expected_positions
```

## replay_steps

```
replay_steps(wire)
```

(time, pending, effects) steps in engine application order.

Events rank before completed intents at equal times because the engine applies every event up to an intent's eligibility before executing it.

Source code in `src/almanak/reports.py`

```
def replay_steps(wire):
    """(time, pending, effects) steps in engine application order.

    Events rank before completed intents at equal times because the engine
    applies every event up to an intent's eligibility before executing it.
    """
    steps = [
        (event["time"], 0, None, (event.get("transition") or {}).get("effects"))
        for event in wire.get("events", [])
        if isinstance(event.get("time"), int)
    ]
    steps += [
        (
            entry["pending"]["eligible_at"],
            1,
            entry["pending"],
            (entry.get("transition") or {}).get("effects"),
        )
        for entry in wire.get("completed", [])
        if isinstance(entry.get("pending"), dict)
        and isinstance(entry["pending"].get("eligible_at"), int)
    ]
    steps.sort(key=lambda step: (step[0], step[1]))
    return [(time, pending, effects) for time, _, pending, effects in steps]
```

## series_identity

```
series_identity(key)
```

Stable identity for a wallet asset or position key: its full sorted JSON.

Source code in `src/almanak/reports.py`

```
def series_identity(key):
    """Stable identity for a wallet asset or position key: its full sorted JSON."""
    return json.dumps(key, sort_keys=True, separators=(",", ":"))
```

## simulation_activity

```
simulation_activity(wire)
```

Project completed intents and liquidation events into one chronological list.

Rows sort by effective simulation time, ascending: `eligible_at` for completed intents and the recorded event time for liquidations. Completed intents precede events recorded at equal times (stable sort over intents-first input).

Source code in `src/almanak/reports.py`

```
def simulation_activity(wire):
    """Project completed intents and liquidation events into one chronological list.

    Rows sort by effective simulation time, ascending: ``eligible_at`` for completed
    intents and the recorded event time for liquidations. Completed intents precede
    events recorded at equal times (stable sort over intents-first input).
    """
    timed = []
    for entry in wire.get("completed", []):
        pending = entry["pending"]
        timed.append(
            (
                pending["eligible_at"],
                {
                    "action": pending["intent"]["kind"],
                    "scope": pending["intent"].get("target"),
                    "decision_time": pending.get("decision_time"),
                    "eligible_at": pending["eligible_at"],
                    "complete": entry["transition"].get("complete", True),
                    "asset_changes": [
                        string_amount(delta)
                        for delta in entry["transition"].get("effects", {}).get("assets", [])
                    ],
                },
            )
        )
    for event in wire.get("events", []):
        for effect in liquidation_effects(event):
            timed.append((event["time"], liquidation_row(event, effect)))
    timed.sort(key=lambda pair: pair[0])
    return [row for _, row in timed]
```

## simulation_series

```
simulation_series(wire, manifest=None)
```

Replay retained effects into a per-step portfolio series in token units.

The initial point carries the manifest's frozen initial portfolio at the window start. Each completed intent adds a step point after its effects apply; connector event effects (for example liquidations) fold into the running state without their own points, and events precede intents recorded at equal times, matching engine application order. The final point carries the checkpoint portfolio at the checkpoint time. Replay self-checks against that final portfolio: schema drift or unknown shapes mark the series `complete: false` instead of failing. Zero-amount wallet entries are kept, since they mark exited assets. Wallet amounts serialize as decimal strings; JSON numbers cannot hold base units exactly. Position payloads pass through verbatim; no revaluation happens here.

Source code in `src/almanak/reports.py`

```
def simulation_series(wire, manifest=None):
    """Replay retained effects into a per-step portfolio series in token units.

    The initial point carries the manifest's frozen initial portfolio at the
    window start. Each completed intent adds a step point after its effects
    apply; connector event effects (for example liquidations) fold into the
    running state without their own points, and events precede intents recorded
    at equal times, matching engine application order. The final point carries
    the checkpoint portfolio at the checkpoint time. Replay self-checks against
    that final portfolio: schema drift or unknown shapes mark the series
    ``complete: false`` instead of failing. Zero-amount wallet entries are
    kept, since they mark exited assets. Wallet amounts serialize as decimal
    strings; JSON numbers cannot hold base units exactly. Position payloads
    pass through verbatim; no revaluation happens here.
    """
    initial = initial_portfolio(manifest)
    schedule = wire.get("schedule")
    start = schedule.get("start") if isinstance(schedule, dict) else None
    complete = initial is not None and start is not None
    points = []
    wallets: dict[str, dict] = {}
    positions: dict[str, dict] = {}
    if complete:
        seed_state(wallets, positions, initial)
        points.append(series_point(start, "initial", wallets, positions))
        for time, pending, effects in replay_steps(wire):
            if apply_effects(wallets, positions, effects):
                complete = False
            if pending is not None:
                points.append(series_point(time, "step", wallets, positions, pending))
    portfolio = wire.get("portfolio")
    time = wire.get("time")
    if isinstance(portfolio, dict) and time is not None:
        if complete and portfolio_differs(wallets, positions, portfolio):
            complete = False
        points.append(final_point(time, portfolio))
    return {"complete": complete, "points": points}
```

## string_amount

```
string_amount(delta)
```

Base-unit amounts serialize as decimal strings; JSON numbers cannot hold them exactly.

Source code in `src/almanak/reports.py`

```
def string_amount(delta):
    """Base-unit amounts serialize as decimal strings; JSON numbers cannot hold them exactly."""
    if isinstance(delta, dict) and isinstance(delta.get("amount"), int):
        return {**delta, "amount": str(delta["amount"])}
    return delta
```

## support_filters

```
support_filters(connector=None, chain=None)
```

Validate the --connector and --chain filters as canonical identities.

Source code in `src/almanak/reports.py`

```
def support_filters(connector=None, chain=None):
    """Validate the --connector and --chain filters as canonical identities."""
    from almanak.caip import ChainId
    from almanak.configuration.selection import require_connector
    from almanak.configuration.yaml import ConfigurationError

    selected = (
        require_connector(connector, field="--connector", file="<command line>")
        if connector is not None
        else None
    )
    try:
        selected_chain = ChainId(chain) if chain is not None else None
    except ValueError as error:
        raise ConfigurationError(str(error), file="<command line>", field="--chain") from error
    return selected, selected_chain
```

# JSON schemas

The public strategy and backtest roots compose connector-specific market and position definitions. Economic and historical model schemas remain separate. These schemas are published with the site and versioned with the package.

- [aave-backtest.schema.json](https://sdk.docs.almanak.co/schemas/aave-backtest.schema.json)
- [aave-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/aave-v3-economic.schema.json)
- [aerodrome-economic.schema.json](https://sdk.docs.almanak.co/schemas/aerodrome-economic.schema.json)
- [aerodrome-slipstream-economic.schema.json](https://sdk.docs.almanak.co/schemas/aerodrome-slipstream-economic.schema.json)
- [backtest.schema.json](https://sdk.docs.almanak.co/schemas/backtest.schema.json)
- [composed-economic.schema.json](https://sdk.docs.almanak.co/schemas/composed-economic.schema.json)
- [compound-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/compound-v3-economic.schema.json)
- [curve-economic.schema.json](https://sdk.docs.almanak.co/schemas/curve-economic.schema.json)
- [gmx-v2-economic.schema.json](https://sdk.docs.almanak.co/schemas/gmx-v2-economic.schema.json)
- [markets.schema.json](https://sdk.docs.almanak.co/schemas/markets.schema.json)
- [markets/aave_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/aave_v3.schema.json)
- [markets/aave_v4.schema.json](https://sdk.docs.almanak.co/schemas/markets/aave_v4.schema.json)
- [markets/aerodrome.schema.json](https://sdk.docs.almanak.co/schemas/markets/aerodrome.schema.json)
- [markets/aerodrome_slipstream.schema.json](https://sdk.docs.almanak.co/schemas/markets/aerodrome_slipstream.schema.json)
- [markets/compound_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/compound_v3.schema.json)
- [markets/curve.schema.json](https://sdk.docs.almanak.co/schemas/markets/curve.schema.json)
- [markets/gmx_v2.schema.json](https://sdk.docs.almanak.co/schemas/markets/gmx_v2.schema.json)
- [markets/morpho_blue.schema.json](https://sdk.docs.almanak.co/schemas/markets/morpho_blue.schema.json)
- [markets/pancakeswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/pancakeswap_v3.schema.json)
- [markets/sparklend.schema.json](https://sdk.docs.almanak.co/schemas/markets/sparklend.schema.json)
- [markets/uniswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/uniswap_v3.schema.json)
- [markets/uniswap_v4.schema.json](https://sdk.docs.almanak.co/schemas/markets/uniswap_v4.schema.json)
- [morpho-blue-economic.schema.json](https://sdk.docs.almanak.co/schemas/morpho-blue-economic.schema.json)
- [pancakeswap-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/pancakeswap-v3-economic.schema.json)
- [positions.schema.json](https://sdk.docs.almanak.co/schemas/positions.schema.json)
- [positions/aave_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/aave_v3.schema.json)
- [positions/aave_v4.schema.json](https://sdk.docs.almanak.co/schemas/positions/aave_v4.schema.json)
- [positions/aave_v4_debt.schema.json](https://sdk.docs.almanak.co/schemas/positions/aave_v4_debt.schema.json)
- [positions/aerodrome.schema.json](https://sdk.docs.almanak.co/schemas/positions/aerodrome.schema.json)
- [positions/aerodrome_slipstream.schema.json](https://sdk.docs.almanak.co/schemas/positions/aerodrome_slipstream.schema.json)
- [positions/compound_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/compound_v3.schema.json)
- [positions/curve.schema.json](https://sdk.docs.almanak.co/schemas/positions/curve.schema.json)
- [positions/gmx_v2.schema.json](https://sdk.docs.almanak.co/schemas/positions/gmx_v2.schema.json)
- [positions/morpho_blue.schema.json](https://sdk.docs.almanak.co/schemas/positions/morpho_blue.schema.json)
- [positions/pancakeswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/pancakeswap_v3.schema.json)
- [positions/sparklend.schema.json](https://sdk.docs.almanak.co/schemas/positions/sparklend.schema.json)
- [positions/uniswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/uniswap_v3.schema.json)
- [positions/uniswap_v4.schema.json](https://sdk.docs.almanak.co/schemas/positions/uniswap_v4.schema.json)
- [sparklend-economic.schema.json](https://sdk.docs.almanak.co/schemas/sparklend-economic.schema.json)
- [strategy.schema.json](https://sdk.docs.almanak.co/schemas/strategy.schema.json)
- [uniswap-economic.schema.json](https://sdk.docs.almanak.co/schemas/uniswap-economic.schema.json)
- [uniswap-historical.schema.json](https://sdk.docs.almanak.co/schemas/uniswap-historical.schema.json)
- [uniswap-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/uniswap-v3-economic.schema.json)

# Strategy state

Decision and memory contracts shared by economic families and run modes.

Decision and memory contracts shared by economic families and run modes.

## DecisionContext

Bases: `Model`

### price

```
price(asset)
```

The declared series for one asset; a strategy declares at most one per asset.

Source code in `src/almanak/strategy_api.py`

```
def price(self, asset):
    """The declared series for one asset; a strategy declares at most one per asset."""
    matches = [series for series in self.prices if series.binding.asset == asset]
    if len(matches) != 1:
        raise ValueError(f"{asset}: no single declared strategy price series")
    return matches[0]
```

## StrategyState

Bases: `Memory`

Subclass as State inside strategy.py; typed fields are persisted by the worker.
