<!-- Generated by `npm run skills` in apps/web from packages/build-sdk/README.md, index.d.mts and math.d.mts. Edit that, not this file. -->

# @ftw/sdk

The SDK for ftw.trade: hosted TypeScript automations and sources (D1181), and
a typed client for calling ftw.trade from your own code (below). An
automation belongs to a project. It is written in the project's
Automations panel, or saved by an AI through the `save_automation` MCP tool,
and ftw.trade runs it.

It was called `@ftw/build` before; code that imports `@ftw/build` still runs,
as the sandbox resolves both names to the same SDK.

```ts
import { automation } from '@ftw/sdk';

export default automation({
  name: 'Dip buyer',
  triggers: [{ pool: 'base:0x…', below: 0.0021 }, { every: '5m' }],
  policy: { wallets: ['w1'], maxEthPerTrade: 0.05, maxEthPerDay: 0.2 },
  async run(ctx, event) {
    if (event.type !== 'price') return;
    const fill = await ctx.wallets.get('w1').buy({ token: '0x…', eth: 0.05 });
    ctx.log.info('bought the dip', fill);
    ctx.metric('price', event.price);
  },
});
```

## Exact integer math

`math` is the same dependency-free implementation in Node and the hosted
QuickJS sandbox. Constructors and arithmetic return canonical decimal strings,
so results can cross action, source, memory and JSON boundaries without
serializing a native `bigint`.

```ts
import { math } from '@ftw/sdk';

const amount = math.u256Units('1.000000000000000001', 18);
const minimum = math.u256MulDiv(amount, 9970, 10000);
const signed = math.i64Add('-9223372036854775808', 1);
console.log(math.u256Format(minimum, 18), signed);
```

`@ftw/sdk/math` also exports `math`. `u256`/`i64` accept exact decimal strings,
native bigint values, or safe integer numbers; `u256` also accepts hexadecimal
strings. Unsafe numbers refuse before arithmetic. Invalid TypeScript inputs
throw `TypeError` or `RangeError`; FlowScript's equivalent math produces `na`.
A Number is already floating point when a helper receives it; the safe-number
check cannot recover digits lost in its literal or earlier calculations. Use
text or bigint when exact decimal input matters.

- Checked arithmetic: `u256Add/Sub/Mul/Div/Mod/Min/Max/Cmp` and
  `i64Add/Sub/Mul/Div/Mod/Neg/Abs/Min/Max/Cmp`.
- `u256DivUp`, `u256MulDiv` and `u256MulDivUp` provide explicit unsigned
  rounding. Multiply-divide retains the full product before checking the
  final uint256 result. Signed division truncates toward zero.
- `u256Units` and `u256Format` parse and format exact decimal amounts with
  0–77 decimal places. Subunit remainders and exponent strings refuse.
- `u256Number`/`i64Number` explicitly approximate for display.
  `u256SafeNumber`/`i64SafeNumber` refuse values outside the safe integer range.

Use the math functions and `Cmp` for integer arithmetic and ordering. The
returned strings keep ordinary JavaScript string operators. Convert a native
bigint with a constructor before publishing it; raw bigint remains invalid
in action, source and durable-memory JSON.

- **Where it runs.** QuickJS in WebAssembly, in a fresh runtime for each run:
  no Node, filesystem, timers or network, except `ctx.fetch` to hosts listed
  in `policy.egress`. Each run gets 2 s of CPU, 30 s in all, and 32 MB.
- **Wallets are handles.** The code never sees a key. A buy or sell is
  checked against the policy approved at deploy, then fills through the
  project, on paper or live as the project is.
- **Versions.** Every save is a new immutable version. Deploying picks one,
  rolling back deploys an earlier one, and every run records its version.
- **Monitoring.** Every run, log line, trade, refusal and emitted row is
  kept, and five failed runs in a row halt the automation with the reason.

- **Bridges (D1240).** `wallet.bridge({ from, to, token, amount, toToken? })`
  moves funds between chains: build asks every bridge that carries the move
  (Relay, Circle CCTP) and sends the route that lands the most, soon enough.
  `prefer: 'cheapest' | 'fastest'` changes the pick and `via` names one
  bridge. The policy caps each move by token (`bridge: { usdc: 500 }`); a
  token it does not name is refused.

- **Signals.** An automation can declare `signals` and publish them with
  `ctx.signal(name, fields)`. A workflow armed with the trigger
  `{ kind: "signal", signal }`, or another automation with
  `{ signal }`, then runs on each one, with its fields. A signal agent
  watches a source through `ctx.fetch` (an API key set as an account secret
  and sent as `{{secrets.NAME}}` in a header, never seen by the code) and
  turns what it sees into signals: "the account followed this token on this
  chain".

- **Sources (D1232).** `source({ name, fields, every, poll })` is a typed
  stream: `poll` returns an event's fields (or a list of them), build checks
  each against `fields` and publishes it. Kinds are text, number, boolean,
  address, chain, pool, hash, url and json; `t.address()` and friends write
  them, `t.url({ optional: true })` an optional one. It polls as often as
  every 1 s. A source an outside service feeds is declared on the Sources
  page instead, and pushed to with its own key.

