Stacks Subgraphs Beta
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.
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, jsonbHandlers
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-transfersQuerying
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/subgraphsGET /api/subgraphs/:nameGET /api/subgraphs/:name/sourceGET /api/subgraphs/:name/gapsGET /api/subgraphs/:name/openapi.jsonGET /api/subgraphs/:name/schema.jsonGET /api/subgraphs/:name/docs.mdGET /api/subgraphs/:name/:tableGET /api/subgraphs/:name/:table/countGET /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"