# k256 Replay — operator quickstart for an AI assistant

You are helping a user operate a **k256 Replay** Solana node. This
document is the complete contract for the surface they have access
to: every endpoint, every field, every error kind, every lifecycle
rule. Generate runnable `curl`, TypeScript, or Python snippets on
demand using the values under **Connection**.

## What this environment is

A managed Solana Replay environment running a paused fork of mainnet.
The user can fast-forward it to any future slot, splice transactions
into a block, patch account state, deploy programs, and reboot from
any catalog snapshot. It is **not** a live mainnet node — block
height never advances unless the user calls `POST /advance`, and
every state change since the last `/boot` is local to this fork
(no other node will accept it).

This document describes the customer-visible contract only: endpoints,
request/response fields, lifecycle rules, errors, and safe operating
guidance. Do not infer or describe internal implementation details.

## Connection

- Base URL: `https://api.k256.xyz`
- Per-server base: `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>`
- API key: `<YOUR_API_KEY>` — org-scoped `k256_live_` bearer (one key reaches all your servers); never in a URL, always `Authorization: Bearer`
- Environment selection: choose an environment by its `<SERVER_ID>` in the path. Find your server ids in the dashboard or via `GET https://api.k256.xyz/v1/replay/servers`.
- Runtime build: `(unknown build)`

| Surface | URL | Auth |
| --- | --- | --- |
| Per-server HTTP API | `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>` | `Authorization: Bearer <YOUR_API_KEY>` |
| Server record | `GET https://api.k256.xyz/v1/replay/servers/<SERVER_ID>` | same bearer — returns `{ "server": { id, label, region, status, ipv4, … } }` |
| Dashboard catalog API | `https://app.k256.xyz/api/snapshots` | WorkOS session cookie (browser only) |

> **Direct Solana endpoints.** The per-server HTTP API above is the
> Replay control surface; it is not a Solana JSON-RPC passthrough.
> Standard Solana JSON-RPC, PubSub, and Geyser endpoints are shown in
> the authenticated Replay console RPC tab. Those copied endpoint URLs
> are bearerless operational secrets. HTTPS browser apps need their own
> HTTPS JSON-RPC proxy and WSS bridge.

The org-scoped `k256_live_` key is created and revoked from the
dashboard at `/app/workspace`; it reaches every Replay server your
org owns. Treat it as a secret — never put it in a URL query string,
always use the `Authorization` header.

## Four surfaces, one fork

Same state, same audit trails; pick whichever fits the task you're on.

| Surface | Best for | How |
| --- | --- | --- |
| **Web console** at `https://app.k256.xyz/app/replay` | Boot, inspect, mutate by clicking. Includes the Blocks → State diff tab. | Sign in with your workspace identity. |
| **HTTP API** at `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>` | Scripts, agents that already speak HTTP, anything portable. Documented end-to-end below. | `Authorization: Bearer <YOUR_API_KEY>` on every request. |
| **Replay MCP server** at `https://api.k256.xyz/replay/mcp` | Coding agents (Cursor, Claude Desktop, Codex, VS Code). ~49 Replay tools (one per operation) wrap this HTTP surface, named by dotted op id (`replay.boot_server`, `replay.advance_server`, …). | Configure your client with the same bearer; see the Replay API Explorer's MCP tab. |
| **`k256-replay` CLI** at `github.com/k256-xyz/replay-cli` | Operators in a terminal — `status`, `diff`, `advance --wait`, `checkpoint save/restore`. Mirrors the State diff tab as a colored table. | `cargo install --git https://github.com/k256-xyz/replay-cli --locked`, then `export REPLAY_API_KEY=…` and `export REPLAY_ENDPOINT=https://api.k256.xyz/v1/replay/servers/<SERVER_ID>`. |
| **Anchor IDL fetch** at `https://api.k256.xyz/v1/replay/idl/:program_id` | Read an on-chain Anchor IDL straight off mainnet without selecting a Replay server. | Same org bearer, no per-server endpoint; see **Anchor IDL fetch** below. |

The CLI is the friction-reducer for the workbench loop —
`k256-replay diff <sig>` is the same fixture the web State diff tab
renders. `--json` round-trips the wire shape verbatim for `jq`
pipelines, `--bytes` dumps base64. Exit codes map to HTTP error
classes (2=401, 3=403, 4=404, 5=409, 6=400, 7=5xx).

## Evidence and receipts

Replay has several evidence lanes. Use the narrowest one that proves
the action you just took; do not invent missing receipt fields.

| Action family | Immediate proof | Durable/follow-up proof |
| --- | --- | --- |
| Boot | `202 Accepted` from `POST /boot` with `request_id`, then `/status.phase` | `/status` with snapshot/current slot and RPC readiness |
| Advance | `job_id` + `request_id` from `POST /advance`, live `/status.advance` | `GET /advance/history` and `/status.last_advance` |
| Account patch / transaction splice | `POST /accounts/patch` response or splice response | `GET /accounts/patch/history`; rows may include `request_id` when the runtime captured a gateway request id |
| Program deploy | deploy response | `GET /programs/deploy/history`; rows may include `request_id` when available |
| Checkpoint / restore | accepted response with `request_id` plus the dashboard operation strip | `GET /checkpoints` for saved restore points and slot metadata; older rows can say slot `not recorded` |
| Billing / deprovision | billing API receipt with `request_id` / refund ids | Billing refund history, Replay Activity, and `replay.server.*` Webhooks events |

The console History tab combines local runtime rows with durable gateway
audit rows. Local runtime rows prove fork-local state changes; gateway
audit rows prove authenticated product API actions and include actor,
resource, result, and request id when recorded. Older rows and rows
created before a runtime upgrade can legitimately show `request_id:
null` or `not recorded`.

## Lifecycle phases (`/status.phase`)

| Value | Meaning |
| --- | --- |
| `idle` | No runtime active. `/boot` to start from a snapshot. |
| `downloading` | Fetching an uncached snapshot archive. `/status.download` carries progress. Cached boots skip this phase. |
| `booting` | Replay runtime is starting. |
| `extracting` | Decompressing the selected snapshot archive into the ledger. Check `/status.boot_cache_hit` to distinguish cached-local extraction from extraction after a fresh fetch. |
| `indexing` | Building the account index. |
| `ready` | JSON-RPC is accepting queries. This is the only phase where `/advance`, `/accounts/patch`, and `/programs/deploy` are accepted. |
| `advancing` | An advance job is running. `/status.advance` is the live job. |
| `dead` | The runtime stopped or failed. `/status.last_error` carries the reason. `/boot` to recover. |

## Concurrency rules (read before any mutation)

