Architecture
Terrarium is a folder of plain JavaScript. There is no contract to deploy, no database, no server that holds anything. One process (the runner) owns one wallet; a static site reads what the process writes.
+---------------------- the runner (one per organism) ----------------------+
Pons escrow -> | claim -> split -> tier -> hooks -> mind (optional) -> execute -> journal | -> data/<id>/*.json
Pons curve <- | | |
v4 pool <- | buy and burn <---------------+ |
+--------------------------------------------------------------------------+
|
browser <- site/ (static) <- data/<id>/status.json, feed.json, series.json <-----------+
(localhost: the folder; elsewhere: the repository's `data` branch, pushed by the runner)
|
+-> Verify: JSON-RPC straight to the chain (no Terrarium server in between)
The engine (engine/)
| File | Role |
|---|---|
config.mjs | chain facts, the tier table, the limits, the defaults |
battery.mjs | pure functions: level, tier, charge, drain, claim split. Also run by the browser |
hooks.mjs | pure rules: the drip and the dip defender, the caps, the settings validation. Also run by the browser |
mind.mjs | the optional model review; can only trim an order |
ledger.mjs | the book and the journal, written atomically |
organism.mjs | the beat: claim, reconcile, tier, hooks, mind, execute. Paper and live executors |
pons.mjs | claim from the escrow; buy and burn on the curve or in the pool |
v4.mjs | the Uniswap v4 pool of a graduated coin: key, price, liquidity, the swap |
market.mjs | where the coin trades and at what price, and ETH in dollars |
publish.mjs | status.json, feed.json, series.json, organisms.json |
launch.mjs, rescue.mjs, runner.mjs | launching a coin, the exit, the process |
battery.mjs, hooks.mjs and config.mjs have no network and no key. The site serves only these three to the browser, so the lab runs exactly the code the organism runs.
The beat, as code
tick(market, now):
claim() -> CLAIM, battery, treasury
reconcile(treasury, wallet) -> RISK if trimmed
tier = tierFor(level(battery)) -> TIER / WAKE / SLEEP
if dormant or not time yet: return
if market.canBurn is false: log, return
orders = hooks.evaluate(...) -> HOOK lines for hooks that did not fire
review = think(orders) -> THINK, battery drain (scale <= 1)
for each order: burn() -> BURN with tx, saved at once
SAY
What each side trusts
| Side | Trusts | Does not trust |
|---|---|---|
| The runner | the chain's answers, its own caps | the model's amounts, a thrown error as proof that nothing was sent |
| The model | nothing: it sees only the orders and a small context | it never touches a key or an address |
| The site | data/<id>/*.json for display | it never needs them for the Verify page, which reads the chain |
| Verify | the chain | the organism's own numbers, which it compares with the chain |
Failure model
- A crash between a transaction and a save: the next beat reconciles the treasury with the wallet balance.
- A transaction mined but reported as failed: the balance before and after decides; if unreadable, the order counts against the daily ceiling.
- An RPC outage: claims and burns fail, nothing moves on the books, the next beat retries.
- A model outage or a bad answer: the hooks run as written.
The chaos soak test (tests/soak.mjs) exercises all four.