```ts
import { source, t } from '@ftw/sdk';

export default source({
  name: 'kol-calls',
  description: 'A contract address an account we follow posted',
  fields: { token: t.address(), chain: t.chain(), handle: t.text(), post: t.url({ optional: true }) },
  every: '5s',
  policy: { egress: ['api.x.com'], secrets: ['X_BEARER'] },
  async poll(ctx) {
    const r = await ctx.fetch('https://api.x.com/2/users/123/tweets?max_results=5', { headers: { authorization: 'Bearer {{secrets.X_BEARER}}' } });
    const posts = ((r.json as { data?: { id: string; text: string }[] })?.data ?? []);
    return posts.flatMap((p) => {
      const ca = /0x[0-9a-fA-F]{40}/.exec(p.text)?.[0];
      return ca ? [{ token: ca, chain: 'base', handle: 'someone', post: `https://x.com/i/status/${p.id}` }] : [];
    });
  },
});
```

A source can be a sum type (D1236): `cases` names each kind of event with
its own fields, and every event says which it is in `case`. A strategy
reading it matches on the tag and must cover every case.

```ts
export default source({
  name: 'kol',
  fields: { handle: t.text() },
  cases: {
    call: { token: t.address(), chain: t.chain() },
    exit: { token: t.address() },
    note: { text: t.text() },
  },
  every: '5s',
  async poll(ctx) {
    return [{ case: 'call', handle: 'someone', token: '0x…', chain: 'base' }];
  },
});
```

In a strategy: `sum(window(map(sources.kol(), (e) => match (e.case) { call => 1, exit => -1, note => 0 }), "1h"))`.

- **Pools.** `pool(chain, address)` writes a checked pool key
  (`"base:0x…"` lowercase, a V4 pool id, or `"solana:<base58>"`) and
  `parsePool(key)` reads one back. Beside `ctx.pools.price`, `candles` and
  `swaps`, `ctx.pools.get(pool)` is the pool as the Pools page shows it with
  its day's stats, and `ctx.pools.liquidity(pool)` its liquidity over time, or
  by price with `{ view: 'by_price' }`, for a pool indexed with track
  liquidity. `wallet.quoteBridge(move)` ranks every route a `bridge` would
  take and sends nothing, so code can check the cost before it moves funds.

```ts
import { automation, pool } from '@ftw/sdk';

const WETH_USDC = pool('base', '0xd0b53D9277642d899DF5C87A3966A349A798F224');

export default automation({
  name: 'Thin-book guard',
  triggers: [{ every: '5m' }],
  async run(ctx) {
    const info = await ctx.pools.get(WETH_USDC);
    const quote = await ctx.wallets.get('w1').quoteBridge({ from: 'base', to: 'arbitrum', token: 'usdc', amount: 100 });
    ctx.metric('volume24h', info?.stats.volume24h ?? 0);
    ctx.metric('bridge_cost_usd', quote.plan.best.feeUsd ?? 0);
  },
});
```

## Actions (D1267)

`action()` is a script other scripts call by name, with typed args, for what
the built-ins don't do: post to a chat, call an exchange, run a keeper.

```ts
import { action, t } from '@ftw/sdk';

export default action({
  name: 'telegram-post',
  args: { chat: t.text(), text: t.text() },
  returns: { messageId: t.number() },
  timeout: '10s', // 1 s to 30 s
  retries: 1, // after a timeout or an error, with backoff; only for work safe to repeat
  stub: { messageId: 0 }, // a caller's test run gets this; a real call never does
  policy: { egress: ['api.telegram.org'], secrets: ['TG_BOT'] },
  async run(ctx, args) {
    const r = await ctx.fetch('https://api.telegram.org/bot{{secrets.TG_BOT}}/sendMessage', { method: 'POST', body: { chat_id: args.chat, text: args.text } });
    const body = r.json as { ok?: boolean; result?: { message_id: number } };
    if (!body?.ok || !body.result) throw new Error(`Telegram answered ${r.status}`);
    return { messageId: body.result.message_id };
  },
});
```

A caller names the action in `policy.actions` and calls it. **A call never
throws**: it answers `{ ok: true, value }` or `{ ok: false, kind, error }`,
so the types make you handle both.

```ts
export default automation({
  policy: { actions: ['telegram-post'] },
  async run(ctx) {
    const r = await ctx.actions.call<{ messageId: number }>('telegram-post', { chat: '42', text: 'filled' }, { timeoutMs: 5000 });
    if (!r.ok) ctx.log.warn(`not posted (${r.kind}): ${r.error}`);
  },
});
```

- `kind` is `timeout`, `error` (it threw), `refused` (a policy or a limit,
  no such action, or it is stopped or halted) or `invalid` (args, or its
  return, off the contract). A wrong return shape is not retried.
- Args are checked before the action runs and its return against `returns`
  after, so `value` is what the contract says.
- `ctx.actions.send(name, args)` starts it and answers `{ sent }` at once.
- An action runs under its own policy, approved when it is deployed; each
  try is a run of the action, with its log. A project has one deployed
  action of each name. `run_action` (MCP) and `ftw.action()` call one from
  outside.

## Execution memory

`ctx.memory` keeps small private JSON values for an automation or action across
runs, process restarts and code versions. Paper and live have separate stores.
The first lookup loads a snapshot; further lookups use that run's local copy.
Await every write: its receipt acknowledges a database commit. A failed durable
host write stops the run, and conflicts are refused without a hidden retry.
Later effect calls wait for memory writes already called, including parallel
calls. Local malformed JSON/key requests fail before queue admission and may
be caught and corrected. Effects already dispatched still require outcome
reconciliation.

```ts
import { action, t } from '@ftw/sdk';

