Eschaton

Repository docs

docs/INTERFACES.md

Rendered from the repository at request time. Back to research

eacc-swarm — cross-component interface contract (v1)

Status: frozen for parallel development (2026-10-06). Every workstream builds against this file. Additive extensions inside a component you own are fine. Anything that changes a shared shape must be appended to the Changelog at the bottom (timestamp, workstream, what, why) and called out in your final report.

1. Workstreams and path ownership

Edit only paths you own. Read anything.

| Workstream | Owns |
|---|---|
| `train` (lead: Pluralis-faithful training network, research, integration) | `node/`, `coordinator/`, `docs/RESEARCH.md`, `docs/DESIGN.md`, `README.md`, `AGENTS.md`, `Makefile`, root `.gitignore`, root `package.json` (reserved), `scripts/demo/` |
| `chain` (Solana program, SDK, settlement, localnet) | `programs/`, `Anchor.toml`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, `tests/`, `migrations/`, `sdk/`, `settlement/`, `scripts/localnet/`, `config/` |
| `fees` (trade-fee router) | `fee-router/`, `docs/FEES.md` |
| `dashboard` (web app) | `dashboard/` |

2. Clusters, config, keys

  • ›CLUSTER=localnet|devnet|mainnet (default localnet). Mainnet code paths additionally require I_UNDERSTAND_MAINNET=1 and --live. Nobody sends mainnet transactions during development; read-only mainnet RPC is fine.
  • ›Every script that can send a transaction is dry-run by default and needs --live (localnet included).
  • ›config/<cluster>.json is owned by chain. config/localnet.json is generated by bootstrap and gitignored; config/localnet.example.json, config/devnet.json, config/mainnet.json are committed.
{
  "cluster": "localnet",
  "rpcUrl": "http://127.0.0.1:8899",
  "wsUrl": "ws://127.0.0.1:8900",
  "programId": "<base58>",
  "stakeMint": "<base58>",
  "stakeTokenProgram": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
  "stakeDecimals": 6,
  "coordinatorUrl": "http://127.0.0.1:8787",
  "settlementUrl": "http://127.0.0.1:8788"
}

config/mainnet.json uses the real e/acc mint CbcyNo7m1amFWqEQm2m4PLv1UNvpcL3C1Ujm6AkzpKoU (Token-2022, 6 decimals) and "programId": "TBD".

  • ›Keys come from env, are never printed, never committed. Each is a path to a Solana JSON keypair file: ADMIN_KEYPAIR, COORDINATOR_KEYPAIR, OWNER_KEYPAIR (wallet holding the stake), NODE_KEYPAIR (hot key on the GPU machine), FEE_RECIPIENT_KEYPAIR, PAYER_KEYPAIR.
  • ›Localnet keys live in .localnet/keys/ (gitignored): admin.json, coordinator.json, fee-recipient.json, node-<i>-owner.json, node-<i>-hot.json.

2b. Localnet bootstrap (chain)

