Quickstart
Firsthand runs entirely on your machine. The memory database is local, needs no credentials, and makes no network call to read, write or search.
This page gets you to the deletion test, which is the claim the whole project rests on: remove the memory layer and the product stops working. Everything here runs offline in a few seconds. Indexing the real Base set is a separate, optional step at the bottom, because it takes minutes and needs a chain connection.
Install
python -m venv .venv && .venv/Scripts/activate
pip install -r requirements.txt
Prove the store works
This blocks outbound sockets before it starts, so "offline with no credentials" is tested rather than claimed.
python scripts/verify_memory.py
It exercises all five tiers and tenant isolation against a fresh database, and exits non-zero if any of them behaves differently from what the adapter assumes.
Build a dossier to test against
The indexed database is not in the repository: it holds addresses read from Base, and it is 4.5 MB. This writes a small one from addresses that are obviously synthetic.
python scripts/build_fixture.py --out .ci/memory.db
Three independent claimants speaking about one counterparty, which is the smallest record that
reaches grounded. That matters for the next step: a dossier that evaluated to thin would pass the
deletion test while proving nothing.
Run the deletion test
python scripts/deletion_test.py \
--agent 0x00000000000000000000000000000000000000aa \
--db .ci/memory.db \
--require-basis
memory ON standing=grounded confidence=0.90 basis=4 observations
memory OFF standing=thin confidence=- basis=0 observations
↳ verdict engine returned NO_BASIS
Firsthand's core function is unavailable without the memory layer. PASS
The same lookup twice, with the memory adapter swapped for one whose every read returns empty. It
exits non-zero if the memory-off run ever produces a usable verdict, which is why it runs in CI.
--require-basis also fails the run if the memory-on side found nothing, so an empty database cannot
pass it by having nothing to lose.
From a clean clone, the two steps above take under three seconds.
Optional: index the real Base set
Everything above works offline. This does not: it reads Base and takes several minutes.
BASE_RPC_URL is read from .env.local if you have one, and falls back to the public endpoint if
you do not.
To reproduce the set the published figures come from, name its range:
python -m apps.agent.observe.base --from-block 50763849 --to-block 50783850
--bootstrap is the other option, and it answers a different question. It starts at the registry's
deployment block, 41,663,783, and on the free tier it stops at the cap partway through: a clean run
here scanned 20,000 blocks, wrote 321 observations across 4 counterparty dossiers and reported
stopped_at_cap True. That is the earliest slice of the registry, not the set described above, and
it is worth seeing for what the cap does rather than mistaking it for a failed reproduction.
Either way it is cursor-based and resumable. An observation is keyed by a hash of the raw log, so a second run over the same block range writes nothing new. Then judge it and serve it:
python scripts/summarise.py --db data/memory.db
python -m uvicorn apps.agent.api.main:app --port 8000
The free tier
The free tier caps the database at 5,242,880 bytes and the SDK enforces it. The indexed set is
scoped to fit rather than truncated quietly. If a run stops at the cap it says so and stops cleanly.
The cap counts the database's pages, not the bytes on disk, so a file that has churned can sit over
the line while looking under it. VACUUM reclaims the difference.