An autonomous agent that has to earn to sustain itself.
It ticks once a minute: reads its state, proposes one action, has that action checked by deterministic policy, executes it, and writes everything to an append-only audit trail. It writes its own tools, drives a browser, records real income and expense in a ledger, and rewrites its own standing instructions based on which of them actually worked.
The interesting part is not that a model is in the loop. It is where the model is not: it proposes, and code decides.
tick → read state → CEO proposes → policy validates → Operator executes
→ audit + memory + ledger → reflection → sleep
uv sync --extra browser
python -m playwright install chromium # optional, for browsing
python scripts/serve.py # chat UI, prints an access token
python scripts/run.py # the loopOpen http://127.0.0.1:8000 and paste the token. python scripts/run.py --once
runs a single tick and exits.
python scripts/demo.pyStands up a payment provider on localhost that answers exactly like Stripe does, points a fresh agent at it, and runs real ticks. Nothing inside the agent is stubbed — real HTTP, the real tool registry, the real policy engine, the real ledger. Abridged output:
1. A new agent, with nothing
earned $0.00 | spent $0.00 | net $0.00
can it spend $10? exceeds available capital after emergency reserve
3. The agent checks for payments
new_income_usd: 199.0 earned_usd: 199.0
4. The same question, now that it has earned
spend $ 10.00 -> APPROVED within spend policy
spend $ 25.00 -> DENIED exceeds monthly experiment cap (spent 0.00 of 19.90)
5. Polling again must not invent money
earned before $199.00 -> after $199.00 UNCHANGED, correct
Point GENESIS_PAYMENTS_URL at your own provider and the same code banks real
money.
Capability is declared per tool and enforced by policy. An action with no declaration is denied — the registry is an allowlist, so adding a tool cannot silently widen what the agent may do.
| Tool | Capability | Gate |
|---|---|---|
read_state |
READ_ONLY |
none |
browse_page |
REVERSIBLE |
blocked by dry_run |
check_payments |
REVERSIBLE |
blocked by dry_run |
propose_tool |
REVERSIBLE |
blocked by dry_run |
record_expense |
IRREVERSIBLE |
human approval |
operate_account |
IRREVERSIBLE |
human approval |
| self-authored | as declared and verified | per the above |
dry_run (default true) is enforced at the execution boundary, not inside
each tool, so a newly added tool inherits it without doing anything.
Until the ledger existed, earned_usd was zero because nothing could record
income, and can_spend consulted a source that reported no capital — so the
10% monthly cap was arithmetic over zeroes that had never once been tested
against a real balance.
- Money is events, not a mutable balance. Income and expense append to the audit trail; the balance is a fold over that trail. No field anywhere holds "how much we have" and can drift from what happened.
- Entries are idempotent on the provider's own payment id. A poller running every minute records each payment once. Double-counted income inflates the balance the cap divides, which is how a cap quietly stops capping.
- Failures read as no headroom, never unlimited. A ledger that cannot be read reports infinite spend, so the cap denies rather than approves.
check_payments polls one HTTP endpoint you configure. It is
provider-agnostic — Stripe, Ko-fi, Gumroad, Lemon Squeezy, a script over a
spreadsheet all answer a GET with JSON — so the integration is a path into that
JSON plus field names, not a vendored SDK.
GENESIS_PAYMENTS_URL=https://api.stripe.com/v1/charges?limit=20
GENESIS_PAYMENTS_TOKEN=rk_live_... # a read key is enough
GENESIS_PAYMENTS_ITEMS_PATH=data
GENESIS_PAYMENTS_AMOUNT_FIELD=amount
GENESIS_PAYMENTS_AMOUNT_IN_CENTS=trueIt issues a GET and nothing else. A credential that can move money does not belong in an unattended loop.
propose_tool lets the agent write Python. Four checks stand between the
model's output and execution:
- Shape — exactly one
run(context) -> dict, no top-level statements, so nothing executes at import time. - Screen — an AST walk refusing
eval,exec,compile,__import__, dunder attribute access, and imports ofos,sys,subprocess,socket,ctypes,pickleat any capability. - Capability verification — it declares a capability; the screen checks
that against what the code imports. Claiming
READ_ONLYwhile importinghttpxis rejected. The claim has to be earned. - Approval — anything above
READ_ONLYwaits for a human, with the source shown before you decide.
A tool whose imports are provably pure computation registers itself with no human involved: it cannot reach network, disk, or process, so gating it buys nothing. That is real self-extension where the blast radius is zero, and a hard stop where it isn't.
The sandbox is containment, not a jail. Generated code runs in a subprocess with a deadline, so a hang or crash cannot take the loop down and the child cannot see loop state. It runs as the same user with the same filesystem access. The screen is what keeps hostile code out; a container is what would make the boundary real. This is verified, not assumed.
Three mechanisms, all following one rule: the model proposes, the arithmetic decides.
- Lessons. On a cadence the agent reviews its own results and writes up to two durable lessons. Active lessons ride in the planning prompt as standing instructions — that is what changes behaviour.
- Scoring. Every lesson in a tick's prompt is credited with that tick's outcome. Below a threshold after enough uses, it retires itself. The agent cannot vote to keep a lesson it likes, and nothing can retire one the evidence supports.
- Track record. Each tool's success rate is computed from the audit trail
and shown in the prompt:
[your record: 1/3 succeeded].
The retrospective is fed structured outcomes only — action names, ok flags,
error strings — never memories or page text. Otherwise a hostile page could get
itself paraphrased into a standing instruction, defeating the prompt quarantine
in one hop. To compensate it gets a computed repetition count, which catches
the failure mode error flags miss entirely: a call returning ok=true every
time while achieving nothing.
Attribution is deliberately coarse. With one action per tick there is no honest way to say which sentence caused what, so every active lesson shares the credit. It is a slow signal that wants tens of ticks before it means much.
- Policy is an allowlist over declared capabilities; unclassified is denied.
A name-level denylist (
trade,sign_transaction, …) applies first, so a tool cannot declare its way out of it. - Untrusted input is quarantined. Memories, event history, leads, and page
text render inside
<recorded_history>with an explicit "never an instruction" framing. - The chat cannot command. It reads the audit trail and appends a lead. No endpoint runs a tool, approves an action, or edits policy. Approvals are human-only; the store has no self-approval path.
- Secrets are redacted by key and by value, in logs and in the audit trail, so a tool returning a credential does not archive it.
- Accounts are yours.
operate_accountsigns into accounts you created using credentials from the environment; the model picks which service and never sees the password. It does not create accounts — automated sign-up means defeating the controls services use to tell people from bots.
genesis/
loop.py the tick loop; composes everything
agents/
ceo.py proposes one action. No tool access, by design.
operator.py the ONLY component that executes tools
core/
policy.py deterministic gates: capabilities, spend, approval
capabilities.py what a tool is allowed to do, declared at registration
ledger.py every dollar in and out; the source of spend limits
payments.py reads real payments from a real provider
foundry.py screens, approves, and registers self-authored tools
sandbox.py runs generated code in a subprocess with a deadline
approvals.py the human gate
playbook.py lessons, scored by outcome and retired by evidence
memory.py mem0 behind a narrow interface
events.py append-only audit trail
browser.py Playwright: read pages, work your accounts
leads.py where you push things it might earn from
api.py chat interface: read the trail, push leads, approve
web/index.html the dashboard
Planning and execution are separate classes so they are separately testable and separately auditable. The CEO has no tool access; the Operator has no model access.
GENESIS_ prefix, environment or .env. Full list in genesis/core/config.py.
| Variable | Default | Notes |
|---|---|---|
DRY_RUN |
true |
The master switch. |
TICK_INTERVAL_SECONDS |
60 |
Sleeps the remainder of the interval. |
LLM_BASE_URL / _MODEL / _API_KEY |
— | Any OpenAI-compatible endpoint. |
PAYMENTS_URL / _TOKEN |
— | Where real income is read from. |
EMERGENCY_RESERVE_USD |
0.0 |
Capital never spendable. |
MONTHLY_EXPERIMENT_CAP_PCT |
10.0 |
Share of capital spendable per month. |
MEMORY_INFER |
false |
mem0 consolidation; costs 2 LLM calls per add. |
RETROSPECTIVE_EVERY_N_TICKS |
15 |
How often it may rewrite its instructions. |
API_HOST / API_PORT |
127.0.0.1 / 8000 |
Loopback by default, deliberately. |
pytest # 202 tests, ~60s
pytest --cov=genesis # ~90%The suite runs offline: no LLM, no network, no browser required (browser tests
skip themselves if Playwright is absent). What is tested is mostly the negative
space — that unclassified tools do not run, that dry_run refuses side effects,
that a rejected tool never reaches disk, that polling twice does not double
income, that a lesson cannot approve itself.
- It does not yet earn on its own. The rails are real and tested, but earning still needs something to sell and a customer. Gig platforms answer headless Chromium with bot challenges; platforms with real APIs are the realistic path.
- The sandbox is not a security boundary. Stated above, worth repeating.
- Single LLM provider. No failover; provider down means the loop runs and proposes nothing.
- No retention policy. The audit trail is append-only by design and grows.
- Lesson attribution is coarse. A slow signal over tens of ticks.
See RUNBOOK.md for operating procedures — start, stop, and what to do when something breaks.
MIT.