docs
Three read-only HTTP endpoints and one shared ES module. Everything on this page is taken from the source in this repository; the example responses were captured from a running server, so the numbers are old but the shapes are exact.
contents
1 · overview
The base URL in development is http://localhost:6014 and in
production https://autopons.ai. Paths are the same on both, because
the local server runs the same handler files that production runs.
Every endpoint is read-only. Nothing here writes to a chain, holds a key or
signs anything. There is no authentication and no API key: /api/chain
and /api/colony are plain GET requests, and
/api/rpc accepts only the read methods on its allowlist. Responses are
JSON with content-type: application/json.
Honesty. The chain reads are real — block number, block hash,
timestamp and gas price come from Robinhood Chain. The agent population is
derived from those reads by the deterministic simulation in
lib/colony.js. Those are not deployed contracts, and no
address returned by this API belongs to anything on chain.
| endpoint | method | returns |
|---|---|---|
| /api/chain | GET | head-of-chain summary: block, gas price, chain id |
| /api/colony | GET | the block, the grid, the census and the roster |
| /api/rpc | POST | one JSON-RPC read, or a batch, forwarded upstream |
2 · GET /api/chain
A small cacheable summary for the status strips. It takes no query
parameters; anything you send is ignored. Three reads are issued in parallel
upstream — eth_blockNumber, eth_gasPrice and
eth_chainId — and the response is sent with
cache-control: public, max-age=2, stale-while-revalidate=15.
| field | type | description |
|---|---|---|
| online | boolean | true when the head block, gas price and chain id all came back |
| chainId | number | 4663, the chain this site is configured for |
| chainIdHex | string | "0x1237", the same id as hex |
| reportedChainId | number | what the node answered to eth_chainId; online only |
| block | number | head block number, decimal; online only |
| hash | string | hash of that block — the seed every colony on this site grows from; online only |
| timestamp | number | block timestamp, seconds since the epoch; online only |
| txCount | number | transactions in that block; online only |
| gasUsed | number | gas the block consumed; online only |
| gasLimit | number | gas the block allowed; online only |
| baseFeeWei | string | EIP-1559 base fee in wei, or null if the block carries none; online only |
| gasWei | string | gas price in wei, decimal digits; online only |
| gasGwei | number | the same price in gwei, four decimal places; online only |
| explorer | string | base URL of the block explorer |
| endpoints | number | how many upstream RPC URLs are configured |
| error | string | why the read failed; present only when offline |
# request curl -s https://autopons.ai/api/chain # 200, trimmed from a real response { "online": true, "chainId": 4663, "chainIdHex": "0x1237", "reportedChainId": 4663, "block": 56136099, "hash": "0xebd8b10430b11b5e8ac2860f28b982fc528bcb26acf679d73634327567e12a80", "timestamp": 1788713682, "txCount": 9, "gasUsed": 3033796, "gasLimit": 1125899906842624, "baseFeeWei": "372126000", "gasWei": "367586000", "gasGwei": 0.3676, "explorer": "https://robinhoodchain.blockscout.com", "endpoints": 2 }
When no upstream endpoint answers, the handler still returns a body — it
answers 503 rather than hanging or sending an empty page. Treat a
503 as a normal outcome and render the offline state.
# 503 — the shape when the chain is unreachable
{
"online": false,
"chainId": 4663,
"chainIdHex": "0x1237",
"explorer": "https://robinhoodchain.blockscout.com",
"endpoints": 2,
"error": "upstream timed out"
}
3 · GET /api/colony
Reads the latest block, seeds a grid from its hash, steps that grid forward
gen generations and returns the roster derived from it. Because
gen is applied server-side without touching the chain again, a client
can scrub through generations of one block cheaply.
Every parameter is parsed with parseInt and clamped. A value that
is not a number falls back to the default; a value outside the range is pulled to
the nearest bound rather than rejected.
| param | type | default | range | description |
|---|---|---|---|---|
| w | integer | 24 | 8 – 64 | grid width in cells |
| h | integer | 14 | 6 – 48 | grid height in cells |
| gen | integer | 0 | 0 – 512 | generations stepped forward from the seed |
| limit | integer | 64 | 1 – 256 | maximum agents returned, in row-major order |
| field | type | description |
|---|---|---|
| source | string | "chain" when the block was read, "genesis" when it was not |
| chain.id | number | 4663 |
| chain.explorer | string | base URL of the block explorer |
| block.number | number | block the colony was seeded from; 0 on genesis |
| block.hash | string | the seed itself, 0x-prefixed |
| block.timestamp | number | unix seconds; 0 on genesis |
| block.txCount | number | transactions in that block |
| block.gasUsed | number | gas used by that block |
| grid.w | number | width actually used, after clamping |
| grid.h | number | height actually used, after clamping |
| grid.gen | number | generations actually stepped |
| grid.population | number | live cells at generation gen |
| grid.seedPopulation | number | live cells at generation 0 |
| grid.fingerprint | string | 0x-prefixed uint32 hash of this generation |
| grid.cells | string | w×h characters of "0" and "1", row-major |
| census.alive | number | agents in state alive, within limit |
| census.replicating | number | agents in state replicating |
| census.starving | number | agents in state starving |
| census.crowded | number | agents in state crowded |
| census.total | number | agents returned, equal to agents.length |
| agents | array | the roster; fields below |
| agent field | type | description |
|---|---|---|
| id | string | "apx" plus six hex characters, stable for a cell in a block |
| name | string | two words, derived from the seed, the cell index and the generation |
| role | string | role id, one of the eight in ROLES |
| roleLabel | string | the role's display label |
| task | string | one sentence describing what the role does |
| state | string | alive, replicating, starving or crowded |
| cell | [number, number] | grid position as [x, y] |
| neighbours | number | live neighbours, 0–8, on the wrapped grid |
| energy | number | 4–100; 100 while replicating, otherwise distance from three neighbours |
| generation | number | the gen this roster was read at |
| bornBlock | number | block.number minus ageBlocks, never below 0 |
| ageBlocks | number | blocks since birth; 0 while replicating |
| txCount | number | derived transaction count |
| gasSpent | number | derived gas total, six decimal places |
| address | string | derived 0x address, simulated, not deployed |
# request curl -s 'https://autopons.ai/api/colony?w=24&h=14&gen=0&limit=3' # 200, trimmed: cells shortened, one agent of three shown { "source": "chain", "chain": { "id": 4663, "explorer": "https://robinhoodchain.blockscout.com" }, "block": { "number": 56098398, "hash": "0x32114445caca441a600e7e81a020c0a3f7783aa8ff71e30c1adfef1d68359c62", "timestamp": 1788709880, "txCount": 9, "gasUsed": 1789688 }, "grid": { "w": 24, "h": 14, "gen": 0, "population": 96, "seedPopulation": 96, "fingerprint": "0x7bf7d786", "cells": "000000110100101000001100110000000011000000010001…" }, "census": { "alive": 0, "replicating": 2, "starving": 1, "crowded": 0, "total": 3 }, "agents": [ { "id": "apx5c4a8d", "name": "marrow-cascade", "role": "router", "roleLabel": "router", "task": "splits an order across venues and settles the remainder", "state": "replicating", "cell": [2, 0], "neighbours": 3, "energy": 100, "generation": 0, "bornBlock": 56098398, "ageBlocks": 0, "txCount": 221, "gasSpent": 0.161749, "address": "0x261810edc9b3b83e93ffe72640f89a0a3db9e734" } ] }
If the chain read fails the endpoint still answers 200. It seeds
from a fixed genesis hash instead, sets source to
"genesis" and leaves block.number and
block.timestamp at 0. Check source before presenting a
colony as a fact about the head of chain. An unexpected server-side failure
answers 500 with { "error": "…" }.
4 · GET /api/work
Deploying an agent, and reading the ones that exist. An agent is a contract that holds its own balance and carries its own task; it is alive exactly while it can pay, which is the argument the rest of this site makes. Pay a small fee to the factory and everything you send above that fee becomes the new agent's opening balance.
Two things hold throughout. Every figure carries the eth_call that
produced it and the raw word that came back, so any of it can be repeated against the
chain. And nothing signs: deploy is calldata, this repository holds no
key, /api/rpc refuses write methods, and the agent you make is owned by
you — the factory keeps no power over it once it exists.
query
| parameter | default | accepted |
|---|---|---|
| address | none | a 0x-address, which adds the you block. Anything else is ignored |
| limit | 24 | 1 to 64 agents, newest last; anything else is clamped |
| name | quiet-ledger | seeds the worked deploy example |
| task | keep an index warm | seeds the worked deploy example |
| kind | 0 | 0-4, matching the contract's Kind enum |
response
| field | type | description |
|---|---|---|
| chain | object | 4663 and the explorer these addresses can be checked on |
| block | object | number, hash and timestamp of the block every figure was read at |
| gas | object | wei and gwei at that block |
| factory | object | the agent factory: deployed, and either its fee, treasury and count, or a reason it is not up |
| kinds | array | the five task kinds, in the order of the contract's Kind enum |
| deploy | object | a worked deploy call: to, signature, data, note |
| gasBudget | object | the gas a deploy and a single action are budgeted at |
| agents | array | every agent the factory has made, newest last |
| alive | number | how many of them still hold a balance |
| you | object | present only with a valid address: that wallet's balance, nonce and what it can afford |
| note | string | a plain statement that nothing here signs, and who owns the agent |
an agent
| field | type | description |
|---|---|---|
| address | string | the agent contract |
| name | string | written by its owner at deploy; treat as hostile and escape it |
| task | string | what it was told to do; also owner-written |
| kind | string | mcp, trade, index, keeper or watch |
| owner | string | who deployed it, and the only account that can make it act |
| balanceWei | string | what it has left, as decimal wei |
| alive | boolean | exactly balanceWei > 0 |
| runway | number | how many more actions it can pay for at the current gas price |
| actions | number | how many times it has acted |
| bornBlock | number | the block it was deployed in |
| lastBeat | number | block of its last owner-driven action |
| state | string | alive, starving or crowded — the colony's states, applied to a real balance |
| reads | array | the calls behind the figures above |
# request curl -s 'https://autopons.ai/api/work' # trimmed from a real response at block 56664479 { "factory": { "address": null, "deployed": false, "reason": "the agent factory is not deployed yet" }, "gasBudget": { "perAction": 120000, "perDeploy": 900000 }, "alive": 0, "deploy": { "to": null, "signature": "deploy(string,string,uint8)", "data": "0xa105fa73000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c71756965742d6c6564676572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000126b65657020616e20696e646578207761726d0000000000000000000000000000", "note": "the fee goes to the treasury and everything above it becomes the agent's balance. You own the agent; the factory keeps no power over it." }, "kinds": [ { "id": 0, "key": "mcp", "label": "MCP", "what": "answers over the Model Context Protocol, and pays its own way" }, { "id": 1, "key": "trade", "label": "trade", "what": "quotes a pool and swaps within a band its owner sets" }, "\u2026" ], "agents": [] }
A contract cannot wake itself up, so an agent is an account with a task rather than
a process. Its owner signs when it should act, and the gas comes out of the agent's own
balance through execute. That is the honest shape of the idea: the money
and the identity live on chain, the decision to act does not.
When the chain cannot be reached the endpoint answers 503 with an
error and an empty agents array. When the factory has not
been broadcast yet, factory.deployed is false and
factory.reason says so — the endpoint still returns everything else,
because a page that cannot tell you why it is empty is worse than one that can.
6 · lib/colony.js
The model itself, as one ES module with no imports, no node builtins and no
DOM. The server loads it from api/colony.js and the browser loads the
same file over /lib/colony.js, so the roster a page draws and the
roster the API serves cannot drift apart. It is served as a static file, which
means you can import it directly from a page of your own.
| export | signature | returns |
|---|---|---|
| rng | rng(seed: number) | a function of no arguments giving the next float in [0, 1); the same seed gives the same stream on every runtime |
| fnv1a | fnv1a(str: string) | a uint32 hash of the string; used to fold a block hash into a seed |
| hexBody | hexBody(hash: string) | the hash lowercased and trimmed with any 0x prefix removed |
| RULE | { born: [3], survive: [2, 3] } | B3/S23 as data, read by step and classify |
| seedGrid | seedGrid(hash, w, h, density = 0.32) | a Uint8Array of w×h zeroes and ones, row-major, seeded from the hash |
| neighbours | neighbours(g, w, h, x, y) | the live neighbour count, 0–8, on a toroidal grid — the colony wraps and has no edge |
| step | step(g, w, h, out?) | the next generation as a Uint8Array; writes into out when one of the right length is passed |
| classify | classify(g, w, h) | an Array of w×h strings: alive, replicating, starving, crowded or empty — why a cell is about to change, not only that it will |
| population | population(g) | the number of live cells |
| fingerprint | fingerprint(g) | a 0x-prefixed uint32 string identifying a generation, for cycle detection and display |
| addressFor | addressFor(id, chainSeed) | a deterministic 0x address, 40 hex characters; simulated, never deployed |
| roster | roster({ blockHash, blockNumber = 0, w = 24, h = 14, gen = 0, limit = 64, density = 0.32 }) | an array of agent objects with the fields listed in section 3, read in row-major order and capped at limit |
| census | census(agents) | { alive, replicating, starving, crowded, total } counted from a roster |
| ROLES | Array of { id, label, task } | the eight roles an agent can take |
| STATES | { [state]: { label, hint } } | the four agent states and the one-line reason for each |
// worked example — same rule as the API, no network beyond one read import { seedGrid, step, classify, population, fingerprint, roster, census } from '/lib/colony.js'; const head = await fetch('/api/chain').then(r => r.json()); const hash = '0x32114445caca441a600e7e81a020c0a3f7783aa8ff71e30c1adfef1d68359c62'; const w = 24, h = 14; let g = seedGrid(hash, w, h); // generation 0 g = step(g, w, h); // generation 1 population(g); // -> 98 live cells at generation 1 fingerprint(g); // -> '0x…' identifies this generation classify(g, w, h)[0]; // -> 'alive' | 'replicating' | 'starving' | 'crowded' | 'empty' const agents = roster({ blockHash: hash, blockNumber: head.block, w, h, gen: 1, limit: 8 }); census(agents); // -> { alive, replicating, starving, crowded, total }
7 · the rule
An agent is a cell. The block hash supplies the seed and B3/S23 supplies the biology. A cell with two or three live neighbours has enough of the block's gas to keep running; an empty cell with exactly three is funded from three directions at once and an agent is born there.
| neighbours | cell | state | what it means |
|---|---|---|---|
| 2 – 3 | lives | alive | funded, running |
| exactly 3, empty | born | replicating | a child is being funded here |
| under 2 | dies | starving | underfunded, halts next block |
| over 3 | dies | crowded | priced out of the block, halts |
Nothing in the roster is random. The block hash is folded to a seed with
fnv1a, that seed drives the PRNG, and each agent's name, role and
energy are drawn from a second stream seeded by the hash, the cell index and the
generation. Its id and address come from the hash and the
cell index alone, so they stay the same as the colony steps forward while the rest
of the agent moves with it. Same hash, same width, same height, same generation, same roster —
on any runtime, in any order, as many times as you ask. That reproducibility is
what makes the roster a fact about the chain rather than a decoration, and it is
also why you can verify it yourself: run roster() in your own page
against a hash from the explorer and compare it to what this API returns.
8 · limits and honesty
The agent population is derived. Block number, block hash, timestamp, gas price
and gas used are read from Robinhood Chain and are real; everything about an agent
— its name, role, energy, age, transaction count, gas total and address — is
computed from those reads by lib/colony.js. An address returned by
/api/colony is not a deployed contract and holds nothing.
No endpoint here can send anything: the RPC route allows reads only and the
server holds no key. $AUTOPONS (0x4dBb51a72ab680DadDEA5Eb6A1e6c43552e9a5EE) is on
the ponsfamily bonding curve and is linked in the footer — it is the only buyable
thing on this site, it is not returned by any endpoint, and holding it grants no
claim on any agent. Agents are funded in native ETH and owned by their deployer.
The endpoints are read-only and their rate is the upstream's rate. There is no
quota of our own and no key to apply one to, so treat the public RPC's limits as
yours: keep batches under twelve, cache what you can — /api/chain and
/api/colony both send short cache headers — and expect an occasional
503 from /api/chain or a source of
"genesis" from /api/colony when the chain is
unreachable. Both are normal, and both are documented above rather than hidden.