export default action({
  name: 'record-wallet-checkpoint',
  args: { wallet: t.text(), cursor: t.text() },
  returns: { revision: t.number(), durable: t.boolean() },
  retries: 0,
  async run(ctx, args) {
    const key = `wallet:${args.wallet}`;
    const previous = await ctx.memory.get<{ cursor: string }>(key);
    ctx.log.info('previous checkpoint', previous);
    return await ctx.memory.set(key, { cursor: args.cursor });
  },
});
```

`get(key)` returns `{ value, revision }` or `null` when absent; `delete(key)`
commits a deletion; `keys(prefix?)` and `stats()` inspect the local snapshot.
The namespace permits 64 KiB, 256 keys, 128 UTF-8 bytes per key, and 16 KiB
per finite JSON value. Isolated previews start empty and return
`durable: false`. `ftw.getAutomationMemory({ id, mode: 'paper' })` or the
`get_automation_memory` MCP tool reads committed values without running code.

A stored marker records application context; confirm trade settlement with
the venue. Signal-derived state belongs in FlowScript folds and checkpoints.
To feed execution memory into a strategy, publish a committed versioned
source event so a backtest can replay the same input. The evaluator does no
storage reads on its evaluation path. Use the secret store for credentials.

## From your repo: `ftw push`

The `ftw-trade` CLI (packages/ftw-trade, `npx ftw-trade`) has the same
`push` and `call`, and signs in, runs strategies and reads the log too.

Keep sources, automations and actions as files in your own repo, and push
them to a project. Each push is a new version of the project's script with
the same name, a new script when there is none, and nothing when the code
has not changed.

```sh
export FTW_TOKEN=fwb_…            # from the AI & API page
ftw push scripts/*.ts --project p_123 --deploy
ftw call telegram-post '{"chat":"42","text":"hi"}' --project p_123
```

`--live` approves live trades when deploying in a live project; `--url`
points at another ftw.trade. From code, `ftw.push({ project, source,
deploy })` does the same.

## The client: ftw.trade from your own code (D1264)

`@ftw/sdk/client` runs anywhere with `fetch` (Node 18+, Bun, Deno, a
browser, a Worker), outside the sandbox. It holds an API token from the AI &
API page and calls the same tools an AI connected to ftw.trade does, so
anything the page or an AI can do, a script can: projects, wallets,
strategies, backtests, running strategies, pools, sources, signals, bridges
and perps.

```ts
import { Ftw, FtwError } from '@ftw/sdk/client';

const ftw = new Ftw({ token: process.env.FTW_TOKEN! });

const { projects } = await ftw.listProjects();
const { strategies } = await ftw.listStrategies();
const report = await ftw.backtestStrategy({ id: strategies[0].id, series: 'pool:base:0x…', interval: '1h' });
console.log(report.stats.returnPct, report.stats.maxDrawdownPct);

try {
  await ftw.runStrategy({ id: strategies[0].id, series: 'pool:base:0x…', interval: '1h', account: { project: projects[0].id, ethPerBuy: 0.01 } });
} catch (e) {
  if (e instanceof FtwError) console.error(e.status, e.field, e.message);
}
```

- **Every tool is a method**, named for it: `list_projects` is
  `listProjects`, `get_bridge_quote` is `getBridgeQuote`. Its arguments are
  typed from the tool's own schema, and its description is the method's doc
  comment. `ftw.call('tool_name', args)` reaches a tool newer than this
  package, and `ftw.tools()` lists what the server has now.
- **Results.** Projects, strategies, backtests and running strategies come
  back typed (`ProjectSummary`, `Project`, `Strategy`, `BacktestReport`,
  `BacktestRun`, `Deployment`); every other tool answers JSON, as its
  description says.
- **Refusals** throw `FtwError` with `status` (400 a bad argument, 401 the
  token, 403 not allowed, 404, 409, 422), `field` when one argument is to
  blame, and `tool`.
- **Live funds.** A token trades live only if it was made allowed to; without
  that, a call that would send a live trade is refused with 403. Paper is
  never gated. `TOOLS[name].trades` says which tools can move funds.
- **Another host.** `new Ftw({ token, url: 'http://localhost:3000' })` for a
  local app; pass `fetch` to use your own.

`tools.mjs` and `tools.d.mts` are generated from the tool list by
`apps/web/scripts/sdk-tools.ts`. After changing a tool, run it in
`apps/web`:

```sh
node --import ./tests/resolve-ts.mjs scripts/sdk-tools.ts
```

`index.d.mts` is the whole sandbox surface. `index.mjs` exists so the file imports
in an editor; the hosted runtime supplies its own module.


## Durable ingestion from your own NATS JetStream

`@ftw/sdk/nats` runs in a worker you own. The SDK has no runtime dependency on
NATS: pass an `open()` factory returning an official NATS connection and a
JetStream manager bound to that connection. The worker owns and closes those
connections. Configure TLS, authentication and broker permissions in that factory;
ftw.trade receives only a pushed-source key and typed events. The platform's
internal NATS token is never a user credential.

The SDK is currently distributed from this repository, not a published npm
release. Install a copy into your worker's project so Node resolves its official
NATS dependencies there:

```sh
npm install --install-links /path/to/flywheel/packages/build-sdk @nats-io/transport-node@3.4.0 @nats-io/jetstream@3.4.0
```

The versioned example is `node_modules/@ftw/sdk/examples/nats-source.mjs`.
First create a pushed source with the exact fields your messages contain. Set
`FTW_SOURCE`, `NATS_BROKER` (a stable namespace label), `NATS_URL`, `NATS_STREAM`,
`NATS_DOMAIN` if used, and either `NATS_CREDS_FILE` or `NATS_USER` with
`NATS_PASSWORD`. `NATS_CA_FILE` supplies a private TLS CA. Broker credentials stay
in that worker. The account must permit its own `$SYS.REQ.USER.INFO`, JetStream
account and stream info, creation/deletion of the worker's consumers, pulls and
acknowledgments. If account identity cannot be read, ingestion refuses it.

For registration, set `FTW_API_TOKEN` to an account controller key and
`NATS_START_AFTER` to an explicit last sequence to exclude (`0` starts at sequence
one). Run the example with `register`; it inspects the actual account, domain,
stream incarnation and complete stream configuration, then calls
`create_source_ingress`. Registration does not make the source public. Record its
returned ID as `FTW_INGRESS_ID`, set `FTW_SOURCE_KEY` to the source's own `fws_`
key, and run the same example with `run`. `FTW_URL` selects an HTTPS ftw.trade
origin, or a loopback HTTP origin for local development. Live input control
requires uncapped live controller authority; publication uses only the source
key. Rotating that key preserves the producer and cursor; deleting/recreating the
source or registering a new origin never resumes the old identity automatically.

V1 accepts a plain owned Limits stream, no mirrors or sourced streams, with
`max_msg_size` explicitly set to 1–8192 bytes. Its dedicated consumer is
unfiltered and uses explicit, confirmed acknowledgments. It never adopts an
existing consumer or computes a cursor from maximum delivered sequence. Legacy
eight-token ACK subjects omit their domain; the SDK checks the actual session
domain through account info and checks any explicit ACK domain when provided. Every
message must decode synchronously to `{ fields, observedAt? }`; filtering or
silently discarding messages is refused. The example treats each JSON payload as
`fields`. A custom decoder's SHA-256 must cover its exact artifact, dependencies
and configuration. `broker` is your namespace declaration, not cryptographic
broker attestation; account/domain/creation/configuration are checked against the
connected broker. Stream configuration or creation changes and missing sequences
refuse instead of skipping ahead.

Each pull contains at most 32 messages (at most 256 KiB raw payload). A batch
uses a 5 ms flush timer after its first decoded message, or flushes at its event/byte cap;
empty pulls may wait one second without delaying an available message. The total
canonical HTTP envelope is bounded at 256 KiB. Only one HTTP request runs at a
time. The app atomically commits every source event and the consecutive cursor
before returning a digest-bound receipt. The SDK verifies that receipt before
requesting confirmed broker ACKs. Lost HTTP headers or response-body transport
retry the identical serialized batch at most three times by default; complete
malformed responses, authentication, schema and quota refusals stop unacknowledged.
A transient `503` retries the same bounded request. A lost connection, buffered
input or broker ACK causes a fresh connection and fresh consumer from the durable
server cursor, with bounded consecutive reconnect attempts. Replaying an already
committed prefix does not republish events or spend quota again, including after
source-log pruning. A covered receipt proves prefix acceptance, not historical
payload retention. These guarantees cover ingestion; they do not make downstream
external trading effects exactly once.

The accepted publication quota remains 100,000 events/account/day (about 1.16
per second averaged over a day). Aggregate raw order-book traffic into appropriate
signals or metrics before this interface; no raw book-rate capacity or remote
latency SLA is claimed. Use `onState` and `onReceipt` for bounded telemetry; both
observers are synchronous. `onReceipt` runs after durable receipt validation and
before ACK, so a crash there is recoverable from the committed server cursor.


The protocol choices follow the official [NATS consumer contracts](https://docs.nats.io/concepts/jetstream) and [pull-consumer API](https://nats-io.github.io/nats.js/jetstream/index.html). Ephemeral consumers expire after 60 seconds of inactivity when a process cannot clean up after a crash; set appropriate broker consumer limits as part of operating your own NATS account.

## Types (index.d.mts)

```ts
export { math } from './math.mjs';
export type { ExactMath, IntegerInput, U256, I64 } from './math.mjs';
/**
 * The SDK an automation on ftw.trade is written against (D1181).
 * `export default automation({ ... })` is the whole contract: the hosted
 * runtime reads the definition, then calls `run(ctx, event)` on each trigger.
 */