- One `/boot` at a time. POSTing `/boot` while phase ∈ {downloading, booting, extracting, indexing} returns `409 boot_in_flight`.
- One `/advance` at a time. POSTing `/advance` while `advance` is in-flight returns `409 advance_in_flight` with the active job.
- No `/advance`, `/accounts/patch`, or `/programs/deploy` outside `phase=ready` → `409 phase_not_ready`.
- No `/boot` while an advance is in-flight → `409 advance_in_flight`. Cancel the advance first.
- `mutation.dirty=true` after `/accounts/patch`, `/programs/deploy`, or `/advance` with `splices`. The dashboard warns, but `/advance` remains available after mutations. Only a fresh `/boot` clears the dirty flag.

## Authentication and error format

The gateway checks `Authorization: Bearer <key>` on every route
except `https://api.k256.xyz/healthz` (gateway uptime probe). A missing or
malformed bearer yields a JSON `401`; a revoked, unknown, or
disabled key yields `403`:

```json
{ "error": "unauthenticated" }
```
```json
{ "error": "invalid_token" }
```

When the bearer is valid but the environment isn't ready yet, the gateway
returns `503 server_not_ready` (`retry-after: 30`). When the environment
has been deprovisioned, `410 deprovisioning`. Both have JSON
bodies with the same `{ "error": "<kind>" }` shape.

Every other error response uses this shape:

```json
{ "error": "<machine_readable_kind>", "message": "<human readable>" }
```

Stable error kinds you can dispatch on:

| Kind | Status | Where |
| --- | --- | --- |
| `boot_in_flight` | 409 | `/boot` while booting |
| `advance_in_flight` | 409 | `/boot`, `/advance`, `/accounts/patch`, `/programs/deploy` |
| `phase_not_ready` | 409 | `/advance`, `/accounts/patch`, `/programs/deploy` |
| `bad_snapshot` | 400 | `/boot` with malformed descriptor (missing r2_key/sha256/canonical_filename) |
| `bad_geyser_config` | 400 | `/boot` with an invalid plugin reference |
| `rpc_url_missing` | 400 | `/advance` when no upstream RPC is configured |
| `target_in_past` | 400 | `/advance` with `target_slot <= current_slot` |
| `current_slot_unavailable` | 500 | `/advance` when the environment cannot answer `getSlot` |
| `shred_version_unknown` | 500 | `/advance` before runtime readiness completes; retry in a second |
| `splice_empty` | 400 | `/advance` with a splice op that has zero transactions |
| `splice_slot_in_past` | 400 | `/advance` with `splices[i].slot <= current_slot` |
| `splice_slot_beyond_target` | 400 | `/advance` with `splices[i].slot > target_slot` |
| `confirm_dangerous_required` | 400 | `/accounts/patch`, `/programs/deploy` without `confirm_dangerous: true` |
| `no_patches` | 400 | `/accounts/patch` with empty `patches` array |
| `bad_mode` | 400 | `/accounts/patch` with `mode` not in {merge, replace} |
| `bad_status` | 400 | `/programs/deploy` with `status` not in {deployed, finalized} |
| `authority_required` | 400 | `/programs/deploy` with `status=deployed` and no `authority_address` |
| `next_version_required` | 400 | `/programs/deploy` with `status=finalized` and no `next_version_address` |
| `empty_elf` | 400 | `/programs/deploy` with empty `elf_base64` |
| `elf_too_large` | 413 | `/programs/deploy` with base64 over 13 MiB (raw ELF over 10 MiB) |
| `no_active_advance` | 409 | `/advance/cancel` with nothing to cancel |
| `already_cancelling` | 409 | `/advance/cancel` after another cancel landed |
| `admin_rpc_error` | 502 | runtime operation failed |

---

# Two transports, one surface

Replay exposes the same verbs over two transports. Pick
whichever fits the integration:

- **HTTP** — `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/<path>` with `Authorization: Bearer <YOUR_API_KEY>`.
  Documented below in full. Use this for cURL, runtime SDKs, or any
  client that already speaks REST.
- **MCP** (Model Context Protocol) — `https://api.k256.xyz/replay/mcp`.
  Streamable HTTP transport per spec `2025-11-25`. Use this when an
  agent runtime (Cursor, Claude, VS Code Copilot, …) is the caller.
  Same bearer, same wire contract, same error kinds; tool names are
  the dotted operation ids (`replay.get_status`,
  `replay.boot_server`, `replay.advance_server`,
  `replay.patch_accounts`, …) — roughly one tool per operation
  (~49 replay tools).

The all-product MCP endpoint `https://api.k256.xyz/mcp` uses the same bearer
but is broader: it may include Replay, Webhooks, Program OS, and other
k256 product tools in one list. Use `https://api.k256.xyz/replay/mcp` when you want the
Replay-only 49-tool surface described here.

The HTTP docs below are the canonical contract. The MCP tool inputs
match the JSON request bodies described for each endpoint 1:1, and the
MCP tool result is the response body verbatim. Two endpoints are
**HTTP-only** and not exposed via MCP:

- Custom Geyser plugin upload — dashboard-only/server-side multipart;
  use the Boot tab. The public per-server API does not expose a
  customer-callable `POST /plugin/upload`.
- `GET /logs/stream` — SSE; agents poll `replay.tail_logs` instead.

---

# Replay HTTP API — `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>`

All per-server endpoints require `Authorization: Bearer <YOUR_API_KEY>`.
The gateway uptime probe `https://api.k256.xyz/healthz` takes no bearer.

## Read state

### `GET https://api.k256.xyz/healthz` (public, no bearer)

Returns `{ "ok": true }` while the gateway is up. Wire to uptime
checkers. This is the *gateway's* health, not an environment's — to check
whether a specific environment is ready, use `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/readyz` or
`https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/status`.

### `GET /readyz`

Returns 200 only when Replay JSON-RPC accepts queries
(i.e. `/status.phase == "ready"`). 503 otherwise. Use this to
gate downstream traffic.

### `GET https://api.k256.xyz/v1/replay/servers/<SERVER_ID>`

The server record — returns `{ "server": ... }` with connection
metadata and lifecycle state.

```json
{
  "server": {
    "id": "01KS2V3H4Z1S25X7DKGYGHM9YK",
    "label": "trader-mainnet-1",
    "region": "us-east",
    "status": "running",
    "ipv4": "203.0.113.42"
  }
}
```

Use the authenticated Replay console RPC tab for standard Solana
JSON-RPC, PubSub, and Geyser endpoint URLs. Do not POST Solana JSON-RPC
to the per-server API base; `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>` is the Replay control surface.

### `GET /status`

The single source of truth the dashboard polls every second.

Response (every field always present unless explicitly nullable):

