Stacks Subgraphs are the L3 surface for app-specific materialized tables.

Table list routes return { data, meta }; count routes return { count }.

Pick the events your app needs and shape them into a queryable API.

Install with bun add @secondlayer/subgraphs.

Set up subgraphs with your agent.
Ensure setup once, skipping any step already done:
- Skill: `bunx skills add ryanwaits/secondlayer --skill secondlayer -y`
- CLI: `bun add -g @secondlayer/cli`
- Auth: `sl login` then `sl whoami`
- Instance: `sl instance info`; if missing, start a trial from Billing or run `sl instance create --plan launch`

Subgraphs are declarative SQL tables that auto-index Stacks blockchain activity into queryable Postgres tables. Define named sources, a typed schema, and handlers in TypeScript, then deploy and query.

/secondlayer Help me create a subgraph from a Stacks contract. Ask me for the contract id and the events or function calls I care about. Scaffold with `sl subgraphs scaffold` so the module package and dependencies are prepared, explain the generated named sources and tables, let me review or customize the handlers, deploy with `sl subgraphs deploy`, query recent rows, then ask whether I want a subscription webhook.

Getting started

A subgraph has three parts: sources (what events to listen for), a schema (what tables to create), and handlers (how to process each event into rows).

import { defineSubgraph } from "@secondlayer/subgraphs"

export default defineSubgraph({
  name: "stx-transfers",
  sources: {
    transfer: { type: "stx_transfer" },
  },
  schema: {
    transfers: {
      columns: {
        sender: { type: "principal", indexed: true },
        recipient: { type: "principal", indexed: true },
        amount: { type: "uint" },
        memo: { type: "text", nullable: true },
      },
    },
  },
  handlers: {
    transfer: (event, ctx) => {
      ctx.insert("transfers", {
        sender: event.sender,
        recipient: event.recipient,
        amount: event.amount,
        memo: event.memo,
      })
    },
  },
})

Schema

Each subgraph gets its own PostgreSQL schema (subgraph_<name>). Tables are defined declaratively with typed columns. System columns are added automatically: _id, _block_height, _tx_id, _created_at. Enable full-text search on any text column with search: true.

schema: {
  balances: {
    columns: {
      address: { type: "principal", indexed: true },
      token: { type: "text", indexed: true, search: true },
      amount: { type: "uint" },
    },
    uniqueKeys: [["address", "token"]], // enables upsert
  },
}

// Column types: text, uint, int, principal, boolean, timestamp, jsonb

Handlers

Handlers process events into table rows. The context object provides write, read, and aggregate operations — all batched and flushed atomically per block.

handlers: {
  transfer: async (event, ctx) => {
    // Write
    ctx.insert("transfers", { sender: event.sender, amount: event.amount })
    ctx.upsert("balances", { address: event.sender }, { amount: event.amount }) // key, values — requires uniqueKeys
    ctx.update("balances", { address: "SP..." }, { amount: 0n })
    ctx.delete("balances", { address: "SP..." })

    // Read
    const row = await ctx.findOne("balances", { address: event.sender })
    const rows = await ctx.findMany("balances", { token: "usda" })

    // Block / tx metadata
    ctx.block.height     // current block height
    ctx.tx.txId          // transaction id
    ctx.tx.sender        // transaction sender
  },
}

Monitoring signals

For monitoring and response, keep hard facts in the subgraph: extract the protocol-specific sender, compute fields such as approved_sender, and store transaction evidence. A subscription can then wake a Slack, Inngest, Trigger.dev, or AI workflow only when that deterministic rule is violated.

const APPROVED_SENDERS = new Set([
  "SP123.dao-core",
  "SP456.approved-proposal-factory",
])

schema: {
  proposals: {
    columns: {
      proposal_id: { type: "text", indexed: true },
      sender: { type: "principal", indexed: true },
      approved_sender: { type: "boolean", indexed: true },
      title: { type: "text", search: true },
      tx_id: { type: "text", indexed: true },
      block_height: { type: "uint", indexed: true },
      raw: { type: "jsonb" },
    },
    uniqueKeys: [["proposal_id"]],
  },
},
handlers: {
  proposalCreated(event, ctx) {
    const value = event.value as Record<string, unknown>
    const sender = String(
      value.sender ?? value.proposer ?? ctx.tx.sender,
    )

    ctx.upsert(
      "proposals",
      { proposal_id: String(value["proposal-id"] ?? ctx.tx.txId) },
      {
        proposal_id: String(value["proposal-id"] ?? ctx.tx.txId),
        sender,
        approved_sender: APPROVED_SENDERS.has(sender),
        title: String(value.title ?? ""),
        tx_id: ctx.tx.txId,
        block_height: ctx.block.height,
        raw: value,
      },
    )
  },
}

Deploy

# Scaffold from a deployed contract's ABI; writes package.json + runs bun install
sl subgraphs scaffold SP1234ABCD.token-transfers --output subgraphs/token-transfers.ts

# Deploy to Second Layer
sl subgraphs deploy subgraphs/token-transfers.ts

# Dev mode — watches for changes, auto-redeploys
sl subgraphs dev subgraphs/token-transfers.ts

# Force reindex
sl subgraphs reindex token-transfers

Querying

Query via the SDK, CLI, or HTTP API. Supports filtering, comparison operators, ordering, and pagination. For typed queries with autocompletion, see typed subgraphs in the SDK docs.

Each deployed Stacks Subgraph exposes list, detail, source, gaps, generated OpenAPI, compact schema, Markdown reference, table list, table count, and row detail routes.

  • GET /api/subgraphs
  • GET /api/subgraphs/:name
  • GET /api/subgraphs/:name/source
  • GET /api/subgraphs/:name/gaps
  • GET /api/subgraphs/:name/openapi.json
  • GET /api/subgraphs/:name/schema.json
  • GET /api/subgraphs/:name/docs.md
  • GET /api/subgraphs/:name/:table
  • GET /api/subgraphs/:name/:table/count
  • GET /api/subgraphs/:name/:table/:id
const rows = await client.subgraphs.queryTable(
  "token-transfers",
  "transfers",
  {
    sort: "_block_height",
    order: "desc",
    limit: 25,
    filters: { sender: "SP1234...", "amount.gte": "1000000" },
  }
)

// CLI
sl subgraphs query token-transfers transfers --sort _block_height --order desc
sl subgraphs query token-transfers transfers --filter sender=SP1234... --count

// HTTP API routes
GET /api/subgraphs
GET /api/subgraphs/:name
GET /api/subgraphs/:name/source
GET /api/subgraphs/:name/gaps
GET /api/subgraphs/:name/openapi.json
GET /api/subgraphs/:name/schema.json
GET /api/subgraphs/:name/docs.md
GET /api/subgraphs/:name/:table
GET /api/subgraphs/:name/:table/count
GET /api/subgraphs/:name/:table/:id

curl -H "Authorization: Bearer $SL_SERVICE_KEY" \
  "https://<your-slug>.secondlayer.tools/api/subgraphs/token-transfers/transfers?_sort=_block_height&_order=desc&_limit=25&sender=SP1234...&amount.gte=1000000"

curl -H "Authorization: Bearer $SL_SERVICE_KEY" \
  "https://<your-slug>.secondlayer.tools/api/subgraphs/token-transfers/transfers/count?sender=SP1234..."

curl -H "Authorization: Bearer $SL_SERVICE_KEY" \
  "https://<your-slug>.secondlayer.tools/api/subgraphs/token-transfers/openapi.json"