/** A pool as the Pools page names it: "chain:address". */
export type PoolKey = `${string}:${string}`;
export type Address = `0x${string}`;

/** A message filter: each key a dotted path into the message ("data.usd"), each value what it must equal, or bounds. */
export type MessageWhere = Record<string, string | number | boolean | null | { gt?: number; gte?: number; lt?: number; lte?: number; ne?: string | number | boolean | null; in?: (string | number | boolean)[] }>;

/**
 * Build's own streams: what build already indexes, handed out as it lands.
 * - launches: { chain, launchpad, token, name, symbol, deployer, devBuyEth, block, ts, tx }
 * - graduations: { chain, token, pool, ts }
 * - curves: { chain, token, pool, launchpad, symbol, progress (percent), graduated }, on each change, for the curves build indexes
 * - swaps: { pool, chain, address, tx, block, ts, buy, base, quote, price, trader }, for the pools build indexes
 * - prices: { feed, value, ts }, each new value of a shared price feed
 */
export type BuildStream = 'launches' | 'graduations' | 'curves' | 'swaps' | 'prices';

export type Trigger =
  | { manual: true }
  /** "1s" to "7d". */
  | { every: `${number}${'s' | 'm' | 'h' | 'd'}` }
  /** Fires when the pool's price crosses the line, not while it stays past it. */
  | { pool: PoolKey; above?: number; below?: number }
  /**
   * Fires once for each signal of that name another automation publishes, whose fields equal `where`. The signal is held
   * until its run ends: a run the server never finished gets it again, with event.attempt counting up, and a failed run
   * gets it again while `retries` (0 to 5) last. `delivery: 'at-most-once'` never hands a signal again.
   */
  | { signal: string; where?: Record<string, string | number | boolean>; retries?: number; delivery?: 'at-least-once' | 'at-most-once' }
  /**
   * An always-on websocket build holds: each message that passes where runs the code with { type: 'message', data }. Its host must be in policy.egress.
   * `resume` and `catchUp` are as a source's subscribe has them; a catch-up runs the code with a GapEvent.
   */
  | { socket: `wss://${string}`; send?: unknown; where?: MessageWhere; resume?: string; catchUp?: true | { every?: string } }
  /** One of build's own streams: each event that passes where runs the code with { type: 'message', stream, data }. No egress needed. */
  | { stream: BuildStream; where?: MessageWhere };

export interface Policy {
  /** Project wallet ids this automation may trade from; omitted is read-only. */
  wallets?: string[] | 'all';
  /** ETH per buy. Default 0. */
  maxEthPerTrade?: number;
  /** ETH across all buys in a UTC day. Default maxEthPerTrade. */
  maxEthPerDay?: number;
  /** Token addresses it may trade; omitted is any. */
  tokens?: Address[];
  /** Hosts ctx.fetch may reach over https; omitted is none. */
  egress?: string[];
  /** Account secrets (set on the Scripts tab) a fetch header may send as "{{secrets.NAME}}", to egress hosts only. */
  secrets?: string[];
  /** Ask to trade in a live project. Approved at deploy, never by the code. */
  live?: boolean;
  /** The most one move may bridge, by token symbol: { usdc: 500, eth: 0.2 }. A token not named is not bridged. */
  bridge?: Record<string, number>;
  /** The project's actions (D1267) ctx.actions may call, by name; omitted is none. */
  actions?: string[];
}

