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(defaultlocalnet). Mainnet code paths additionally requireI_UNDERSTAND_MAINNET=1and--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>.jsonis owned bychain.config/localnet.jsonis generated by bootstrap and gitignored;config/localnet.example.json,config/devnet.json,config/mainnet.jsonare 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]withpage = epoch_id / 1024; bitepoch_id % 1024is 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) andEaccTwap(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'sLOCALNET_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::hashvon-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.
- ›
chainpublishes test vectors atsdk/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
trainand is documented indocs/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" }. - ›
signatureis base58 ed25519 bycoordinatorover 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 (\uXXXXescapes), no floats, integers below 2^53 (u64 quantities are decimal strings). - ›
message_sha256is 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 beConfig.adminorConfig.coordinator),X-Eacc-Timestamp(unix seconds, within ±60 s),X-Eacc-Nonce(32–64 lowercase hex, single use) andX-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-Tokenis accepted only in the coordinator's localnet--devmode. - ›Client:
python -m coordinator.admin; signer helper:node0.eacc.protocol.admin_headers. - ›
POST /api/auth/operator { peer_public_key, role }withX-Admin-Token: <operator credential>→ trainer or seed bearer token. - ›Read-only:
GET /api/cases,GET /api/routing/bans,GET /api/admission, andGET /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 (403not_registered/node_key_mismatch); rate-limited (429too_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>.originis the RFC 6454 origin of the coordinator URL (scheme://host[:port], lowercase, default port dropped). - ›
signature: base58 ed25519 bynode_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_keyand must equalpeer_id. - ›Node helper:
node0.eacc.protocol.request_stake_credential. - ›v1 (
signatureover"eacc-swarm-auth:" + nonce, noauth_version) is accepted only withAUTH_LEGACY_V1=1or--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]: fetchGET {coordinatorUrl}/api/epochs/{id}/scores(or read--scores-file) → re-check eligibility on-chain → compute allocations (§6) → build the tree (§5) → writesettlement/out/epoch-<id>.json→ with--live, sendpost_epochsigned byCOORDINATOR_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; readsconfig/<cluster>.json) - ›PDAs:
configPda(programId),stakeVaultPda(programId),rewardVaultPda(programId),eaccLockPda(programId),nodePda(programId, owner),epochPda(programId, epochId),claimsPda(programId, owner, epochId)(v2;claimReceiptPdais a deprecated shim),lockConfigPda,eaccTwapPda,lockWsolPda,eaccLockedPda - ›Instruction builders returning
TransactionInstruction:initializeIx,registerNodeIx,setNodeKeyIx,stakeIx,requestUnstakeIx,withdrawIx,fundRewardsIx,postEpochIx,claimIx,slashIx,setConfigIx, plus v2executeSlashIx,approveSlashIx,cancelSlashIx,applyConfigIx,cancelConfigIx,proposeAdminIx,acceptAdminIx,cancelAdminIx,voidEpochIx,releaseExpiredIx,initLockIxs,setLockConfigIx,observePriceIx,buyAndLockIx(setAdminIxremoved) - ›Fetchers:
fetchConfig,fetchNode,fetchEpoch,fetchIsClaimed,fetchClaimStatuses,fetchClaimBitmap,fetchLockConfig,fetchEaccTwap(fetchClaimReceiptis 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>). Nevergit add -A/git add .. Neverreset,checkout,stash,clean,rebase, or amend anything that touches another workstream. If.git/index.lockexists, wait a few seconds and retry. - ›The git index is shared, so always commit with a pathspec:
git commit -m "<prefix>: …" -- <your paths>. A baregit commitalso 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 installat the repo root and don't create a rootpackage.json(reserved fortrainduring 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_epochandclaimalso fail withPausedwhile paused, not onlystake, so the pause switch stops vault outflows during an incident;request_unstake/withdrawstay open so owners can always exit. (2)initializerequires the admin signer to be the program's upgrade authority, which stops front-running the one-time init after deploy. (3)claimtakes apayersigner for the fee and the ClaimReceipt rent; the SDK'sclaimIxdefaults it to the owner, so existing callers are unaffected. (4) Additive admin ixunjail_node(SDKunjailNodeIx,swarmctl unjail). It resets jailed → active; eligibility still needsstaked >= 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 towardavailable. The fee router targets the project token viaFEE_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
availablebut nottotal_rewards_funded, which countsfund_rewardscalls only. Lifetime vault inflow isvault lamports − rent-exempt reserve + total_rewards_claimed; anything labelled "total funded" or "all-time fees" must use that, nottotal_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_configcan 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(supersedesESCHATfrom 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 v2buy_and_lockinstruction (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: settlementPAYOUT_FRACTION = 1/60per 24 h epoch (localnet demos keep 1);min_stake50,000 e/acc (50_000_000_000base units),unbonding_seconds1,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 prefundandstatusread 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-tablesimulates against existing ones). (3) New surface: SDKeaccLockPda; optionalpayoutFractioninconfig/<cluster>.json(default 1);config/mainnet.example.jsonwith the production settings;swarmctl initreads 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 withAUTH_LEGACY_V1=1or--devon localnet. Challenges are rate-limited and issued only to registered nodes. The node helper isnode0.eacc.protocol.request_stake_credential;StakeAuthorizerstill has to delegate to it (node/ owner). (2) Admin endpoints (epoch close, unjail, points edits, operator credentials) need an ed25519 request signed byConfig.adminorConfig.coordinator:X-Eacc-Signer/Timestamp/Nonce/Signatureovereacc-swarm-admin:v1:<cluster>:<origin>:<METHOD>:<path>:<ts>:<nonce>:<sha256(body)>. Accepted requests are audited inadmin-audit.jsonl. Trainers and seeds use revocable operator credentials.COORDINATOR_ADMIN_TOKENonly works in localnet--dev. (3) Closed epoch scores carryattestation(ed25519 by the coordinator key over canonical JSON with the domaineacc-swarm-scores:v1, cluster and program id). Settlement should verify it againstConfig.coordinator. (4) Slashing: no single trainer verdict slashes. Confirmed cases callSLASH_CMDwith an explicit--amount(25% fault, 100% fraud of staked + pending) and--reason 4|5, at most once per owner per epoch.SLASH_AMOUNTwas removed. Outside localnet the coordinator now needsCOORDINATOR_ORIGINSandCOORDINATOR_KEYPAIR, and the stake check defaults torpc. - ›2026-10-06 16:31 UTC
chain— v2: SECURITY.md fixes, slash delay, e/acc buy-and-lock (branchchain/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. NewClaimBitmap(171 bytes) at["claims", owner, page u16 LE], one per owner per 1,024 epochs; it replaces `ClaimReceipt` and the `["claim", epoch, owner]` PDA. NewLockConfig(238) andEaccTwap(129) at["lock_config"]/["eacc_twap"], plus["lock_wsol"]and["eacc_locked"]token accounts. (2) Instructions.initialize(InitializeParams)andset_config(ConfigUpdate)take structs and an optionaltreasuryaccount;set_admin→propose_admin/accept_admin/cancel_admin; newexecute_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 atfault_cap_bps; above that only an admin-approved fraud slash. With a slash delay the node is jailed at once andrequest_unstake/withdraw/unjail_nodefail withSlashPendinguntil it is executed or cancelled (the one exception to the 13:12 "owners can always exit").post_epochis capped atmax_payout_bpsofavailableand spaced bymin_epoch_seconds; claims openchallenge_secondsafter posting and expire afterclaim_expiry_seconds; the admin canvoid_epocha bad root. Loosening config changes waitunbonding_seconds + 1 day;unbonding_secondsis bounded to 1–30 days.initializerejects stake mints with a freeze authority or unlisted extensions (mainnet e/acc passes). (4) Production values (config/mainnet.example.json, SDKPRODUCTION_POLICY): unbonding 14 days, slash delay 48 h,max_payout_bps167 (≥ the 1/60payoutFraction), 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 deprecatedclaimReceiptPda/fetchClaimReceiptshims 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-proposalsstatuses are nowqueued running skipped failed dry_run proposed pending approved executed cancelled expired, reconciled from NodeAccount/Config: `executed` now means seen on-chain (slashed_totalgrew), not "the command exited 0" (that isproposed). 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. NewGET /metrics(Prometheus) andslashingin/api/health. The coordinator runsswarmctl slash-execute(crank) andslash-approve --emit-unsigned --json(readsunsigned.transactionBase58/messageBase58). - ›2026-10-06 17:00 UTC
chain— launch tooling: Squads/unsigned mode, keyless dry runs, lock crank (§8b). (1) swarmctl's--emit-unsignedprints an unsigned base58 transaction plus a decoded summary for a Squads vault;--authority/--fee-payerandADMIN_PUBKEY,COORDINATOR_PUBKEY,OWNER_PUBKEY,NODE_PUBKEY,PAYER_PUBKEYname signers without key files. (2) Dry runs no longer need keys and run on mainnet too (simulation only);--liveon mainnet still needsI_UNDERSTAND_MAINNET=1. The settle job no longer skips mainnet dry runs. (3)lock-crank/swarmctl lock crank:observe_pricethenbuy_and_lock, exit 0 when idle;--jsonaddsaction: buy|noneandreason. (4) Cluster config gains an optionallockobject (maxBuyReserveBps,maxDeviationBps,slippageBps,emaAlphaBps,minObserveIntervalSlots,maxPriceAgeSlots,minBuyIntervalSeconds,dailyBudgetDivisor,maxSolPerBuy,minSolPerBuy,lookupTable), used bylock-config --from-configand the crank. SDKPumpPoolStategainsvirtualQuoteReserves(offset 245). No on-chain change.