```json
{
  "phase": "ready",
  "snapshot_slot": 421018067,
  "current_slot": 421018102,
  "shred_version": 50093,
  "rpc_listening": true,
  "uptime_secs": 16994,
  "last_error": null,
  "snapshot_file": "snapshot-421018067-<hash>.tar.zst",
  "download": null,
  "boot_cache_hit": true,
  "advance": null,
  "last_advance": null,
  "mutation": {
    "dirty": false,
    "last_mutation_slot": null,
    "last_mutation_kind": null,
    "mutation_count": 0
  },
  "geyser_plugin": "<opaque-active-plugin-ref>",
  "validator_version": "4.0.0"
}
```

Field semantics:

- `phase` — see **Lifecycle phases**.
- `snapshot_slot` — slot the environment booted from. Null until first boot.
- `current_slot` — live `getSlot` (commitment=processed). Populated only when phase ∈ {ready, advancing}; null otherwise.
- `shred_version` — required by `/advance`. Null briefly after boot until runtime readiness completes.
- `rpc_listening` — JSON-RPC is accepting connections.
- `last_error` — populated on the last failure (e.g. snapshot sha256 mismatch). Cleared on the next successful boot.
- `snapshot_file` — canonical filename of the active snapshot.
- `download` — `{ file, downloaded_bytes, total_bytes, percent, started_at_secs, bytes_per_sec, eta_secs }` while `phase=downloading`; null otherwise.
- `boot_cache_hit` — true when the active boot found the selected archive already local and skipped network fetch; false when the active boot fetched and verified the archive first; null/absent on older runtimes.
- `advance` — the current `AdvanceJob` while an advance is running (see `/advance` below). Null when no advance is current.
- `last_advance` — the most recent terminal `AdvanceJob` when the fork is no longer advancing. Use this for audit/status evidence after restore or completion; do not treat it as an in-flight operation.
- `mutation.last_mutation_kind` — one of `"account_patch"`, `"transaction_splice"`, `"program_deploy"`.
- `geyser_plugin` — opaque reference for the loaded plugin, or null when none. Treat it as a capability flag, not a filesystem path.
- `validator_version` — runtime compatibility version used for snapshot/plugin compatibility. Null if unavailable.

### `GET /snapshots/cache`

What's already cached for this environment.

```json
{
  "entries": [
    { "filename": "snapshot-421018067-<hash>.tar.zst", "size_bytes": 117972850398 }
  ]
}
```

A cached snapshot skips the network fetch. It still decompresses locally
before indexing. Match the `filename` against the catalog's
`canonical_filename` and `size_bytes` to know whether a future
`/boot` is cached.

This endpoint **does not list the bootable catalog** — only what's
cached for this environment. The catalog lives on the dashboard
(`GET https://app.k256.xyz/api/snapshots`, see below).

---

## Boot lifecycle

### `POST /boot`

Stop any running runtime and start from a chosen
snapshot. Resets the local fork — every advance and mutation since
the last boot is dropped. The control API does **not** choose a
catalog row for you; pass the exact descriptor from
`GET https://app.k256.xyz/api/snapshots`, verifies `sha256`,
then boots.

Returns `202 Accepted` immediately; poll `/status.phase` to watch
progress. Uncached boots walk `downloading → extracting → indexing →
ready`. Cached boots skip `downloading` and can move directly through
`booting/extracting → indexing → ready`; `/status.boot_cache_hit=true`
is the exact marker.

Request body:

