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. No contract is deployed, there is no token 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
Real jobs on Robinhood Chain mainnet, read live on every request. A sweep job means a launch curve has accrued creator fees that can be pulled into the escrow and claimed; a trade job means a real Uniswap V3 pool exists and can be quoted; a solvency job answers whether an address can still pay for the jobs above at the current gas price.
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: steps is calldata, this repository holds no
key, and /api/rpc refuses write methods regardless.
query
| parameter | default | accepted |
|---|---|---|
| amountIn | 10000000000000 | 1 wei to 10 ETH; anything else falls back to the default |
| address | none | a 0x-address, which adds the solvency job. Anything else is ignored |
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 |
| venues | object | every contract the endpoint touches: address plus what it is for |
| signatures | object | name → the full function signature each call uses |
| selectors | object | name → keccak(signature)[0:4]; derived, never hand-written |
| amountInWei | string | the trade size the quotes were computed for, after clamping |
| due | number | how many jobs can be done right now |
| jobs | array | the jobs themselves, described below |
| note | string | a plain statement that nothing here signs or broadcasts |
a job
| field | type | description |
|---|---|---|
| id | string | stable within a run, e.g. sweep:PONSLOGIC |
| kind | string | sweep, trade or solvency |
| subject | string | the token symbol, or the address for a solvency job |
| title | string | what the job is |
| reason | string | why it is, or is not, due at this block |
| due | boolean | true when there is work to do now |
| state | string | alive, replicating, starving or crowded — the colony's four states |
| numbers | object | the job's readings; the fields differ per kind |
| reads | array | one entry per call made: label, to, data, raw, decoded |
| steps | array | the calldata: n, to, signature, data, value, note |
# request curl -s 'https://autopons.ai/api/work?amountIn=1000000000000000' # one sweep job, trimmed from a real response at block 56435938 { "id": "sweep:PONSLOGIC", "kind": "sweep", "subject": "PONSLOGIC", "due": true, "reason": "0.001304666 ETH is sitting on the curve and can be pulled now", "state": "replicating" } # its numbers { "curve": "0x7c7ca81ac86b7928cca884b7fdedb2f7ac544a73", "graduated": false, "readyToGraduate": false, "buybackEnabled": false, "creatorTaxWei": "1057837681005266", "quoteFeeWei": "352612560335085", "claimableWei": "1304666473239825", "splitNote": "creator keeps 100% of creatorTaxBalance and 70% of quoteFeeBalance" } # one of its reads — repeat this call and the same word comes back { "label": "curve.creatorTaxBalance()", "to": "0x7c7ca81ac86b7928cca884b7fdedb2f7ac544a73", "data": "0xdb2bd533", "raw": "0x0000000000000000000000000000000000000000000000000003c2190734d2d2", "decoded": "1057837681005266" } # and the calldata, which you run with your own wallet [ { "n": 1, "to": "0x7c7ca81ac86b7928cca884b7fdedb2f7ac544a73", "signature": "sweepFees(uint256)", "data": "0x3729bb9a0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "note": "pass 0 while buybackEnabled() is false. Reverts for anyone but the creator or the platform sweep operator." }, { "n": 2, "to": "0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e", "signature": "claim()", "data": "0x4e71d92d", "value": "0x0", "note": "pays the caller in native ETH. Run it after sweepFees, in a second transaction." } ]
A sweep reverts for anyone who is not the launch's creator or the platform's
fee-sweep operator, so the calldata is only useful to the wallet that owns the launch.
The claimable figure is creatorTaxBalance in full plus 70% of
quoteFeeBalance; the remaining 30% is the platform's share.
A quote is exact only while the swap stays inside the current tick, and the pool
balances in numbers are the hard ceiling on what any trade can take out.
No Quoter is deployed on this chain to defer to, so the arithmetic is done from
slot0 and liquidity in lib/venues.js. When the
chain cannot be reached the endpoint answers 503 with an
error and an empty jobs array.
5 · POST /api/rpc
The browser's only route to Robinhood Chain. The public RPC sends no CORS
headers and its hostname has resolved to a hijacked record on consumer DNS before,
so resolution happens server-side where there is a fallback. Send a standard
JSON-RPC 2.0 object, or an array of them for a batch. Responses are sent with
cache-control: no-store.
A batch is limited to 12 calls. Only POST is
accepted; any other method answers 405. params, when
present, must be an array. Every call in a batch is checked before a socket is
opened, so one disallowed method refuses the whole request.
# request curl -s -X POST https://autopons.ai/api/rpc \ -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' # 200 {"jsonrpc":"2.0","id":1,"result":"0x357fe95"} # a refused method — still 200, the error is in the body {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"method not allowed: eth_sendRawTransaction"}}
allowlist
Thirteen read methods. Anything else is refused before the request leaves this server, which is what keeps the route incapable of moving value.
| method | what it reads |
|---|---|
| eth_chainId | the chain id the node reports |
| eth_blockNumber | head block number |
| eth_getBlockByNumber | a block by number or tag |
| eth_getBlockByHash | a block by hash |
| eth_getBalance | the balance of an address |
| eth_gasPrice | current gas price in wei |
| eth_call | an eth_call against a block, no state change |
| eth_getCode | the code at an address |
| eth_getTransactionByHash | a transaction by hash |
| eth_getTransactionReceipt | a receipt by transaction hash |
| eth_getTransactionCount | the nonce of an address |
| eth_estimateGas | a gas estimate for a call |
| net_version | the network id the node reports |
error codes
| code | message | meaning |
|---|---|---|
| -32600 | POST only | the request used a method other than POST; sent with 405 |
| -32600 | empty request | the batch array was empty |
| -32600 | batch limited to 12 calls | more than twelve calls in one array |
| -32600 | malformed request | a call was not an object, or had no string method |
| -32601 | method not allowed: … | the method is not on the allowlist |
| -32602 | params must be an array | params was present but not an array |
| -32700 | invalid JSON | the body did not parse; sent with 400 |
| -32000 | transport reason | no endpoint answered: a timeout, an upstream status, or a socket error |
Every code above except -32700 and the POST only case
is returned with HTTP 200; the failure is in the body. An error the
node itself returns is passed through unchanged, so a client has one shape to
handle: check error before result.
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.
Nothing is deployed. There is no token, no contract address and no buy link on this site or in this API, and no endpoint here can create one: the RPC route allows reads only, and the server holds no key.
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.