secondlayerryanwaits/secondlayer
Self-hosted Stacks data runtime
- Problem
- Your contract is in nobody's API. Getting its data means writing an indexer.
- Runtime
- Postgres plus one container beside a Stacks node. Decoded chain data in a database you operate.
- Index
- Events, transfers, blocks, decoded on your instance.
/v1/index/events - Subgraphs
- Your schema, your instance. One
defineSubgraph()file → Postgres + REST. - Streams
- The raw signed event firehose underneath, for building your own.
- History
- Bootstrap from a signed archive in hours. Verifying and replaying it is free.
Your Contract Isn't in Anyone's API
Your app needs Stacks data no public API has: your contract, your schema, your uptime. A general API indexes what's general, and your contract is specific by definition. Today the real answer is writing an indexer, which means a node, decoding, backfill, and reorg handling before your first useful row.
The Runtime
Secondlayer is that work already done, running on your hardware. It sits beside a Stacks node and turns the chain into decoded rows in a database you operate: backfilled from a signed archive, kept current with the tip, correct across reorgs. The instance itself is Postgres plus one container.
bun add -g @secondlayer/clisecondlayer init --network mainnetcd docker/oss && docker compose up -d# indexer, api, and postgres on your box
Index
Events, transfers, blocks, and transactions land decoded on your instance, behind a REST API with a resumable cursor. Or build your own app index on the same rows: a checkpointed consume loop that rewinds automatically when the chain rewrites its recent past, so your tables stay correct without you writing reorg code.
curl "http://127.0.0.1:3800/v1/index/events?event_type=ft_transfer&limit=5"{ "data": [ ... ], "next_cursor": "eyJ...", "tip": { ... } }# your instance, your uptime
Subgraphs
Need a table the built-ins don't cover? Three steps from a deployed contract to Postgres tables behind the same read API. The result is a REST API you didn't write. That's the point, and the tradeoff.
- 1
Scaffold it
Point it at a deployed contract and it infers sources, schema, and handlers from the contract's observed events.
Terminalsecondlayer subgraphs scaffold SP1234ABCD.my-contract -o subgraphs/my-contract.ts - 2
Own it
The output is one TypeScript file: sources, schema, and handlers that turn matched events into rows. Edit it like any code you own.
subgraphs/stx-transfers.tsimport { defineSubgraph } from "@secondlayer/subgraphs";export default defineSubgraph({name: "stx-transfers",version: "1.0.0",startBlock: 0,sources: {transfer: { type: "stx_transfer" },},schema: {transfers: {columns: {sender: { type: "principal", indexed: true },recipient: { type: "principal", indexed: true },amount: { type: "uint" },},},},handlers: {transfer(event, ctx) {ctx.insert("transfers", {sender: event.sender,recipient: event.recipient,amount: event.amount,});},},}); - 3
Deploy it
Backfills history from the given block, then stays current as new blocks arrive.
Terminalsecondlayer subgraphs deploy subgraphs/stx-transfers.ts --start-block <recent-block> - 4
Query it
Live behind the /v1 read API on your instance immediately. No separate publish step.
Terminalsecondlayer subgraphs query stx-transfers transfers --sort _block_height --order desc
Streams
Building your own indexer or ETL instead? Streams is the raw event firehose the decoder itself rides: cursor-paginated history, a live tail that resumes exactly where it left off, and parquet dumps with signed manifests, replayable from any height. Everything handed to you is signed, so you can check the work instead of trusting it.
secondlayer streams pull --to ./dumpduckdb -c "SELECT event_type, count(*) FROM read_parquet('./dump/**/*.parquet') GROUP BY 1"# Cold history: signed parquet dumps# Live tail: checkpointed, auto reorg rewind
Verified History
The runtime is MIT, and free in the boring sense where there's no tier above it. What costs money is history: a signed public archive of the whole chain, so bootstrapping takes hours instead of a two-week sync. Pulling a large restore or a deep backfill out of it is the metered part. Verifying the archive, replaying it, and checking the work are free.
secondlayer bootstrap --against <manifest> # verified history, hours not weekssecondlayer verify all --against <manifest> # check the work, free
Typed Contract Calls
Not part of the runtime, a different layer entirely: a viem-style client for writing Stacks code. Get a typed contract once, then read and call it like normal TypeScript. The same package carries PoX-5 Bitcoin Staking support (SIP-045) and the SPV module below.
await contractCall({contractAddress: "SP2...",contractName: "my-contract",functionName: "transfer",functionArgs: [uintCV(100), // amount? recipient?principalCV(to), // is this right?],});// No autocomplete. No type safety.
Bitcoin Reads
The same stacks package does native Bitcoin SPV verification, live on mainnet since Stacks Epoch 4.0 activated in July 2026. A contract can prove a Bitcoin transaction was actually mined, no oracle, but the built-ins demand the merkle proof shaped exactly right: byte order, witness stripped, args encoded. This does that off-chain prep, trustless by default (your own node first, hosted fallback second), and the reference verifier contract is deployed on mainnet and resolved automatically when you don't pass one.
import {verifyBitcoinPayment, fallbackProofSource,bitcoinRpcSource, esploraSource,} from "@secondlayer/stacks/bitcoin";const source = fallbackProofSource([bitcoinRpcSource({ url: "http://127.0.0.1:8332", auth: { ... } }),esploraSource({ url: "https://blockstream.info/api" }),]);const result = await verifyBitcoinPayment(client, {txid: "f4184fc5...",source,vout: 0,expect: { address: "1A1zP1...", amount: 5_000_000_000n },});// → { verified, mined, output, proof }
Setup
There's one real fork, and you already know your answer: if you have an API layer, consume rows into your own schema; if you'd rather not, deploy a subgraph and inherit the generated one. Either way, the setup is the same.
bun add -g @secondlayer/clisecondlayer init --network mainnetcd docker/oss && docker compose up -d# postgres plus one container; the docs' self-host guide is the whole ceremony