export type Event =
  | { type: 'action'; args: Record<string, FieldValue>; caller: string }
  | { type: 'manual'; [field: string]: unknown }
  | { type: 'schedule'; every: number; at: number }
  | { type: 'price'; pool: PoolKey; price: number; previous: number; crossed: 'above' | 'below'; line: number; at: number }
  | { type: 'signal'; signal: string; id: number; fields: Record<string, FieldValue>; source: string; at: number }
  /** A message on a socket trigger ({ socket: "wss://…" }, or source({ subscribe })): parsed JSON, or { text } when it is not JSON. */
  /** An event on one of build's streams ({ stream: "launches" }): data as BuildStream lists it. */
  | { type: 'message'; url?: string; stream?: BuildStream; data: unknown; at: number };

/**
 * The one type model sources, signals, actions and workflow steps share
 * (D1232). `hash` is a 32-byte hex hash (a transaction, a V4 pool id) or a
 * Solana signature; `json` is any JSON value up to 4 KB.
 */
export type FieldKind = 'text' | 'number' | 'boolean' | 'address' | 'chain' | 'pool' | 'hash' | 'url' | 'json';
export type Json = string | number | boolean | null | Json[] | { [k: string]: Json };
/** A field's value as a source or a signal carries it. */
export type FieldValue = Cell | Json;

/** A signal this automation may publish: its fields and their kinds (a field name ending in ? is optional). */
export interface SignalDeclaration {
  description?: string;
  /** The fields every event has; with `cases`, the fields every case shares. */
  fields?: Record<string, FieldKind | `${FieldKind}?`>;
  /**
   * A sum type (D1236): each event is one case, `{ case: "call", … }`,
   * holding the shared fields and its case's own. A strategy matches on
   * `e.case` and must cover every case.
   */
  cases?: Record<string, Record<string, FieldKind | `${FieldKind}?`>>;
}

export interface Position {
  token: Address;
  symbol: string | null;
  units: string;
  valueEth: number | null;
}

export interface Balance {
  mode: 'paper' | 'live';
  eth: number;
  valueEth: number | null;
  pnlEth: number | null;
  positions: Position[];
}

export interface TradeResult {
  mode: 'paper' | 'live';
  ok: boolean;
  side: 'buy' | 'sell';
  wallet: string;
  token: Address;
  symbol: string | null;
  eth: number | null;
  units: string | null;
  error: string | null;
}

/** Funds to move between chains: build asks every bridge that carries it and sends the best route. */
export interface BridgeMove {
  /** Chain names as ftw.trade writes them: base, arbitrum, ethereum, solana, robinhood, hyperliquid, … */
  from: string;
  to: string;
  /** usdc, eth, sol, usdg, or any symbol a bridge lists there. */
  token: string;
  amount: number;
  /** To swap on the way, like usdc on base to sol on solana. The token itself unless named. */
  toToken?: string;
  /** auto (default): the most that lands within two minutes, waiting longer only to save 5 bps or more. */
  prefer?: 'auto' | 'cheapest' | 'fastest';
  /** One bridge by name; any that carries it unless named. */
  via?: 'relay' | 'cctp';
}

export interface BridgeResult {
  mode: 'paper' | 'live';
  ok: boolean;
  wallet: string;
  /** What is expected to land, in the destination token. */
  amount: number | null;
  provider: 'relay' | 'cctp' | null;
  speed: 'fast' | 'standard' | null;
  etaSec: number | null;
  txs: string[];
  error: string | null;
}

/** A project wallet. It holds no key: each trade is checked against the approved policy and signed by ftw.trade. */
export interface Wallet {
  readonly id: string;
  readonly label: string;
  readonly group: string | null;
  readonly address: Address | null;
  balance(): Promise<Balance>;
  /** Throws "refused: …" when the policy does not allow it. */
  buy(order: { token: Address; eth: number }): Promise<TradeResult>;
  /** Sells `percent` (default 100) of the holding. */
  sell(order: { token: Address; percent?: number }): Promise<TradeResult>;
  /** Moves funds between chains by the best route, within policy.bridge. Throws "refused: …" when the policy does not allow it. */
  bridge(move: BridgeMove): Promise<BridgeResult>;
  /** Every route for a move from this wallet, ranked, and the one bridge() would send; nothing is sent, and no policy is needed. Counts as a fetch. */
  quoteBridge(move: BridgeMove): Promise<BridgeQuote>;
}

/** One bridge's route for a move. */
export interface BridgeRoute {
  provider: 'relay' | 'cctp';
  label: string;
  speed: 'fast' | 'standard' | null;
  amountIn: number;
  token: string;
  /** What is expected to land, and the least the bridge lets land, in toToken. */
  amountOut: number;
  minOut: number;
  toToken: string;
  feeUsd: number | null;
  etaSec: number;
}

export interface BridgeQuote {
  mode: 'paper' | 'live';
  wallet: string | null;
  plan: {
    intent: { from: string; to: string; token: string; toToken: string; amount: number; prefer: 'auto' | 'cheapest' | 'fastest'; via: 'relay' | 'cctp' | null };
    /** The route bridge() would send: the first of routes. */
    best: BridgeRoute;
    routes: BridgeRoute[];
    /** Bridges that do not carry this move, and why. */
    skipped: { provider: 'relay' | 'cctp'; reason: string }[];
    /** Why best is best, in a sentence. */
    why: string;
  };
}

/** A pool in the project's workspace, as the Pools page shows it. */
export interface PoolInfo {
  pool: {
    key: PoolKey;
    chain: string;
    address: string;
    name: string;
    /** v2, v3, v4, solana, … */
    kind: string;
    venue: string | null;
    base: string;
    quote: string;
    /** What an order on this pool buys or sells. */
    baseToken: string;
    lastPrice: number | null;
    lastTs: number | null;
    /** The base token's supply in whole tokens; price × supply is its market cap in the quote. */
    supply: number | null;
    state: string;
    error: string | null;
  };
  stats: {
    price: number | null;
    /** Percent change over each window. */
    change: Record<'5m' | '1h' | '6h' | '24h', number | null>;
    volume24h: number;
    buys24h: number;
    sells24h: number;
    makers24h: number;
    buyVolume24h: number;
    sellVolume24h: number;
    buyers24h: number;
    sellers24h: number;
  };
}

