Your contract, as a table.
Write one TypeScript file. Get Postgres tables, a REST API and client types, backfilled from your start block and held at tip.
Reading your tables is free
A schema change breaks the build
Handlers and client types come from the same file, so a renamed column fails in your editor, not in production.
Reorgs roll your rows back
An orphaned block takes its rows with it, and the canonical ones replace them. No cleanup job to write.
Tested on real blocks first
subgraphs test runs your handlers over a real block range. Events in and zero rows out fails the run.
Sources, schema, handlers. That's the whole thing.
Name the events you care about, declare the table, and write one handler per source. Contract sets, contracts a factory deploys later, and every token of a standard all fit the same shape.
5backfill starts here
6these events, nothing else
18the write: one row per event
Queryable while it fills
The first deploy backfills from your start block, then new blocks stream in. Go live at the tip first if your handlers only insert, and let history catch up behind.
Read it typed, or over REST
The SDK knows your columns. REST pages the same rows with a cursor and reports how far the table is behind the tip, in the same envelope the index uses.
8pass back as ?cursor
9how far behind the table is
Every read of your own subgraph, REST, SDK or stream. Point a dashboard at it and leave it running.
Event and transaction rows the subgraph reads to fill itself. The first 1M each month are free, so set a start block.
Memory plus storage, $10/mo minimum while it runs. Delete every subgraph and pause every webhook, and it stops within minutes.
Your box or ours, the same file
Hosted is one private stack per account in a sandbox. On your own box any source runs, with the same CLI and the same routes.
| Hosted | Self-host | |
|---|---|---|
| Sources | Event filters, contract_call, contract_deploy. Anything else is refused at deploy, naming the source. | Any source |
| Isolation | Handlers run in a gVisor sandbox with 512 MB, reaching only the secondlayer API and the public internet | Your machine, your rules |
| Visibility | Private to your account | Private to your network |
| Failure | A stalled handler restarts. Three deaths at the same height marks it error: fix and redeploy | Same, in your logs |
| Cost | indexing rows + stack memory + storage | free · MIT |
Check the table without asking us
A subgraph built only from contract state writes earns the state level. Anyone can rebuild its rows from block proofs and compare them with what the API serves.
Not covered yet: which transaction made a write, and writes overwritten inside one block. State-write history on hosted is coming soon
Your rows, your trigger
The table is yours, so reacting to it is too. Use what your database already does, hold a stream open, or get pushed by the chain itself.
A trigger on the table
Rows land in your own database. LISTEN/NOTIFY tells your app the moment one is written.
create function notify_transfer()
returns trigger as $$ begin
perform pg_notify('transfers',
row_to_json(new)::text);
return new;
end $$ language plpgsql;
create trigger on_transfer
after insert on transfers
for each row
execute function notify_transfer();Tail the table
Server-sent events from the tip, or replay from a height and keep going. Same filters as a read.
const stop = subgraph.transfers.subscribe(
(row) => onTransfer(row),
{ since: 9137000,
where: { sender: "SP3PE…X44J" } },
);
// or page with ?cursor=next_cursor
// on your own schedulePushed to your server
When the trigger is something on-chain, skip the table. A webhook delivers the event itself, signed, retried and logged.
secondlayer webhooks create sbtc-in \
--url https://app.example/hook \
--trigger '{"type":"sbtc_deposit"}'/webhooks →Same chain data, other surfaces
You hold the cursor
Every event as a pull you resume yourself, when you already run a sink and want no state on our side.
Streams →Before you know the shape
Query decoded events by type, contract or address while you work out which table you want.
Index →We hold the retry queue
Chain events pushed to a URL you run, with failed deliveries logged and requeueable.
Webhooks →Run it yourself
Hosted is api.secondlayer.tools. Point SECONDLAYER_API_URL at your own instance and the same subgraphs deploy writes to your Postgres. Codegen for Prisma, Drizzle or Kysely reads those tables directly.
Subgraph questions
Do I get to pick the API shape?
What happens on a reorg?
How is a hosted subgraph billed?
Can I get a webhook when a row changes?
Can I test before I deploy?
secondlayer subgraphs test runs your local handlers over a real block range. The first run records a cassette, so later runs are free and offline.