scripts/localnet/bootstrap.sh [--nodes N] [--stake] [--reset]: starts solana-test-validator (ports overridable via RPC_PORT, FAUCET_PORT, GOSSIP_PORT, DYNAMIC_PORT_RANGE, LEDGER_DIR; defaults = the shared validator in §3) → deploys eacc_swarm → creates a Token-2022 test mint (6 decimals) standing in for e/acc → initialize with the demo defaults (§4) → creates and airdrops the keys in .localnet/keys/ → mints 100,000 test e/acc to each node owner → with --stake, register_node (node_key = that node's hot key) and stake min_stake for each → writes config/localnet.json. Re-runs are idempotent; --reset wipes the ledger. scripts/localnet/stop.sh stops the validator it started.

3. Ports (so workstreams can test in parallel without collisions)

| Service | Port |
|---|---|
| Shared localnet validator (integration demo, dashboard dev) | RPC 8899, WS 8900, faucet 9900, ledger `.localnet/ledger` |
| `chain`'s own test validator (if not using LiteSVM/bankrun) | `--rpc-port 18899 --faucet-port 19900 --gossip-port 18001 --dynamic-port-range 18002-18200 --ledger .localnet/ledger-chain-tests` |
| Coordinator HTTP | 8787 |
| Settlement proof server | 8788 |
| Dashboard | 3000 (dashboard's own testing: 3100) |
| Training P2P (DHT / libp2p / RPC between nodes) | 31330–31399 |

4. On-chain program eacc_swarm (Anchor)

Program ID: whatever target/deploy/eacc_swarm-keypair.json yields. Consumers read it from config/<cluster>.json or the IDL address. Never hardcode it.

Units: stake in base units of a 6-decimal mint (u64); rewards in lamports (u64).

PDAs (seeds are UTF-8 literals; integers are u64 little-endian)

  • ›Config: ["config"]
  • ›Stake vault (token account for stake_mint, authority = Config PDA, owned by the mint's token program): ["stake_vault"]
  • ›Reward vault (program-owned account holding SOL; anyone may also transfer SOL to it directly): ["reward_vault"]
  • ›e/acc lock (system-owned lamport account, no data; receives the 10% fee share; no v1 instruction can move lamports out, v2 adds buy_and_lock): ["eacc_lock"]
  • ›Node: ["node", owner]
  • ›Epoch: ["epoch", epoch_id_le]
  • ›Claim bitmap (v2; replaces v1's ["claim", epoch_id_le, owner] receipt): ["claims", owner, page_le_u16] with page = epoch_id / 1024; bit epoch_id % 1024 is set once the owner claimed that epoch
  • ›e/acc buy-and-lock (v2; docs/V2.md §3): ["lock_config"], ["eacc_twap"], ["lock_wsol"], ["eacc_locked"]

Account layouts (field order is part of the contract; Python decodes by offset)

All accounts start with Anchor's 8-byte discriminator; offsets below include it. Borsh, no padding.

  • ›Config: admin Pubkey @8 · coordinator Pubkey @40 · stake_mint Pubkey @72 · stake_token_program Pubkey @104 · treasury Pubkey @136 · min_stake u64 @168 · unbonding_seconds i64 @176 · current_epoch u64 @184 (last posted; 0 = none) · total_staked u64 @192 · total_rewards_funded u64 @200 · total_rewards_claimed u64 @208 · outstanding_rewards u64 @216 (posted but unclaimed) · paused u8 @224 · slash_destination u8 @225 (0 = burn, 1 = treasury) · bump u8 @226 · stake_vault_bump u8 @227 · reward_vault_bump u8 @228 · slash_delay_seconds i64 @229 · max_payout_bps u16 @237 · fault_cap_bps u16 @239 · fraud_cap_bps u16 @241 · min_epoch_seconds i64 @243 · challenge_seconds i64 @251 · claim_expiry_seconds i64 @259 · last_epoch_posted_at i64 @267 · reserved [u8;18] @275 · pending_admin Pubkey @293 (default = none) · pending PendingConfig @325..414 (docs/V2.md §1) · reserved_tail [u8;64] @414 — total 478 bytes
  • ›NodeAccount: owner Pubkey @8 · node_key Pubkey @40 · staked u64 @72 · pending_unstake u64 @80 · unstake_available_at i64 @88 · lifetime_points u64 @96 · total_claimed u64 @104 · slashed_total u64 @112 · registered_at i64 @120 · status u8 @128 (0 = active, 1 = jailed) · bump u8 @129 · pending_slash u64 @130 · pending_slash_executable_at i64 @138 · pending_slash_reason u8 @146 · pending_slash_approved u8 @147 · last_slash_epoch u64 @148 · slash_count u32 @156 · reserved [u8;34] @160 — total 194 bytes
  • ›Epoch: id u64 @8 · merkle_root [u8;32] @16 · total_rewards u64 @48 · total_points u64 @56 · claimed u64 @64 · num_nodes u32 @72 · posted_at i64 @76 · bump u8 @84 · claims_open_at i64 @85 · expires_at i64 @93 · status u8 @101 (0 open, 1 voided, 2 released) · reserved [u8;32] @102 — total 134 bytes
  • ›ClaimBitmap: owner Pubkey @8 · page u16 @40 · bits [u8;128] @42 (bit i = byte i/8, LSB first = epoch page×1024 + i) · bump u8 @170 — total 171 bytes
  • ›RewardVault: bump u8 @8
  • ›LockConfig (238 bytes) and EaccTwap (129 bytes): docs/V2.md §3

Instructions (Anchor snake_case names; args in this order)

| ix | signer | args | effect |
|---|---|---|---|
| `initialize` | admin (payer) | `params: InitializeParams { coordinator, min_stake, unbonding_seconds, slash_destination, slash_delay_seconds, max_payout_bps, fault_cap_bps, fraud_cap_bps, min_epoch_seconds, challenge_seconds, claim_expiry_seconds }`; the treasury is the optional `treasury` token account (its key is stored) | creates Config, stake vault, reward vault; records the mint's token program (Token or Token-2022); rejects a mint with a freeze authority or unlisted extensions |
| `register_node` | owner | `node_key: Pubkey` | creates NodeAccount |
| `set_node_key` | owner | `node_key: Pubkey` | rotates the hot key |
| `stake` | owner | `amount: u64` | `transfer_checked` owner token account → stake vault; fails while paused |
| `request_unstake` | owner | `amount: u64` | staked → pending_unstake; `unstake_available_at = now + unbonding_seconds`; pending stake stops counting toward eligibility but stays slashable |
| `withdraw` | owner | — | after unbonding, pending_unstake → owner token account |
| `fund_rewards` | anyone | `lamports: u64` | SOL → reward vault; increments total_rewards_funded |
| `post_epoch` | coordinator | `epoch_id: u64, merkle_root: [u8;32], total_rewards: u64, total_points: u64, num_nodes: u32` | requires `epoch_id == current_epoch + 1`, `total_rewards <= max_payout_bps × available` and `min_epoch_seconds` since the last post; creates Epoch with `claims_open_at` / `expires_at`; `outstanding_rewards += total_rewards` |
| `claim` | owner (+ payer) | `epoch_id: u64, amount: u64, points: u64, proof: Vec<[u8;32]>` | epoch open and `claims_open_at ≤ now < expires_at`; verifies the leaf (§5); sets the owner's ClaimBitmap bit (a double claim fails with `AlreadyClaimed`); lamports reward vault → owner; `lifetime_points += points` (the model-ownership ledger) |
| `slash` | coordinator | `amount: u64, reason: u8` (1–5) | not paused, no pending slash, one per node per epoch; delay 0: takes up to `fault_cap_bps` of staked + pending at once; delay > 0: schedules and jails (docs/V2.md §2) |
| `execute_slash` / `approve_slash` / `cancel_slash` | anyone / admin / admin | — | apply a matured pending slash (capped) / let a fraud slash reach `fraud_cap_bps` / drop it (node stays jailed) |
| `set_config` | admin | `update: ConfigUpdate` (all `Option`: coordinator, min_stake, unbonding_seconds, paused, slash_destination, treasury, slash_delay_seconds, max_payout_bps, fault_cap_bps, fraud_cap_bps, min_epoch_seconds, challenge_seconds, claim_expiry_seconds); optional `treasury` account | coordinator, min_stake, paused and tightening values apply now; loosening values are queued for `unbonding_seconds + 1 day` |
| `apply_config` / `cancel_config` | anyone / admin | — | apply or drop the queued change |
| `propose_admin` / `accept_admin` / `cancel_admin` | admin / pending admin / admin | `new_admin: Pubkey` / — / — | two-step admin rotation (v1 `set_admin` removed) |
| `void_epoch` / `release_expired` | admin / anyone | `epoch_id: u64` | revoke an open epoch / close an expired one; both return the unclaimed remainder to `available` |
| `init_lock`, `set_lock_config`, `observe_price`, `buy_and_lock` | admin, admin, anyone, anyone | docs/V2.md §3 | spend `eacc_lock` SOL on e/acc and lock it |
  • ›available = reward_vault.lamports − rent_exempt_minimum(reward_vault) − outstanding_rewards
  • ›Eligibility (checked by the coordinator at auth and at epoch close, and by settlement): node.staked >= config.min_stake && node.status == 0 && config.paused == 0
  • ›Events: Staked, UnstakeRequested, Withdrawn, RewardsFunded, EpochPosted, Claimed, Slashed (v2 additions: docs/V2.md §8)
  • ›Slash reason codes: 1 spot-check, 2 outlier, 3 manual, 4 fault, 5 fraud (only 5 can be approved above the fault cap)
  • ›Localnet demo defaults: min_stake = 10_000 e/acc (10_000_000_000 base units), slash_destination = 0 (burn), and the SDK's LOCALNET_POLICY: unbonding_seconds = 86_400, no slash delay, max_payout_bps = 10_000, no epoch spacing or challenge window, claim_expiry_seconds = 15_552_000. Production: docs/V2.md §10.

5. Reward merkle tree (Rust, TypeScript and Python must agree byte for byte)

  • ›Hash: SHA-256 (solana_program::hash::hashv on-chain).
  • ›Leaf: sha256(0x00 || epoch_id u64 LE || owner [32] || amount u64 LE || points u64 LE)
  • ›Internal node: sha256(0x01 || min(a,b) || max(a,b)), with lexicographic byte order. Pairs are sorted, so proofs carry no direction bits.
  • ›Build: sort leaves by owner bytes ascending. An odd node at the end of a level is promoted unchanged. A single leaf is the root, with an empty proof.
  • ›chain publishes test vectors at sdk/test-vectors/merkle.json (≥ 5 leaves: root plus every proof). Every other implementation must pass them.

6. Epochs, scoring, allocation

  • ›Epoch length is a coordinator setting (demo: 5 min or manual close; production: 24 h). Epoch ids start at 1 and match on-chain ids.
  • ›Points are u64 integers of verified work. The definition belongs to train and is documented in docs/DESIGN.md. A node that fails a spot-check in an epoch gets 0 points for that epoch.
  • ›Allocation (settlement): total_rewards = floor(available × PAYOUT_FRACTION) (default 1 for localnet demos; production streams 1/60 per 24 h epoch); amount_i = floor(total_rewards × points_i / Σpoints). Ineligible and zero-point nodes are excluded. Dust stays in the vault and rolls over.

7. Coordinator HTTP API (train implements; base http://127.0.0.1:8787, JSON, CORS enabled)

All u64 chain quantities (points, lamports, token amounts) are decimal strings in JSON. ML metrics are numbers. Times are unix seconds.

  • ›GET /api/health → { "ok": true, "version": "..." }
  • ›GET /api/network → { "run_id", "model": { "name", "params", "n_stages", "seq_len", "compression" }, "stages": [{ "stage", "layers", "nodes": [{ "owner", "node_key", "status", "tokens_processed", "last_seen" }] }], "nodes_online", "total_tokens_processed", "tokens_per_second", "current_step" }
  • ›GET /api/metrics/loss?since=<step> → { "points": [{ "step", "loss", "tokens", "ts" }] }
  • ›GET /api/nodes → [{ "owner", "node_key", "stage", "status": "online|offline|jailed", "points_epoch", "points_lifetime", "spot_checks": { "passed", "failed" }, "joined_at", "last_seen" }]
  • ›GET /api/epochs/current → { "epoch_id", "started_at", "ends_at", "points": { "<owner>": "<points>" } }
  • ›GET /api/epochs/{id}/scores → { "epoch_id", "closed": true, "total_points", "scores": [{ "owner", "points" }], ..., "attestation"? } (closed epochs only; this is the settlement input)
  • ›Signed scores. When the coordinator has COORDINATOR_KEYPAIR (required outside localnet), the record carries "attestation": { "scheme": "ed25519", "domain": "eacc-swarm-scores:v1", "cluster", "program_id", "coordinator", "message_sha256", "signature" }.
  • ›signature is base58 ed25519 by coordinator over the UTF-8 bytes of the canonical JSON of { "cluster", "coordinator", "domain", "program_id", "scores": <the record without "attestation"> }.
  • ›Canonical JSON: keys sorted, separators , and : with no whitespace, ASCII only (\uXXXX escapes), no floats, integers below 2^53 (u64 quantities are decimal strings).
  • ›message_sha256 is the hex SHA-256 of those bytes.
  • ›Settlement must check coordinator == Config.coordinator, the cluster and program id, the digest and the signature (python -m coordinator.attest verify).
  • ›Admin endpoints (POST /api/epochs/close, /api/admin/unjail { owner }, /api/admin/points { owner, action: "exclude"|"restore", note? }, /api/admin/operators { roles, label, ttl_seconds? } → { credential, token } (the token is shown once), /api/admin/operators/revoke { id }) require a signed request:
  • ›headers X-Eacc-Signer (base58 pubkey, must be Config.admin or Config.coordinator), X-Eacc-Timestamp (unix seconds, within ±60 s), X-Eacc-Nonce (32–64 lowercase hex, single use) and X-Eacc-Signature (base58 ed25519);
  • ›signed message: eacc-swarm-admin:v1:<cluster>:<origin>:<METHOD>:<path>:<timestamp>:<nonce>:<hex sha256 of the raw body>.
  • ›Errors are 401 or 403 with a reason (signed_request_required, stale_admin_request, not_admin, bad_admin_signature, admin_nonce_used).
  • ›X-Admin-Token is accepted only in the coordinator's localnet --dev mode.
  • ›Client: python -m coordinator.admin; signer helper: node0.eacc.protocol.admin_headers.
  • ›POST /api/auth/operator { peer_public_key, role } with X-Admin-Token: <operator credential> → trainer or seed bearer token.
  • ›Read-only: GET /api/cases, GET /api/routing/bans, GET /api/admission, and GET /api/rechecks (trainer bearer). See POLICY.md.
  • ›Node auth (mirrors Node0's auth server, but checks stake instead of a login):
  • ›POST /api/auth/challenge { "owner", "node_key" } → { "nonce", "expires_at" }. Only for registered nodes (403 not_registered / node_key_mismatch); rate-limited (429 too_many_challenges).
  • ›v2 (required): POST /api/auth/verify { "auth_version": 2, "owner", "node_key", "nonce", "expires_at", "cluster", "origin", "peer_id", "peer_public_key", "signature", "peer_signature", "role"?, ... }.
  • ›Signed message: eacc-swarm-auth:v2:<cluster>:<origin>:<node_key>:<peer_id>:<nonce>:<expires_at>. origin is the RFC 6454 origin of the coordinator URL (scheme://host[:port], lowercase, default port dropped).
  • ›signature: base58 ed25519 by node_key. peer_signature: base64 RSA-PSS (SHA-256, MGF1-SHA256, max salt) by the libp2p identity key (peer_public_key, OpenSSH RSA).
  • ›The PeerID is derived from peer_public_key and must equal peer_id.
  • ›Node helper: node0.eacc.protocol.request_stake_credential.
  • ›v1 (signature over "eacc-swarm-auth:" + nonce, no auth_version) is accepted only with AUTH_LEGACY_V1=1 or --dev, on localnet.
  • ›The coordinator reads NodeAccount ["node", owner] and Config over RPC and applies the §4 eligibility rule. Success → { "access_token", "expires_at", "stage" }. Failure → 403 with a reason (auth_v2_required, cluster_mismatch, origin_mismatch, peer_proof_required, bad_peer_signature, run_full, ...).

8. Settlement (chain implements, TypeScript)

  • ›swarmctl close-epoch --epoch <id> [--scores-file <path>] [--live]: fetch GET {coordinatorUrl}/api/epochs/{id}/scores (or read --scores-file) → re-check eligibility on-chain → compute allocations (§6) → build the tree (§5) → write settlement/out/epoch-<id>.json → with --live, send post_epoch signed by COORDINATOR_KEYPAIR.
  • ›settlement/out/epoch-<id>.json: { "epoch_id", "merkle_root": "<hex>", "total_rewards", "total_points", "claims": [{ "owner", "amount", "points", "proof": ["<hex>"] }] }
  • ›Proof server on 8788 (swarmctl serve-proofs): GET /api/claims/{owner} → { "owner", "claims": [{ "epoch_id", "amount", "points", "proof", "claimed": bool }] } · GET /api/epochs → summaries · GET /api/epochs/{id} → the epoch file

8b. swarmctl CLI (chain implements in settlement/; used by demo scripts and by the coordinator for slashing)

Cluster from CLUSTER, keys from env (§2), dry-run unless --live, --json prints a machine-readable summary: init, status [--owner <pk>], register --node-key <pk>, stake --amount <ui>, unstake --amount <ui>, withdraw, fund --sol <n>, close-epoch ..., serve-proofs, claim --epoch <id>, slash --owner <pk> --amount <ui> --reason <code> (COORDINATOR_KEYPAIR). v2 adds slash-execute, slash-approve, slash-cancel, set-config (queues loosening fields), config-apply, config-cancel, admin-propose, admin-accept, admin-cancel, void-epoch, release-expired, lock-init, lock-config, lock-observe, lock-buy, lock-crank (alias lock crank; exit 0 with action: none when idle) (docs/V2.md §9). Global flags --emit-unsigned, --authority <pk>, --fee-payer <pk>; without --live every command simulates without keys, mainnet included, resolving signers from <ROLE>_PUBKEY or the chain (docs/V2.md §11).

9. TypeScript SDK (chain implements)

Package @eacc-swarm/sdk in sdk/, consumed via "@eacc-swarm/sdk": "file:../sdk". Must work in Node and the browser.

  • ›loadClusterConfig(cluster) (Node only; reads config/<cluster>.json)
  • ›PDAs: configPda(programId), stakeVaultPda(programId), rewardVaultPda(programId), eaccLockPda(programId), nodePda(programId, owner), epochPda(programId, epochId), claimsPda(programId, owner, epochId) (v2; claimReceiptPda is a deprecated shim), lockConfigPda, eaccTwapPda, lockWsolPda, eaccLockedPda
  • ›Instruction builders returning TransactionInstruction: initializeIx, registerNodeIx, setNodeKeyIx, stakeIx, requestUnstakeIx, withdrawIx, fundRewardsIx, postEpochIx, claimIx, slashIx, setConfigIx, plus v2 executeSlashIx, approveSlashIx, cancelSlashIx, applyConfigIx, cancelConfigIx, proposeAdminIx, acceptAdminIx, cancelAdminIx, voidEpochIx, releaseExpiredIx, initLockIxs, setLockConfigIx, observePriceIx, buyAndLockIx (setAdminIx removed)
  • ›Fetchers: fetchConfig, fetchNode, fetchEpoch, fetchIsClaimed, fetchClaimStatuses, fetchClaimBitmap, fetchLockConfig, fetchEaccTwap (fetchClaimReceipt is a deprecated bitmap-backed shim), fetchRewardVaultState (→ { lamports, rentExempt, outstanding, available })
  • ›Merkle: leafHash, buildTree(leaves) → { root, proofs }, verifyProof
  • ›getMintTokenProgram(connection, mint)
  • ›Until the SDK lands, consumers put stubs with these exact names behind a thin adapter, so swapping in the real package is mechanical.

10. Dashboard env

NEXT_PUBLIC_CLUSTER, NEXT_PUBLIC_RPC_URL, NEXT_PUBLIC_PROGRAM_ID, NEXT_PUBLIC_STAKE_MINT, NEXT_PUBLIC_COORDINATOR_URL, NEXT_PUBLIC_SETTLEMENT_URL, NEXT_PUBLIC_MOCK=1 (fully mocked data, so the dashboard runs with no backend).

11. Shared-repo git etiquette

  • ›Stage only your own paths (git add <path>). Never git add -A / git add .. Never reset, checkout, stash, clean, rebase, or amend anything that touches another workstream. If .git/index.lock exists, wait a few seconds and retry.
  • ›The git index is shared, so always commit with a pathspec: git commit -m "<prefix>: …" -- <your paths>. A bare git commit also sweeps up files another workstream has staged.
  • ›Commit prefixes: train:, chain:, fees:, dashboard:, docs:.
  • ›Never commit keys, .localnet/, .env* (except .env.example), node_modules/, target/, .venv/, model checkpoints, or datasets.
  • ›Don't run npm install at the repo root and don't create a root package.json (reserved for train during integration).

Changelog

  • ›2026-10-06 v1 — initial contract.
  • ›2026-10-06 13:12 UTC chain — §4 behaviour clarifications (no account layout, PDA, arg or event changes): (1) post_epoch and claim also fail with Paused while paused, not only stake, so the pause switch stops vault outflows during an incident; request_unstake/withdraw stay open so owners can always exit. (2) initialize requires the admin signer to be the program's upgrade authority, which stops front-running the one-time init after deploy. (3) claim takes a payer signer for the fee and the ClaimReceipt rent; the SDK's claimIx defaults it to the owner, so existing callers are unaffected. (4) Additive admin ix unjail_node (SDK unjailNodeIx, swarmctl unjail). It resets jailed → active; eligibility still needs staked >= min_stake.
  • ›2026-10-06 13:40 UTC coordinator — fee source correction (user clarification). The reward vault is funded by the trading fees of the project's OWN token (name/ticker TBD; it doesn't exist yet), not by e/acc's fees. e/acc's fees are irrelevant to the protocol. e/acc's only role is the stake mint: deposit it to run a node. Intended launch config: the project token launches on pump.fun with its creator-fee sharing config paying 100% (or a documented share) to the ["reward_vault"] PDA, and the sharing admin is then revoked, so the routing is permanent and needs no trusted key. Pump's distribution is a permissionless crank, and docs/FEES.md shows a PDA can be a recipient. No §4 program changes: direct SOL transfers already count toward available. The fee router targets the project token via FEE_TOKEN_MINT (unset until launch). UI and docs say "<project token> trading fees → node rewards; stake e/acc to participate".
  • ›2026-10-06 13:47 UTC coordinator — brand: Eschaton / $ESCHAT (user decision; see docs/NAMING.md). The product and its token are both "Eschaton", ticker ESCHAT. User-facing copy says "ESCHAT trading fees pay the nodes; stake e/acc to run one". Internal names (eacc-swarm, @eacc-swarm/sdk, eacc_swarm, env vars, PDAs) are unchanged.
  • ›2026-10-06 14:10 UTC coordinator — fee-sharing accounting and trust (from fees 0ebd9dd). No §4 changes. (1) pump fee-sharing payouts reach the vault as direct lamport transfers: they raise available but not total_rewards_funded, which counts fund_rewards calls only. Lifetime vault inflow is vault lamports − rent-exempt reserve + total_rewards_claimed; anything labelled "total funded" or "all-time fees" must use that, not total_rewards_funded. (2) Correction to 13:40: "admin revoked" binds the creator and the sharing-admin key, not pump.fun, which keeps protocol powers (admin_cto_sharing_config can install a new admin, fee-schedule changes, program upgrades). Our program's upgrade authority also controls the vault. Copy may say the creator and team can't redirect the fees; it must not say the route "can't change" or "needs no trusted key". (3) Launch is one atomic tx (create_v2 → create_fee_sharing_config → update_fee_shares), and the update itself revokes the admin, so there is no separate revoke step. The program must be deployed and initialized under its permanent mainnet program id first: the sharing config pays that PDA forever.
  • ›2026-10-06 14:27 UTC coordinator — user decisions: ticker $ESC and a 90/10 fee split (docs/TOKENOMICS.md). (1) The token is Eschaton, ticker ESC (supersedes ESCHAT from 13:47). User-facing copy: "ESC trading fees pay the nodes; stake e/acc to run one". Internal names are unchanged. (2) Launch fee shares, locked in the launch tx: ["reward_vault"] 9,000 bps and ["eacc_lock"] 1,000 bps, no treasury share. ["eacc_lock"] is a system-owned lamport account (no data) at that PDA of this program. No v1 instruction can move lamports out of it; a v2 buy_and_lock instruction (rate-limited, price-guarded) will buy e/acc with it and lock the tokens. Prefund it to the 0-byte rent-exempt minimum (890,880 lamports) before launch so a fee distribution can never fail on it. (3) Production settings: settlement PAYOUT_FRACTION = 1/60 per 24 h epoch (localnet demos keep 1); min_stake 50,000 e/acc (50_000_000_000 base units), unbonding_seconds 1,209,600 (14 days), slashed stake burned; slash 25% of stake for faults and 100% for fraud. (4) Privacy: never write the user's personal holdings, balances or wallet addresses into the repo. Generic advice such as "disclose team holdings" is fine.
  • ›2026-10-06 15:20 UTC coordinator — v1 tooling landed (chain 84d5d22, fees 665461c); corrections to 14:27. (1) The 0-byte rent-exempt minimum is cluster-dependent: mainnet currently 650,240 lamports, localnet 890,880. fee-router prefund and status read it from the cluster, so don't hard-code either value. (2) With real-length metadata the launch tx is 1,249–1,300 bytes, over the 1,232 limit, so it needs an address lookup table (652 bytes with a dedicated table; launch-plan --lookup-table simulates against existing ones). (3) New surface: SDK eaccLockPda; optional payoutFraction in config/<cluster>.json (default 1); config/mainnet.example.json with the production settings; swarmctl init reads config; swarmctl slash --reason fault|fraud (reason codes 4 and 5) slashes 25% or 100% of current stake.
  • ›2026-10-06 16:02 UTC train — coordinator hardening: auth v2, signed admin requests, signed scores (§7, docs/POLICY.md). (1) Node auth v2: the hot key and the libp2p key both sign eacc-swarm-auth:v2:<cluster>:<origin>:<node_key>:<peer_id>:<nonce>:<expires_at>, and the PeerID is derived from the presented RSA key. v1 only with AUTH_LEGACY_V1=1 or --dev on localnet. Challenges are rate-limited and issued only to registered nodes. The node helper is node0.eacc.protocol.request_stake_credential; StakeAuthorizer still has to delegate to it (node/ owner). (2) Admin endpoints (epoch close, unjail, points edits, operator credentials) need an ed25519 request signed by Config.admin or Config.coordinator: X-Eacc-Signer/Timestamp/Nonce/Signature over eacc-swarm-admin:v1:<cluster>:<origin>:<METHOD>:<path>:<ts>:<nonce>:<sha256(body)>. Accepted requests are audited in admin-audit.jsonl. Trainers and seeds use revocable operator credentials. COORDINATOR_ADMIN_TOKEN only works in localnet --dev. (3) Closed epoch scores carry attestation (ed25519 by the coordinator key over canonical JSON with the domain eacc-swarm-scores:v1, cluster and program id). Settlement should verify it against Config.coordinator. (4) Slashing: no single trainer verdict slashes. Confirmed cases call SLASH_CMD with an explicit --amount (25% fault, 100% fraud of staked + pending) and --reason 4|5, at most once per owner per epoch. SLASH_AMOUNT was removed. Outside localnet the coordinator now needs COORDINATOR_ORIGINS and COORDINATOR_KEYPAIR, and the stake check defaults to rpc.
  • ›2026-10-06 16:31 UTC chain — v2: SECURITY.md fixes, slash delay, e/acc buy-and-lock (branch chain/v2-slash-delay-buy-lock; details, per-SEC status and decisions in docs/V2.md). v2 is the first mainnet deploy, so accounts grew; every v1 field offset is unchanged. §4, §8b and §9 above are updated. (1) Layouts. Config 293 → 478: policy fields @229..275, pending_admin @293, PendingConfig @325..414. NodeAccount stays 194: slash state @130..160. Epoch 85 → 134: claims_open_at @85, expires_at @93, status @101. New ClaimBitmap (171 bytes) at ["claims", owner, page u16 LE], one per owner per 1,024 epochs; it replaces `ClaimReceipt` and the `["claim", epoch, owner]` PDA. New LockConfig (238) and EaccTwap (129) at ["lock_config"] / ["eacc_twap"], plus ["lock_wsol"] and ["eacc_locked"] token accounts. (2) Instructions. initialize(InitializeParams) and set_config(ConfigUpdate) take structs and an optional treasury account; set_admin → propose_admin / accept_admin / cancel_admin; new execute_slash, approve_slash, cancel_slash, apply_config, cancel_config, void_epoch, release_expired, init_lock, set_lock_config, observe_price, buy_and_lock. Errors 6023–6055 and new events are additive. (3) Behaviour. Only the coordinator slashes, reason 1–5, once per node per epoch, never while paused or while a slash is pending, capped at fault_cap_bps; above that only an admin-approved fraud slash. With a slash delay the node is jailed at once and request_unstake / withdraw / unjail_node fail with SlashPending until it is executed or cancelled (the one exception to the 13:12 "owners can always exit"). post_epoch is capped at max_payout_bps of available and spaced by min_epoch_seconds; claims open challenge_seconds after posting and expire after claim_expiry_seconds; the admin can void_epoch a bad root. Loosening config changes wait unbonding_seconds + 1 day; unbonding_seconds is bounded to 1–30 days. initialize rejects stake mints with a freeze authority or unlisted extensions (mainnet e/acc passes). (4) Production values (config/mainnet.example.json, SDK PRODUCTION_POLICY): unbonding 14 days, slash delay 48 h, max_payout_bps 167 (≥ the 1/60 payoutFraction), fault cap 2,500 bps, fraud cap 10,000 bps, epoch spacing 23 h, challenge 24 h, claim expiry 180 days. Localnet (LOCALNET_POLICY): unbonding 1 day (was 60 s), no slash delay, no payout cap, no spacing or challenge. (5) Consumers. Python decoders read only v1 offsets and keep working. The proof server (server.ts), the dashboard and the coordinator must move from ClaimReceipt to the bitmap and handle pending slashes; the SDK keeps deprecated claimReceiptPda / fetchClaimReceipt shims until then (docs/V2.md §9).
  • ›2026-10-06 16:56 UTC train — coordinator follows v2 pending slashes (additive, docs/POLICY.md §4.5–4.7). GET /api/slash-proposals statuses are now queued running skipped failed dry_run proposed pending approved executed cancelled expired, reconciled from NodeAccount/Config: `executed` now means seen on-chain (slashed_total grew), not "the command exited 0" (that is proposed). New fields: history[], skip_reason, chain_epoch, chain_before, chain_policy, chain, expected_base_units, executable_at, pending_base_units, needs_approval, approval (awaiting_multisig | approved | unavailable_no_delay), approve {command, squads_command, squads_transaction_base58?, squads_message_base58?}, hold {cancel_cmd, squads_command}, executed_base_units, capped, execute_cmd, execute_error. New GET /metrics (Prometheus) and slashing in /api/health. The coordinator runs swarmctl slash-execute (crank) and slash-approve --emit-unsigned --json (reads unsigned.transactionBase58 / messageBase58).
  • ›2026-10-06 17:00 UTC chain — launch tooling: Squads/unsigned mode, keyless dry runs, lock crank (§8b). (1) swarmctl's --emit-unsigned prints an unsigned base58 transaction plus a decoded summary for a Squads vault; --authority / --fee-payer and ADMIN_PUBKEY, COORDINATOR_PUBKEY, OWNER_PUBKEY, NODE_PUBKEY, PAYER_PUBKEY name signers without key files. (2) Dry runs no longer need keys and run on mainnet too (simulation only); --live on mainnet still needs I_UNDERSTAND_MAINNET=1. The settle job no longer skips mainnet dry runs. (3) lock-crank / swarmctl lock crank: observe_price then buy_and_lock, exit 0 when idle; --json adds action: buy|none and reason. (4) Cluster config gains an optional lock object (maxBuyReserveBps, maxDeviationBps, slippageBps, emaAlphaBps, minObserveIntervalSlots, maxPriceAgeSlots, minBuyIntervalSeconds, dailyBudgetDivisor, maxSolPerBuy, minSolPerBuy, lookupTable), used by lock-config --from-config and the crank. SDK PumpPoolState gains virtualQuoteReserves (offset 245). No on-chain change.