```json
{
  "snapshot_slot": 421018067,
  "r2_key": "<archive-key-from-catalog>",
  "sha256": "<64 lowercase hex>",
  "canonical_filename": "snapshot-421018067-<hash>.tar.zst",
  "size_bytes": 117972850398,
  "catalog_id": "snap_01ks3byt5rkw1rdbwx6gzhtgy1",
  "geyser": null
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `snapshot_slot` | u64 | yes | Slot the snapshot was taken at. Surfaced in logs + `/status.snapshot_slot`. |
| `r2_key` | string | yes | Archive key from the catalog row. Treat it as an opaque handle. |
| `sha256` | string (64 hex, lowercase) | yes | Verified after download; mismatch aborts boot. |
| `canonical_filename` | string | yes | Must match the catalog row exactly. |
| `size_bytes` | u64 | yes | Used to detect a complete cache hit before sha256-verifying. |
| `catalog_id` | string | no | Informational audit linkage. |
| `geyser` | string \| null | no | `null` / omitted → no plugin; `"yellowstone"` → bundled gRPC preset; custom uploads use the opaque `config_path` returned by `GET /plugin/list`. |

202 response:

```json
{
  "phase": "downloading",
  "selected_snapshot": "snapshot-421018067-<hash>.tar.zst",
  "boot_slot": 421018067,
  "request_id": "req_..."
}
```

### `POST /kill`

Stop the running runtime without booting a new one. Phase goes to
`dead`. Any in-flight advance is cancelled and recorded with
`error="runtime stopped by operator"`.

```json
{ "phase": "dead" }
```

---

## Advance

### `POST /advance`

Walk the fork forward to `target_slot`. Non-blocking — registers an
`AdvanceJob`, returns `202 Accepted`, and advances in the
background. Poll `/status.advance` or
`/advance/history` for completion.

Request body:

```json
{
  "target_slot": 421018102,
  "root": true,
  "splices": []
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `target_slot` | u64 | yes | Must be `> current_slot` from `/status`. |
| `root` | bool | no (default `true`) | Force-root each replayed slot so finality tracks the canonical chain. |
| `rpc_url` | string | no | Upstream Solana RPC override. Going through `https://api.k256.xyz`, omit this to use the managed upstream endpoint. |
| `splices` | `SpliceRequest[]` | no (default `[]`) | Transaction splices applied while replaying. See below. |

202 response:

```json
{
  "job_id": "adv-1779303616039",
  "request_id": "req_...",
  "start_slot": 421018067,
  "target_slot": 421018102,
  "status": "running"
}
```

#### `SpliceRequest`

```json
{
  "slot": 421018090,
  "position": "block_end",
  "transactions_base64": ["AQAB..."]
}
```

| Field | Type | Notes |
| --- | --- | --- |
| `slot` | u64 | Must satisfy `current_slot < slot <= target_slot`. On a fresh boot, avoid snapshot warm-up slots: use at least `snapshot_slot + 2`, and if splicing there set `target_slot >= snapshot_slot + 3` so block RPC can serve the result. |
| `position` | `SplicePosition` | One of the variants below. |
| `transactions_base64` | string[] | Base64-encoded `VersionedTransaction` bytes (the same bytes `sendTransaction` would accept). Must be non-empty. |

`SplicePosition` variants (use the exact JSON form shown):

| Variant | JSON form | Meaning |
| --- | --- | --- |
| BlockStart | `"block_start"` | Inject at the very front of the block. |
| BlockEnd | `"block_end"` | Append after every canonical tx. |
| BeforeIndex | `{ "before_index": N }` | Inject right before the canonical tx at 0-based index N. |
| AfterIndex | `{ "after_index": N }` | Inject right after index N. |
| BeforeSignature | `{ "before_signature": "<base58 sig>" }` | Inject before the canonical tx with this signature. |
| AfterSignature | `{ "after_signature": "<base58 sig>" }` | Inject after this signature. |
| ReplaceIndex | `{ "replace_index": N }` | Drop the canonical tx at index N, put yours in its place. |
| ReplaceSignature | `{ "replace_signature": "<base58 sig>" }` | Drop the canonical tx with this signature, put yours in its place. |
| AtIndexClamped | `{ "at_index_clamped": N }` | Insert at index N; if N exceeds the upstream tx count, clamp to block_end. Friendliest for UI pickers. |

Successful splices flip `mutation.dirty=true` and write one row per
operation to `/accounts/patch/history` (the audit log is shared
across patches, splices, and deploys).

### `GET /advance`

The active job, or the most-recent completed one (so a UI can render
"last run" state after refresh). Same `AdvanceJob` shape as
`/status.advance`.

```json
{
  "job": {
    "id": "adv-1779303616039",
    "start_slot": 421018067,
    "target_slot": 421018102,
    "current_slot": 421018102,
    "rpc_url": "[managed by proxy]",
    "root": true,
    "status": "done",
    "error": null,
    "exit_code": 0,
    "started_at_unix": 1779303616,
    "finished_at_unix": 1779303642,
    "percent": 100.0
  }
}
```

`status` ∈ `"running" | "done" | "failed" | "cancelled"`.

The gateway redacts `rpc_url` to `"[managed by proxy]"` so the
managed upstream endpoint never leaks to the customer.

### `POST /advance/cancel`

Cancel the active advance job. Blocks that were already replayed and
rooted stay applied — there's no rollback.

```json
{ "status": "cancelling", "job": { /* AdvanceJob */ } }
```

### `GET /advance/history`

The last 20 finished advance jobs, oldest first. Resets on
validator restart.

```json
{ "jobs": [ /* AdvanceJob */ ] }
```

---

## Local-state mutations

These rewrite the validator's local fork. After any mutation,
`/status.mutation.dirty` is `true` until the next `/boot`. Other
Solana nodes will not accept blocks linked to a mutated bank.

### `POST /accounts/patch`

Overwrite or merge one or more accounts on the local fork.

```json
{
  "patches": [
    {
      "pubkey": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "mode": "merge",
      "lamports": "1000000000",
      "owner": "11111111111111111111111111111111",
      "executable": false,
      "rent_epoch": "0",
      "data_base64": "AAAA..."
    }
  ],
  "confirm_dangerous": true,
  "allow_epoch_boundary": false
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `patches` | `DashPatch[]` | yes | Non-empty. |
| `confirm_dangerous` | bool | yes | Must be `true`. Contract that prevents accidental fork corruption. |
| `allow_epoch_boundary` | bool | no (default `false`) | Allow patches that cross an epoch boundary mid-bank. Set `true` only when you understand the implications. |

`DashPatch` fields:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `pubkey` | string (base58) | yes | Account being patched. |
| `mode` | `"merge" \| "replace"` | yes | `merge` keeps existing fields you don't set; `replace` overwrites every field (omitted fields reset to defaults). |
| `lamports` | decimal u64 string or safe JSON integer | no | New lamport balance. Prefer strings so JS clients never round large values. |
| `owner` | string (base58) | no | New owner program. |
| `executable` | bool | no | Flip executable flag. |
| `rent_epoch` | decimal u64 string or safe JSON integer | no | New rent epoch. Prefer strings so `18446744073709551615` stays exact. |
| `data_base64` | string | no | Account data, base64-encoded. |

Response:

```json
{
  "request_id": "req_...",
  "base_slot": 421018102,
  "applied_slot": 421018103,
  "applied_now": true,
  "rooted": true,
  "patched_accounts": [
    {
      "pubkey": "Token...",
      "before_lamports": 980000000,
      "after_lamports": 1000000000,
      "before_account_sha256": "...",
      "after_account_sha256": "...",
      "data_len_before": 165,
      "data_len_after": 165,
      "created": false,
      "executable_changed": false
    }
  ]
}
```

### `GET /accounts/patch/history`

Up to 2 000 mutation records (newest first), spanning every patch,
splice, and program deploy.

```json
{
  "records": [
    {
      "kind": "account_patch" | "transaction_splice" | "program_deploy",
      "slot": 421018103,
      "at_unix": 1779303616,
      "pubkey": "...",
      "before_lamports": 980000000,
      "after_lamports": 1000000000,
      "before_sha256": "...",
      "after_sha256": "...",
      "data_len_before": 165,
      "data_len_after": 165,
      "created": false,
      "executable_changed": false,
      "splice_position": "block_end",
      "splice_signatures": ["<base58 sig>"],
      "splice_replaced": [],
      "program_id": null,
      "program_operation": null,
      "program_status": null,
      "elf_len": null,
      "elf_sha256": null,
      "effective_slot": null
    }
  ]
}
```

Fields not applicable to a row's `kind` are omitted (the serde
shape uses `skip_serializing_if = "Option::is_none"`).

### `POST /programs/deploy`

Upload a compiled `.so` and deploy it at any program id under
Loader-v4. Equivalent to a fresh `solana program deploy` against
the local fork, bypassing on-chain authority and cooldown. Set
`force_replace_legacy_loader: true` to convert a program currently
owned by BPFLoader / BPFLoaderUpgradeable / BPFLoaderDeprecated.

Caps: raw ELF ≤ 10 MiB, base64 ≤ 13 MiB.

Request body:

```json
{
  "program_id": "MyProg111111111111111111111111111111111111",
  "elf_base64": "<base64>",
  "status": "deployed",
  "authority_address": "AuthorityPubkey...",
  "next_version_address": null,
  "force_replace_finalized": false,
  "force_replace_legacy_loader": false,
  "confirm_dangerous": true
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `program_id` | string (base58) | yes | Pubkey where the program will live. |
| `elf_base64` | string | yes | Base64 of the ELF. |
| `status` | `"deployed" \| "finalized"` | yes | `deployed` → executable, can be upgraded. `finalized` → immutable, succeeded by `next_version_address`. |
| `authority_address` | string (base58) | when `status=deployed` | The upgrade authority for the new program. No default is synthesized. |
| `next_version_address` | string (base58) | when `status=finalized` | Pubkey of the program version that supersedes this one. |
| `force_replace_finalized` | bool | no (default `false`) | Allow overwriting an existing `finalized` program at the same id. |
| `force_replace_legacy_loader` | bool | no (default `false`) | Allow redeploying a program currently owned by BPFLoader / BPFLoaderUpgradeable / BPFLoaderDeprecated. Destructive — converts the account in place. |
| `confirm_dangerous` | bool | yes | Must be `true`. |

Response:

```json
{
  "request_id": "req_...",
  "program_id": "MyProg...",
  "operation": "created",
  "loader": "LoaderV4",
  "base_slot": 421018103,
  "materialized_slot": 421018104,
  "synthetic_deployment_slot": 421018104,
  "effective_slot": 421018105,
  "status": "deployed",
  "authority_address": "AuthorityPubkey...",
  "next_version_address": null,
  "elf_len": 17072,
  "elf_sha256": "...",
  "account_sha256_before": null,
  "account_sha256_after": "...",
  "cache_published": true,
  "visible_now": true,
  "bank_hash_preserved": true,
  "last_blockhash_preserved": true
}
```

`operation` ∈ `"created" | "replaced" | "force_replaced_finalized" | "replaced_legacy_loader"`.

### `GET /programs/deploy/history`

Same audit log as `/accounts/patch/history`, filtered to
`kind="program_deploy"` rows.

---

## Geyser plugins

### Custom Geyser plugin upload (dashboard only)

The public per-server API does **not** expose a customer-callable
`POST /plugin/upload` route. Custom plugin upload is intentionally handled
by the signed-in dashboard Boot tab, which performs the upload server-side
without exposing internal upload credentials to the browser.

Upload requirements:

- `lib` — compiled `.so` (Geyser shared library). Must start with ELF magic bytes. ≤ 256 MiB.
- `config` — JSON object. The upload flow rewrites `libpath`; keep plugin-specific settings under their normal keys.
- Build the `.so` for the runtime compatibility version reported by `/status.validator_version`.

After a dashboard upload, `GET /plugin/list` returns entries shaped like:

```json
{
  "id": "1779303616039",
  "config_path": "<opaque-uploaded-config-ref>",
  "lib_path": "<opaque-uploaded-lib-ref>",
  "name": "your-plugin"
}
```

Pass `config_path` as the `geyser` field on the next `/boot`.

### `GET /plugin/list`

```json
{
  "plugins": [
    {
      "id": "1779303616039",
      "config_path": "<opaque-uploaded-config-ref>",
      "lib_path": "<opaque-uploaded-lib-ref>",
      "name": "your-plugin",
      "uploaded_unix": 1779303616,
      "size_bytes": 17072
    }
  ],
  "active": {
    "kind": "bundled",
    "config_path": "<opaque-active-plugin-ref>",
    "name": "yellowstone"
  }
}
```

`plugins` is the list of plugins previously uploaded through the dashboard
Boot tab. `active` reports the plugin the running runtime is loading and is
omitted when no plugin is loaded. `kind` is one of:

- `bundled`  — managed preset (e.g. `yellowstone`)
- `uploaded` — a previously uploaded plugin from this environment (`name` matches
  an `id` in the `plugins` array)
- `custom`   — operator-supplied plugin reference that's neither

### `POST /plugin/delete`

```json
{ "id": "1779303616039" }
```

→ `{ "ok": true, "id": "1779303616039" }`. The bundled preset is
protected — only ids returned by `/plugin/list` are deletable.

---

## Logs

### `GET /logs/tail?lines=N`

Returns the last N runtime log lines as a
single JSON payload. Default `lines=200`, max `10000`. `lines`
is the canonical query parameter; `limit` is accepted as an alias for
API callers that use collection-style pagination vocabulary.

```json
{ "lines": ["[2026-05-20T16:41:09Z INFO ...] ...", "..."] }
```

### `GET /logs/stream`

Server-Sent Events. The stream starts with a `connected` event, then
emits one `log` event per line plus 15-second keep-alive comments while
idle.
Tail with `curl -N --no-buffer` (or any SSE client):

```bash
curl -N --no-buffer \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/logs/stream
```

The dashboard's Logs tab consumes this same endpoint.

---

## Checkpoints (snapshot / restore / replay)

Save a point-in-time snapshot of the running fork and restore (replay) any
saved one — instantly and as many times as you like. Use it to test a
transaction sequence, revert bad state, or re-run a scenario without re-booting.

### `GET /checkpoints`

List saved checkpoints (most useful right before a `/restore`).

```bash
curl -H "Authorization: Bearer <YOUR_API_KEY>" https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/checkpoints
```

Returns an array: `[{ "id": "ckpt-1779502056498", "label": "before-experiment", "slot": 421475757, "created_at": "1779502063" }]`.

### `POST /checkpoint`

Save the current fork state. Returns immediately; the snapshot finishes in the
background — poll `GET /checkpoints` for the new entry. The **first** checkpoint
also captures the base, so it takes longer than later (incremental) ones.

NOTE: the checkpoint briefly makes `/status` and RPC
momentarily unavailable (this can surface as a transient 522 at the edge) and
then recover. Treat a checkpoint as "fire, then poll `/checkpoints` until the
new entry appears and `/status` is reachable again."

```bash
curl -X POST -H "Authorization: Bearer <YOUR_API_KEY>" -H "content-type: application/json" \
  -d '{"label":"before-experiment"}' https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/checkpoint
```

Typical accepted response:

```json
{
  "accepted": true,
  "request_id": "req_...",
  "message": "in progress — poll /checkpoints (or /status) for completion"
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `label` | string | no | Human-readable name. Auto-generated if omitted. |
| `slot` | u64 | no | Slot to record. Defaults to `/status.current_slot`. |

### `POST /restore`

Rewind the fork to a saved checkpoint by id. Everything since that checkpoint
is discarded; the fork resumes on the checkpoint's slot. **Repeatable** — restore
the same point over and over. The RPC is briefly unavailable while the fork
comes back; poll `GET /status` until `rpc_listening` is true again.

```bash
curl -X POST -H "Authorization: Bearer <YOUR_API_KEY>" -H "content-type: application/json" \
  -d '{"id":"ckpt-1779502056498"}' https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/restore
```

Typical accepted response:

```json
{
  "accepted": true,
  "request_id": "req_...",
  "message": "in progress — poll /checkpoints (or /status) for completion"
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | string | yes | Checkpoint id from `GET /checkpoints` or `POST /checkpoint`. |

## Tx workbench (session-local state diffs)

Replay records the **exact pre/post account state** of
every committed non-vote transaction in a bounded in-memory cache. Use
this to diff a transaction's effect after `/advance` or a TxBuilder
splice — what changed, by how much, and what stayed the same — without
re-running anything offline.

Lifecycle:
- Reset on every `/boot`, every checkpoint `/restore`, and every
  explicit `POST /fixtures/clear`.
- Vote transactions are intentionally skipped; calling
  `GET /fixtures/tx/:vote_sig` returns `404 fixture_not_found`.
- The cache caps at **5 GiB** (FIFO eviction by insertion order),
  with **256 KiB** of inline account-data bytes per side and a
  **16 MiB** per-fixture cap (over which inline bytes are stripped
  and `cache.fixture_truncated` is set; if still too large, the
  fixture is dropped and `oversized_count` increments). When the
  total cap is reached, Replay drops the oldest
  fixtures one at a time until `bytes_used <= bytes_cap` —
  `evicted_count` rises by one per dropped fixture, capture stays
  infallible, and transaction execution never blocks on cache pressure.

### `GET /fixtures/tx/:signature`

Return the captured state-diff fixture for one transaction.

```bash
curl -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/fixtures/tx/5j…  # base58 signature, 88 chars
```

| HTTP | Error code | Meaning |
| --- | --- | --- |
| 400 | `bad_signature` | Path was not a valid base58 signature. |
| 404 | `fixture_not_found` | Valid signature; never captured (vote, evicted, never advanced, or just cleared). |
| 409 | `phase_not_ready` | Validator is not `ready`. Boot first. |
| 502 | `admin_rpc_error` | Runtime operation failed. |

Response shape (PRD §7):

```json
{
  "schema_version": 1,
  "signature": "5j...",
  "slot": "422097238",                       /* u64 as decimal string */
  "captured_at_unix_seconds": "1779725000",  /* u64 as decimal string */
  "source": "replay_validator",

  "execution": {
    "state_status": "complete",              /* | "fees_only_partial" | "not_executed" */
    "result": "success",                     /* | "executed_error" | "load_error" */
    "error": null,                           /* format!("{err:?}") of the TransactionError; null on success */
    "fee_lamports": "10152",                 /* u64 string. null for top-level load errors. */
    "compute_units_consumed": "694",         /* u64 string. null for FeesOnly + load errors. */
    "log_messages": null,                    /* string[] when execution logs were captured */
    "return_data": null,                     /* { program_id, data_base64 } | null */
    "inner_instructions": null               /* opaque serde shape; null otherwise */
  },

  "transaction": {
    "signatures": ["5j..."],
    "recent_blockhash": "...",
    "account_keys": ["..."],
    "writable_indexes": [0, 1],
    "readonly_indexes": [2, 3, 4],
    "signer_indexes": [0],
    "fee_payer_index": 0
  },

  "accounts": [
    {
      "index": 1,
      "pubkey": "...",
      "declared_writable": true,
      "signer": false,
      "fee_payer": false,
      "invoked": false,
      "instruction_account": true,
      "pre":  { "lamports": "16759680", "owner": "...", "executable": false,
                "rent_epoch": "18446744073709551615", "data_len": 2280,
                "data_sha256": "87d76a7d...", "data_hash_omitted_reason": null },
      "post": { "lamports": "16759680", "owner": "...", "executable": false,
                "rent_epoch": "18446744073709551615", "data_len": 2280,
                "data_sha256": "3962e20b...", "data_hash_omitted_reason": null },
      "changed": true,                       /* true | false | null */
      "changed_fields": ["data"],            /* subset of lamports|owner|executable|rent_epoch|data_len|data */
      "data_comparison": "changed",          /* | "equal" | "length_changed" | "unknown_too_large" | "not_available" */
      "data": { "pre_base64": "...", "post_base64": "...", "omitted_reason": null }
    }
  ],

  "cache": {
    "data_policy": "inline_small_accounts",
    "inline_account_data_max_bytes": 262144, /* 256 KiB */
    "fixture_truncated": false
  }
}
```

`changed` semantics:
- `true` — a committed scalar moved OR a known data sha256 differs.
- `false` — every scalar matched AND data is either equal or unchanged
  by Solana's commit filter.
- `null` — comparison is **unknown** because one or both sides are
  larger than 256 KiB and no hash was captured.

### `GET /fixtures/stats`

Cache health snapshot. Safe to poll.

```json
{
  "fixture_count": 1957,
  "slot_count": 5,
  "oldest_slot": "422097234",   /* u64 decimal string. null when empty. */
  "newest_slot": "422097238",   /* u64 decimal string. null when empty. */
  "bytes_used": 135975627,      /* JS number; cache cap is 5 GiB */
  "bytes_cap": 5368709120,
  "evicted_count": 0,           /* process-lifetime; FIFO drops */
  "oversized_count": 0,         /* process-lifetime; refused inserts */
  "cleared_count": 1            /* process-lifetime; /fixtures/clear calls */
}
```

### `GET /fixtures/recent`

Return newest captured fixture summaries without cloning full account
diff payloads. Use this to offer sample signatures when the workbench
badge says fixtures exist but the user does not know a signature yet.
`limit` is optional and must be between `1` and `50`.

```bash
curl -H "Authorization: Bearer <YOUR_API_KEY>" \
  "https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/fixtures/recent?limit=5"
```

```json
{
  "limit": 5,
  "returned": 1,
  "total": 1957,
  "fixtures": [
    {
      "schema_version": 1,
      "signature": "5j...",
      "slot": "422097238",
      "captured_at_unix_seconds": "1779725000",
      "state_status": "complete",
      "result": "success",
      "error": null,
      "fee_lamports": "10152",
      "compute_units_consumed": "694",
      "account_count": 4,
      "writable_account_count": 2,
      "changed_account_count": 1,
      "fixture_truncated": false,
      "size_bytes": 12144
    }
  ]
}
```

### `POST /fixtures/clear`

Drop every captured fixture for this session. `confirm:true` is
required.

```bash
curl -X POST -H "Authorization: Bearer <YOUR_API_KEY>" -H "content-type: application/json" \
  -d '{"confirm":true}' https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/fixtures/clear
# → { "cleared": true, "fixtures_removed": 1957 }
```

| HTTP | Error code | Meaning |
| --- | --- | --- |
| 400 | `confirm_required` | Body missing or `confirm` not `true`. |
| 409 | `phase_not_ready` | Validator is not `ready`. |

## Anchor IDL fetch (authenticated, not server-scoped)

Anchor's `anchor idl init` publishes a program's IDL as an on-chain
account at a deterministic address derived from the program id:

```text
base = find_program_address(&[], program_id)
idl  = create_with_seed(base, "anchor:idl", program_id)
```

The gateway exposes an authenticated read of that account so the dashboard's
**Accounts** panel and **State diff** bytes pane can decode account
data without the operator hand-uploading the IDL.

### `GET https://api.k256.xyz/v1/replay/idl/:program_id`

**Bearer required.** Use the same org-scoped `k256_live_` key as the
other Replay HTTP API calls. This route is product-level rather than
server-scoped, so it does not need a `SERVER_ID` or per-server
`REPLAY_ENDPOINT`.

```bash
curl -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.k256.xyz/v1/replay/idl/whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc
```

Response (200):

```json
{
  "program_id":       "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc",
  "idl_address":      "2KFqE4RWoPVbvodo8vbggCFeHPS8TDvgpwp79ALMrcyn",
  "authority":        "8ztMi8xauc8yhu1qQ6T27XMiTpZpCqoenNw7GjL7futn",
  "compressed_bytes": 14715,
  "json_bytes":       107776,
  "fetched_at_unix":  1779747000,
  "source":           "anchor_idl_account",
  "idl": { "address": "whirL...", "metadata": { ... }, "instructions": [...], "accounts": [...] }
}
```

| HTTP | Error code | Meaning |
| --- | --- | --- |
| 400 | `bad_program_id` | Argument is not 32-byte base58. |
| 404 | `idl_not_found` | Program does not publish an Anchor IDL (System / Token / BPF Loader / Phoenix / Kamino are common 404s). The derived IDL address is included in the message for debugging. |
| 405 | `method_not_allowed` | Only `GET` and `HEAD` are allowed. |
| 502 | `rpc_error` / `malformed_idl_account` / `inflate_failed` / `malformed_idl_json` | Upstream RPC failed or returned an account that doesn't match the Anchor layout (8-byte discriminator + 32-byte authority + u32 LE length + zlib-compressed JSON). |

Cache behaviour:
- `max-age=3600` — Cloudflare returns the cached body for an hour.
- `stale-while-revalidate=86400` — for the following 23h, stale bodies
  serve immediately while the gateway refreshes in the background.
- IDLs change at program-upgrade frequency. If you've just shipped a
  new IDL and need a fresh read, hit the endpoint from a region whose
  edge hasn't seen it yet (or wait ~1h).

Known programs that publish IDLs (this list will rot — verify):
`whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc` Orca Whirlpool,
`dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH` Drift v2,
`MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA` MarginFi,
`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` Pump.fun,
`JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4` Jupiter v6.

---

# Dashboard catalog API — `https://app.k256.xyz`

This is hosted by the dashboard, not by the gateway. It returns the
snapshot catalog under WorkOS session auth.

### `GET /api/snapshots`

Authenticated by the dashboard's WorkOS session cookie (no bearer
token). From a browser logged into the dashboard, the cookie is
sent automatically. From a terminal, copy the session cookie out of
your browser's DevTools.

Query params (all optional):

| Param | Type | Default | Notes |
| --- | --- | --- | --- |
| `cluster` | string | (default) | E.g. `mainnet`. |
| `agave_version` | string | (any) | Runtime compatibility version. Match this to `/status.validator_version` or `/boot` can fail compatibility checks. |
| `since_unix` | u64 | — | Inclusive lower bound on `scheduled_at_unix`. |
| `until_unix` | u64 | — | Inclusive upper bound. |
| `status` | `"available" \| "deleting" \| "any"` | `"available"` | Ops visibility. |
| `frozen` | `"true" \| "false"` | — | Filter to frozen rows (pinned by publisher; never auto-swept). |
| `limit` | u32 (1..1000) | 100 | Rows to return. |

Response:

```json
{
  "items": [
    {
      "id": "snap_01ks3byt5rkw1rdbwx6gzhtgy1",
      "cluster": "mainnet",
      "agave_version": "4.0.0",
      "slot": 421040195,
      "bank_hash": "5E5E4jCat39g9rSQn9JnkGujot6kZk8Djao63nkrfkLk",
      "bank_hash_short": "5E5E4jCa",
      "genesis_hash": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
      "size_bytes": 117972850398,
      "sha256": "63f90bcf78a366aeaa10388795227eb630ca9f24cc1739ce9170a9693432491b",
      "etag": "...",
      "r2_key": "<archive-key-from-catalog>",
      "canonical_filename": "snapshot-421040195-5E5E4jCat39g9rSQn9JnkGujot6kZk8Djao63nkrfkLk.tar.zst",
      "block_time_unix": 1779260000,
      "scheduled_at_unix": 1779261000,
      "scheduled_at": "2026-05-20T08:30:00Z",
      "uploaded_at_unix": 1779261600,
      "uploaded_at": "2026-05-20T08:40:00Z",
      "frozen": false,
      "status": "available",
      "notes": null
    }
  ],
  "count": 1
}
```

To boot from a row, copy the row's `slot` → `snapshot_slot`,
`r2_key`, `sha256`, `canonical_filename`, and `size_bytes` into
the `/boot` body. The `id` (e.g. `snap_…`) is optional; the
control API records it as `catalog_id`.

---

# Solana JSON-RPC & PubSub

For boot, advance, checkpoint, account patch, and program deploy
workflows, call the Replay control API at `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>`. For standard
Solana reads/subscriptions, copy the runtime endpoints from the
authenticated Replay console RPC tab or put your own HTTPS/WSS proxy
in front of them:

```bash
RPC_ENDPOINT="<COPY_FROM_REPLAY_RPC_TAB>"

curl -s -X POST \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}' \
  "$RPC_ENDPOINT"
```

Browser runtime split:

- `solana-cli`, Anchor CLI, Node `@solana/web3.js`, backend bots, and indexers can use the copied runtime endpoints.
- Local browser pages served over `http://localhost` can use the copied runtime endpoints for development.
- Production browser pages served over HTTPS need an HTTPS JSON-RPC proxy and, for PubSub, a WSS bridge. Do not instantiate browser `Connection` objects against copied runtime endpoints from an HTTPS page unless your proxy terminates HTTPS/WSS.
- The k256 console itself uses a same-origin route-bound RPC proxy for browser reads. If you build your own production dapp, implement the same pattern in your app: authenticate the caller server-side, forward JSON-RPC to the Replay runtime endpoint, and expose PubSub through a WSS bridge if subscriptions are required.

---

# Common pitfalls

1. **Don't expect block production**. `getSlot` returns the same
   value across calls until the user runs `POST /advance`. Anything
   that loops waiting for new slots will deadlock.
2. **Don't query warm-up slots as blocks**. A boot restores account
   state before block entries. On a fresh boot, the snapshot slot and
   the next warm-up slot do not have Replay block entries. The first
   replayed block is normally `snapshot_slot + 2`, and block RPC may
   require the fork head to reach `snapshot_slot + 3` before that
   first replayed block is served. If `getBlock(current_slot)`
   returns no block immediately after boot, advance to
   `snapshot_slot + 3` and load `snapshot_slot + 2`. The dashboard
   does not hide this behind an automatic post-boot advance because that
   would silently move account state past the exact snapshot the user
   selected; the Blocks tab offers the warm-up advance explicitly.
3. **Don't splice into warm-up slots**. `splices[i].slot` only has to
   be greater than `current_slot` to be accepted, but operators expect
   the result to be visible in Blocks, History, and webhook-driven
   workflows. Immediately after boot, choose
   `splice_slot = max(current_slot + 1, snapshot_slot + 2)` and
   `target_slot = max(splice_slot, snapshot_slot + 3)`.
4. **Don't send `rpc_url` through `https://api.k256.xyz`**. Omit it to use the
   managed upstream. Supplying a mismatched upstream can make advance
   results inconsistent with the Replay environment.
5. **Match snapshot compatibility version**. `/boot` verifies the
   snapshot's genesis hash. Booting an incompatible snapshot can fail in
   `extracting`/`indexing` and leave the runtime in `phase=dead`.
   Always filter the catalog by
   `/status.validator_version`.
6. **`mutation.dirty` doesn't block `/advance`** but it does
   poison the fork — no peer will ever accept blocks linked through
   a mutated bank. The only clean recovery is a fresh `/boot`.
7. **One advance at a time**. POST `/advance/cancel` before booting
   or before starting another advance.
8. **Normal `sendTransaction` is not the insertion path**. Reads and
   subscriptions use standard Solana clients, but Replay does not
   produce new blocks from ordinary client submissions. A signature will
   not confirm or appear in blocks until you insert the serialized
   transaction through `POST /advance`
   `splices` or the console's `Advance -> Send tx` workflow.
9. **Account patches are read/audit mutations, not PubSub events**.
   `POST /accounts/patch` mutates the local fork immediately for
   JSON-RPC reads and Replay History, but it does not emit synthetic
   `accountNotification` messages to existing `accountSubscribe`
   listeners. Use a follow-up read/history query to observe the patch;
   subscriptions wake when the runtime emits native events, usually
   after `POST /advance`.
10. **`sha256` is verified after download**. If your catalog row's
   sha drifts (e.g. you edited the catalog row by hand), the boot
   pipeline aborts and the runtime stays `dead`.
9. **`/programs/deploy` rejects ELFs over 10 MiB raw** with
   `elf_too_large`. Strip debug symbols and verify the ELF locally
   before uploading.
10. **Custom plugin upload performs only boundary validation.** Build
    custom plugins for the reported runtime compatibility version.
    Upload from the dashboard Boot tab, then use `GET /plugin/list`
    to confirm the saved `config_path`.

---

# End-to-end recipes

### 1. Wait for the environment to become ready

```bash
until curl -fsS -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/status | jq -e '.phase == "ready"' >/dev/null; do
  sleep 2
done
```

### 2. Pick a snapshot and boot

```bash
# Get the latest mainnet snapshot for the runtime compatibility version
VER=$(curl -s -H "Authorization: Bearer <YOUR_API_KEY>" https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/status | jq -r .validator_version)
ROW=$(curl -s --cookie "<YOUR_DASHBOARD_COOKIES>" \
  "https://app.k256.xyz/api/snapshots?cluster=mainnet&agave_version=$VER&limit=1" | jq '.items[0]')

# Build the /boot body from the row and send it
echo "$ROW" | jq '{
  snapshot_slot: .slot,
  r2_key, sha256, canonical_filename, size_bytes,
  catalog_id: .id
}' | curl -s -X POST \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "content-type: application/json" \
  --data-binary @- \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/boot
```

### 3. Prepare the first queryable block

```bash
SNAPSHOT_SLOT=$(curl -s -H "Authorization: Bearer <YOUR_API_KEY>" https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/status | jq -r .snapshot_slot)
TARGET_SLOT=$((SNAPSHOT_SLOT + 3))
curl -s -X POST \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "content-type: application/json" \
  -d "{\"target_slot\": $TARGET_SLOT, \"root\": true}" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/advance
```

### 4. Splice a transaction at the end of a future block

```bash
# tx_b64 is a base64-encoded VersionedTransaction
curl -s -X POST \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "content-type: application/json" \
  -d '{
    "target_slot": 421018110,
    "splices": [
      { "slot": 421018109, "position": "block_end", "transactions_base64": ["AQAB..."] }
    ]
  }' \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/advance
```

### 5. Patch a token account to give an address 1 SOL

```bash
curl -s -X POST \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "content-type: application/json" \
  -d '{
    "patches": [
      { "pubkey": "<YOUR_PUBKEY>", "mode": "merge", "lamports": "1000000000" }
    ],
    "confirm_dangerous": true
  }' \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/accounts/patch
```

### 6. Deploy a program

```bash
B64=$(base64 < my_program.so | tr -d '\n')
curl -s -X POST \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "content-type: application/json" \
  -d "{
    \"program_id\": \"MyProg111111111111111111111111111111111111\",
    \"elf_base64\": \"$B64\",
    \"status\": \"deployed\",
    \"authority_address\": \"AuthorityPubkey...\",
    \"confirm_dangerous\": true
  }" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/programs/deploy
```

### 7. Tail logs live

```bash
curl -N --no-buffer \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/logs/stream
```

### 8. Upload a custom Geyser plugin, then reboot with it

```bash
# 1. Upload the .so + config.json from the dashboard Boot tab.
#    Then list uploaded plugins and choose the config_path to boot with.
CFG=$(curl -s -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/plugin/list | jq -r '.plugins[] | select(.name=="my_plugin") | .config_path' | head -1)

# 2. Boot using the same descriptor as recipe 2, but pass CFG as `geyser`.
#    (Replace the snapshot fields with a real catalog row.)
curl -s -X POST \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "content-type: application/json" \
  -d "{
    \"snapshot_slot\": 421018067,
    \"r2_key\": \"<archive-key-from-catalog>\",
    \"sha256\": \"<64-hex>\",
    \"canonical_filename\": \"snapshot-421018067-<hash>.tar.zst\",
    \"size_bytes\": 117972850398,
    \"geyser\": \"$CFG\"
  }" \
  https://api.k256.xyz/v1/replay/servers/<SERVER_ID>/boot
```

---

# Rules for generating snippets

1. Use the base URL and bearer token from **Connection** above.
   Never embed the bearer in a URL query string — always the
   `Authorization` header.
2. Block height advances only on `/advance`. Snippets that wait on
   `onSlotChange` / poll until `getSlot` changes will block forever
   unless the user is also calling `/advance`.
3. Mutations (`/accounts/patch`, `/programs/deploy`, splice ops on
   `/advance`) are local and destructive. Warn the user before
   suggesting one unless they asked for it, and remind them the fork
   stays dirty until the next `/boot` reverts it.
4. Do not send standard Solana JSON-RPC to `https://api.k256.xyz/v1/replay/servers/<SERVER_ID>`; that base is
   the Replay control API. For balances, accounts, txns, and PubSub,
   use the runtime endpoints copied from the RPC tab or an HTTPS/WSS
   proxy you operate.
5. The catalog (`/api/snapshots`) is the single source of truth for
   bootable snapshots. Never invent `r2_key` / `sha256` /
   `canonical_filename` values; always fetch a row.
6. Surface error responses verbatim — the `error` kind is stable
   and dispatchable (see **Authentication and error format**).
