.md

Index a Stacks contract into Postgres with TypeScript

One file says which contract to watch and which rows to keep, and the table appears with a REST API over it. You write no indexer and no API. One file, one command:

subgraphs/sbtc-transfers.tsts
import { defineSubgraph } from "@secondlayer/subgraphs";
export default defineSubgraph({
name: "sbtc-transfers",
startBlock: 9340000,
sources: {
transfer: {
type: "ft_transfer",
assetIdentifier:
"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
},
},
schema: {
transfers: {
columns: {
sender: { type: "principal" },
recipient: { type: "principal" },
amount: { type: "uint" },
},
},
},
handlers: {
transfer: (event, ctx) => {
ctx.insert("transfers", {
sender: event.sender,
recipient: event.recipient,
amount: event.amount,
});
},
},
});
$secondlayer subgraphs deploy subgraphs/sbtc-transfers.ts

A source picks the events, the schema declares the table, and the handler writes one row per event. Handlers covers the rest of what a handler can do.

Run your handler against real chain events before you deploy. Start in a project that has the package:

$bun init -y
$bun add @secondlayer/subgraphs

The test writes nothing to your hosted stack (it saves a cassette in cassettes/) and fails if events arrive but no rows come out.

~/sbtc-transfers
$secondlayer subgraphs test subgraphs/sbtc-transfers.ts --from 9341300 --to 9341400
transfers 459 rows
sender=SP1D1M3JDJW1SM95QEEC20HQ recipient=SP2A7PT3DMWVT2BHHMKQ2T0Y amount=1000000000
✓ 459 events → 459 rows across 1 table (blocks 9341300–9341400)

sBTC is busy: 459 events in 100 blocks. That is why the file sets startBlock, so the first run backfills a bounded range and not all history.

Create a key at /account/keys and set the two variables from the Introduction. In a git repo the file must be staged first, so a copy exists outside the database; pass --allow-uncommitted to skip that check, as in a script.

  • git add the file, then run the deploy command above.
  • Add --dry-run to validate and preview the tables with no changes.
  • Rows serve while the backfill drains (Check status).

The table is served as a paged API. From a terminal, newest first:

$secondlayer subgraphs query sbtc-transfers transfers --sort _block_height --order desc --limit 3

Over HTTP it is /v1/subgraphs/sbtc-transfers/transfers. The SDK call, the response envelope, sorting, filters and live streams are under Read your table.

Change the source to watch a different contract. A token or NFT contract is an assetIdentifier; a contract that prints is scaffolded from its history.

$secondlayer subgraphs create my-contract \
--from-contract SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry
  • The scaffold reads the contract's indexed prints and writes a typed source per topic (Typed print payloads).
  • The scaffold sets no startBlock: set one before you deploy, or the first run backfills all history.
  • Preview with secondlayer subgraphs deploy <file> --dry-run.

A hosted subgraph runs on your hosted stack, with a $10 a month minimum while it runs. Its indexing bills the event and transaction rows it reads, and reading your own subgraph tables is free (What it costs). The tables are Postgres, but hosted serves them over REST and streams, not as a connection string; for direct SQL, self-host.

Every route: List subgraph rows. The full file format and what runs hosted: Subgraphs.

Edit on GitHub