/** A pool's liquidity over time: its closing level in each bucket, and the latest adds and removes. */
export interface LiquidityHistory {
  levels: { ts: number; base: number; quote: number }[];
  changes: { block: number; index: number; tx: string; ts: number; add: boolean; base: number; quote: number; owner: string | null }[];
}

/** A V3/V4 or Solana pool's liquidity by price around the price now. */
export interface LiquidityByPrice {
  price: number | null;
  /** True while positions are still being read back. */
  building: boolean;
  bins: { low: number; high: number; base: number; quote: number; value: number; liquidity: number }[];
}

export interface Candle {
  ts: number;
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number | null;
}

export interface Swap {
  block: number;
  tx: string;
  ts: number;
  buy: boolean;
  base: number;
  quote: number;
  price: number;
  trader: string | null;
}

export type Cell = string | number | boolean | null;

export interface Context {
  readonly automation: string;
  readonly version: number;
  readonly run: number;
  readonly mode: 'paper' | 'live';
  readonly project: { id: string; name: string };
  readonly log: {
    debug(message: unknown, data?: unknown): void;
    info(message: unknown, data?: unknown): void;
    warn(message: unknown, data?: unknown): void;
    error(message: unknown, data?: unknown): void;
  };
  /** The one-line status on the automation's card. */
  status(text: string): void;
  /** A point in the "metrics" table. */
  metric(name: string, value: number, tags?: Record<string, Cell>): void;
  /** A timestamped row in a named table, for dashboard charts. */
  emit(table: string, row: Record<string, Cell>): void;
  /** JSON kept between runs, 64 KB in all. */
  readonly state: {
    get<T = unknown>(key: string): Promise<T | null>;
    set(key: string, value: unknown): Promise<void>;
    delete(key: string): Promise<void>;
  };
  /** Private durable execution memory. Paper/live namespaces are separate.
   * Loaded once per run; cached reads perform no host or database call.
   * Await mutations before relying on them: durable:true acknowledges commit.
   * Later effects wait for writes already called. A durable host-write refusal
   * stops the run; local malformed JSON/key requests may be caught and corrected.
   * Effects already dispatched still require outcome reconciliation.
   * Isolated test runs start empty and return durable:false. */
  readonly memory: {
    get<T = unknown>(key: string): Promise<{ value: T; revision: number } | null>;
    set(key: string, value: unknown): Promise<{ revision: number; durable: boolean }>;
    delete(key: string): Promise<{ revision: number; durable: boolean }>;
    keys(prefix?: string): Promise<string[]>;
    stats(): Promise<{ revision: number; keys: number; bytes: number }>;
  };
  readonly wallets: {
    list(): Wallet[];
    /** By id or label. */
    get(id: string): Wallet;
  };
  readonly pools: {
    price(pool: PoolKey): Promise<{ price: number | null; ts: number | null; state: string } | null>;
    candles(pool: PoolKey, opts?: { interval?: '1m' | '5m' | '15m' | '1h' | '4h' | '1d'; limit?: number }): Promise<Candle[]>;
    swaps(pool: PoolKey, opts?: { limit?: number }): Promise<Swap[]>;
    /** The pool as the Pools page shows it, with its day's stats; null when it is not in the workspace. */
    get(pool: PoolKey): Promise<PoolInfo | null>;
    /** For a pool indexed with track liquidity: its level over time (default), or by price around the price now. */
    liquidity(pool: PoolKey, opts?: { view?: 'history'; interval?: '1m' | '5m' | '15m' | '1h' | '4h' | '1d'; limit?: number }): Promise<LiquidityHistory>;
    liquidity(pool: PoolKey, opts: { view: 'by_price'; bins?: number; span?: number }): Promise<LiquidityByPrice | null>;
  };
  /**
   * The project's actions (D1267), by name; policy.actions must name each one.
   * Dispatched calls answer ok with the value, or why they failed. A preceding
   * durable memory refusal rejects the call before dispatch.
   */
  readonly actions: {
    /** Waits for the action's answer, up to its timeout (or timeoutMs, if shorter). A failure answers as one, never with the stub. */
    call<R extends Record<string, FieldValue> = Record<string, FieldValue>>(name: string, args?: Record<string, FieldValue>, opts?: { timeoutMs?: number }): Promise<ActionResult<R>>;
    /** Starts the action and answers at once; its outcome is on the action's runs. */
    send(name: string, args?: Record<string, FieldValue>): Promise<{ sent: true } | { sent: false; kind: ActionFailure; error: string }>;
  };
  readonly prices: {
    /** The latest value of a shared series, e.g. "eth-usd-coinbase". */
    series(name: string): Promise<number | null>;
  };
  /** https to policy.egress hosts only, no redirects, 256 KB. */
  fetch(url: string, init?: { method?: string; headers?: Record<string, string>; body?: string | object }): Promise<{ status: number; headers: Record<string, string>; text: string; json?: unknown }>;
  /** Wait up to 10 s. */
  sleep(ms: number): Promise<void>;
  /**
   * Publish a signal the definition declares. Dropped when the same key (by
   * default, the same fields) was published within dedupeFor seconds (24 h).
   */
  signal(name: string, fields: Record<string, FieldValue>, opts?: { key?: string; dedupeFor?: number }): Promise<{ published: true; id: number } | { published: false; duplicateOf?: number; reason?: string; test?: boolean }>;
}

export interface Definition {
  name?: string;
  description?: string;
  triggers?: Trigger[];
  policy?: Policy;
  /** The signals it may publish, by name. */
  signals?: Record<string, SignalDeclaration>;
  run(ctx: Context, event: Event): unknown | Promise<unknown>;
}

export declare function automation<D extends Definition>(definition: D): D;
export declare function eth(n: number | string): number;
/** A checked pool key: pool('base', '0xAbC…') is "base:0xabc…". Throws on anything but chain:0x… (an address or a V4 pool id) or solana:<base58>. */
export declare function pool(chain: string, address: string): PoolKey;
/** A pool key's chain and address, checked as pool() checks them. */
export declare function parsePool(key: string): { chain: string; address: string };

