Stable

secondlayerryanwaits/secondlayer

Self-hosted Stacks data runtime

GitHubDemo ↗

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.
The Problem

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 Middle Ground

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.

Terminal
bun add -g @secondlayer/cli
secondlayer init --network mainnet
cd docker/oss && docker compose up -d
# indexer, api, and postgres on your box
Out of the 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.

Terminal
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
Custom Views

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. 1

    Scaffold it

    Point it at a deployed contract and it infers sources, schema, and handlers from the contract's observed events.

    Terminal
    secondlayer subgraphs scaffold SP1234ABCD.my-contract -o subgraphs/my-contract.ts
  2. 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.ts
    import { 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. 3

    Deploy it

    Backfills history from the given block, then stays current as new blocks arrive.

    Terminal
    secondlayer subgraphs deploy subgraphs/stx-transfers.ts --start-block <recent-block>
  4. 4

    Query it

    Live behind the /v1 read API on your instance immediately. No separate publish step.

    Terminal
    secondlayer subgraphs query stx-transfers transfers --sort _block_height --order desc
Signed Inputs

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.

Terminal
secondlayer streams pull --to ./dump
duckdb -c "SELECT event_type, count(*) FROM read_parquet('./dump/**/*.parquet') GROUP BY 1"
# Cold history: signed parquet dumps
# Live tail: checkpointed, auto reorg rewind
Where the Money Is

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.

Terminal
secondlayer bootstrap --against <manifest> # verified history, hours not weeks
secondlayer verify all --against <manifest> # check the work, free

A Separate Package — @secondlayer/stacks

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.
A Separate Package — @secondlayer/stacks

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.

verify.ts
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 }
Live since Epoch 4.0 (July 30, 2026). Reference adapter: SP2M1DE95TS0QBM4K893X6ST49FFJ53CCX9CYWNVY.spv-adapter on mainnet

Get Started

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.

Terminal
bun add -g @secondlayer/cli
secondlayer init --network mainnet
cd docker/oss && docker compose up -d
# postgres plus one container; the docs' self-host guide is the whole ceremony