/** Field kinds as helpers: `t.address()`, `t.url({ optional: true })`. */
export declare const t: { readonly [K in FieldKind]: { (): K; (opts: { optional: true }): `${K}?`; (opts?: { optional?: boolean }): K | `${K}?` } };

/**
 * A typed source (D1232): what `poll` returns, one event's fields or a list
 * of them, is checked against `fields` and published as an event of the
 * source `name`. Workflows, automations and strategies build on it.
 */
export interface SourceDefinition<F extends Record<string, FieldKind | `${FieldKind}?`> = Record<string, FieldKind | `${FieldKind}?`>> {
  /** The source's name: lowercase letters, digits, ".", "-" and "_". */
  name: string;
  description?: string;
  /** The fields every event has; with `cases`, the fields every case shares. */
  fields?: F;
  /** A sum type (D1236): each event is one case, `{ case: "call", … }`, with that case's fields. */
  cases?: Record<string, Record<string, FieldKind | `${FieldKind}?`>>;
  /** How often poll runs, "1s" to "7d". */
  every?: `${number}${'s' | 'm' | 'h' | 'd'}`;
  /** More triggers, as an automation takes them. */
  triggers?: Trigger[];
  /** Reach and secrets for ctx.fetch, as an automation's policy. */
  policy?: Policy;
  /** Drop an event whose key was published within this many seconds (24 h by default; 0 keeps every one). */
  dedupeFor?: number;
  /** An event's dedupe key; by default, its fields. */
  key?(fields: Record<string, FieldValue>): string;
  /**
   * Make it public when it is deployed: listed for every account, which can
   * add it or read it live, with its uptime. `publisher` is the name shown
   * (Anonymous unless given); `showCode` shows this code with it as deployed.
   * `false` makes it private again on deploy; left out, its sharing is left
   * as the Sources page or update_source_sharing set it.
   */
  public?: boolean | { publisher?: string; description?: string; showCode?: boolean; codeUrl?: string };
  /**
   * Its major version, 1 unless given. Readers of a public source pin a
   * major, and a deploy that changes its type in a way they cannot read (a
   * field removed, renamed, made required or of another kind) is refused.
   * Such a change is a new major: write it as a script of its own with
   * `version: 2`, published as `<name>.v2` beside v1, which keeps running.
   * Deploying it releases it as the public source's v2 when the source is
   * public; its readers then upgrade by hand before v1 retires.
   */
  version?: number;
  /** What changed in this major, shown to its readers. */
  releaseNotes?: string;
  /** Days the major before this one keeps running once this one is released: 14 to 90, 30 unless given. */
  retireOldAfterDays?: number;
  /**
   * What it listens to, always on, with no server of its own: one of build's
   * streams (`{ stream: 'launches', where: { launchpad: 'pumpfun' } }`), or a
   * list of them. Or an always-on websocket build holds for it, shared with every source on
   * the same URL and `send`. `send` is sent on each connect (a subscribe
   * request); `where` drops messages before any code runs: each key a dotted
   * path into the message, each value what it must equal or
   * `{ gt, gte, lt, lte, ne, in }`. Each message that passes runs onMessage.
   * The host must be in `policy.egress`.
   */
  subscribe?: Subscription | Subscription[];
  /** Each message (a socket's or a stream's) that passes `subscribe.where` (parsed JSON, or `{ text }`): what to publish, as poll returns it. */
  onMessage?(ctx: Context, message: unknown, event: Event): Record<string, FieldValue> | Record<string, FieldValue>[] | null | undefined | Promise<Record<string, FieldValue> | Record<string, FieldValue>[] | null | undefined>;
  /** A socket's catch-up (subscribe.catchUp): what it missed, queried and returned as poll returns it. Without it, poll runs. */
  onGap?(ctx: Context, gap: GapEvent): Record<string, FieldValue> | Record<string, FieldValue>[] | null | undefined | Promise<Record<string, FieldValue> | Record<string, FieldValue>[] | null | undefined>;
  /** What to publish on each `every` (and on a run by hand). */
  poll?(ctx: Context, event: Event): Record<string, FieldValue> | Record<string, FieldValue>[] | null | undefined | Promise<Record<string, FieldValue> | Record<string, FieldValue>[] | null | undefined>;
}

export type Subscription =
  | { stream: BuildStream; where?: MessageWhere }
  | {
      url: `wss://${string}`;
      send?: unknown;
      where?: MessageWhere;
      /** The dotted path of a cursor in each message; the last one seen is sent back as {{cursor}} in `send` on every reconnect. */
      resume?: string;
      /** Run onGap (or poll) after a reconnect, a restart or dropped messages, and every `every` ("10s".."1d"). */
      catchUp?: true | { every?: string };
    };

/** What onGap is handed: the span the socket may have missed (unix ms), why, and the last cursor seen. */
export interface GapEvent {
  type: 'gap';
  url: string;
  reason: 'reconnect' | 'restart' | 'dropped' | 'every';
  from: number | null;
  to: number;
  cursor: string | number | null;
  at: number;
}

export declare function source<D extends SourceDefinition>(definition: D): Definition;

/** Why a call to an action failed: a timeout, an error it threw, refused (policy, limits, no such action), or invalid (args or its return off the contract). */
export type ActionFailure = 'timeout' | 'error' | 'refused' | 'invalid';

/** What ctx.actions.call answers. Check ok before reading value. */
export type ActionResult<R = Record<string, FieldValue>> =
  | {
      ok: true;
      value: R;
      /** The value is the action's stub: the caller's test run, which calls nothing. */
      stub?: true;
      /** The action's run that answered, and how many tries it took. */
      run?: number;
      tries?: number;
    }
  | { ok: false; kind: ActionFailure; error: string; run?: number; tries?: number };

type Kinds = Record<string, FieldKind | `${FieldKind}?`>;
type ValueOf<K> = K extends 'number' ? number : K extends 'boolean' ? boolean : K extends 'json' ? Json : string;
type Required_<S extends Kinds> = { [K in keyof S as S[K] extends `${string}?` ? never : K]: ValueOf<S[K]> };
type Optional_<S extends Kinds> = { [K in keyof S as S[K] extends `${infer B}?` ? K : never]?: S[K] extends `${infer B}?` ? ValueOf<B> : never };
/** The values a field shape holds: { token: t.address(), n: t.number({ optional: true }) } holds { token: string; n?: number }. */
export type ValuesOf<S extends Kinds> = Required_<S> & Optional_<S>;

/**
 * A TypeScript action (D1267): called by name, with args checked against
 * `args` before it runs and what it returns checked against `returns` after.
 */
export interface ActionDefinition<A extends Kinds = Kinds, R extends Kinds = Kinds> {
  /** How callers name it: lowercase letters, digits, ".", "-" and "_". One deployed action of each name in a project. */
  name: string;
  description?: string;
  args?: A;
  returns?: R;
  /** "10s" by default, at most "30s"; a run past it is a timeout. */
  timeout?: `${number}s` | number;
  /** Tries again after a timeout or an error, with backoff, up to 3 times. Only for work that is safe to repeat. */
  retries?: 0 | 1 | 2 | 3;
  /** What a caller's test run gets, which calls nothing. A real call never answers it. */
  stub?: ValuesOf<R>;
  /** Its own reach, as an automation's policy, approved at deploy. */
  policy?: Policy;
  run(ctx: Context, args: ValuesOf<A>): ValuesOf<R> | Promise<ValuesOf<R>>;
}

export declare function action<A extends Kinds, R extends Kinds>(definition: ActionDefinition<A, R>): Definition;
```

## Exact math types (math.d.mts)

```ts
/** Integer operands accept bigint, safe integer Numbers, or bounded text (at most 4096 UTF-8 bytes). */
export type IntegerInput = bigint | number | string;
/** Canonical unsigned 256-bit decimal text, safe for JSON, actions, sources and execution memory. */
export type U256 = string & { readonly __u256: unique symbol };
/** Canonical signed 64-bit decimal text, safe for JSON, actions, sources and execution memory. */
export type I64 = string & { readonly __i64: unique symbol };
/** All operations refuse invalid input, overflow and division by zero with TypeError or RangeError. */
export interface ExactMath {
  readonly u256: (value: IntegerInput) => U256;
  readonly u256Add: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256Sub: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256Mul: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256Div: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256DivUp: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256Mod: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256Min: (a: IntegerInput, b: IntegerInput) => U256;
  readonly u256Max: (a: IntegerInput, b: IntegerInput) => U256;
  /** Full-width product, then unsigned floor division; only the final answer must fit uint256. */
  readonly u256MulDiv: (a: IntegerInput, b: IntegerInput, divisor: IntegerInput) => U256;
  /** Full-width product, then unsigned ceiling division; only the final answer must fit uint256. */
  readonly u256MulDivUp: (a: IntegerInput, b: IntegerInput, divisor: IntegerInput) => U256;
  readonly u256Cmp: (a: IntegerInput, b: IntegerInput) => -1 | 0 | 1;
  /** Exact base units at 0..77 decimals. Nonzero subunits are refused, never rounded. */
  readonly u256Units: (amount: IntegerInput, decimals: number) => U256;
  /** Exact display decimal text, with fractional trailing zeroes removed. */
  readonly u256Format: (value: IntegerInput, decimals: number) => string;
  /** Approximate display Number; never use it as an exact integer operand or signing amount. */
  readonly u256Number: (value: IntegerInput, decimals?: number) => number;
  /** Refuses integers above Number.MAX_SAFE_INTEGER. */
  readonly u256SafeNumber: (value: IntegerInput) => number;
  readonly i64: (value: IntegerInput) => I64;
  readonly i64Add: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Sub: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Mul: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Div: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Mod: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Min: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Max: (a: IntegerInput, b: IntegerInput) => I64;
  readonly i64Neg: (value: IntegerInput) => I64;
  readonly i64Abs: (value: IntegerInput) => I64;
  readonly i64Cmp: (a: IntegerInput, b: IntegerInput) => -1 | 0 | 1;
  /** Approximate display Number. */
  readonly i64Number: (value: IntegerInput) => number;
  /** Refuses integers outside Number.MIN_SAFE_INTEGER..Number.MAX_SAFE_INTEGER. */
  readonly i64SafeNumber: (value: IntegerInput) => number;
}
/** The same dependency-free factory is embedded in the hosted sandbox. */
export declare function createExactMath(): ExactMath;
export declare const math: ExactMath;
export declare const u256: ExactMath['u256'];
export declare const u256Add: ExactMath['u256Add'];
export declare const u256Sub: ExactMath['u256Sub'];
export declare const u256Mul: ExactMath['u256Mul'];
export declare const u256Div: ExactMath['u256Div'];
export declare const u256DivUp: ExactMath['u256DivUp'];
export declare const u256Mod: ExactMath['u256Mod'];
export declare const u256Min: ExactMath['u256Min'];
export declare const u256Max: ExactMath['u256Max'];
export declare const u256MulDiv: ExactMath['u256MulDiv'];
export declare const u256MulDivUp: ExactMath['u256MulDivUp'];
export declare const u256Cmp: ExactMath['u256Cmp'];
export declare const u256Units: ExactMath['u256Units'];
export declare const u256Format: ExactMath['u256Format'];
export declare const u256Number: ExactMath['u256Number'];
export declare const u256SafeNumber: ExactMath['u256SafeNumber'];
export declare const i64: ExactMath['i64'];
export declare const i64Add: ExactMath['i64Add'];
export declare const i64Sub: ExactMath['i64Sub'];
export declare const i64Mul: ExactMath['i64Mul'];
export declare const i64Div: ExactMath['i64Div'];
export declare const i64Mod: ExactMath['i64Mod'];
export declare const i64Min: ExactMath['i64Min'];
export declare const i64Max: ExactMath['i64Max'];
export declare const i64Neg: ExactMath['i64Neg'];
export declare const i64Abs: ExactMath['i64Abs'];
export declare const i64Cmp: ExactMath['i64Cmp'];
export declare const i64Number: ExactMath['i64Number'];
export declare const i64SafeNumber: ExactMath['i64SafeNumber'];
export default math;
```
