.md

@secondlayer/subgraphs

Everything the subgraphs package exports: defineSubgraph, the handler ctx, contract reads, schema codegen, replay and the test harness. The Subgraphs guide shows how they fit together.

@secondlayer/subgraphs@7.7.145 functionsfrom source with openpkgSubgraphs guide
Define a subgraph
defineSchema(schema)SDeclares a schema you can hoist out of defineSubgraph() and reuse.
defineSubgraph(def)TypedSubgraphDefinition<Sources, S>Declares a subgraph and keeps its source and schema literal types.
filterFieldsByType(){ type: string; fields: string[] }[]Lists the fields each source filter type accepts.
validateSubgraphDefinition(def)SubgraphDefinitionValidates a subgraph definition and returns the parsed result.
camelizeKeys(obj)unknownCamelizes object keys recursively, the way print payloads are.
inferPrintTopics(samples)InferredTopicSchema[]Infers per-topic field schemas from sampled print events.
Handler context
ctx.client.contract(contractId, abi, options){ read: Record<string, (args?: Reco…Reads a contract's read-only functions at the block being processed.
ctx.insert(table, row)voidQueues a new row for a table.
ctx.update(table, where, set)voidQueues an update to the rows matching where.
ctx.upsert(table, key, row)voidInserts a row, or updates the one with the same key.
ctx.delete(table, where)voidQueues deletion of the rows matching where.
ctx.increment(table, key, deltas)voidAdds signed deltas to a counter row, creating it if missing.
ctx.findOne(table, where)SystemRow & { [K in keyof T["column…Returns the first row matching where, or null.
ctx.findMany(table, where)SystemRow & { [K in keyof T["column…Returns every row matching where, in insertion order.
Contract reads
readContractAt(ctx, contractId, abi, options){ read: ChainReadMethods<C> }Reads a contract's read-only functions from a handler, with typed results.
Schema and SQL
generateSubgraphSQL(def, schemaNameOverride)GeneratedSQLGenerates PostgreSQL DDL statements for a subgraph definition.
generatePrismaSchema(def, opts)stringGenerates a Prisma schema for a subgraph's tables.
generateDrizzleSchema(def, opts)stringGenerates Drizzle table definitions for a subgraph's tables.
generateKyselySchema(def, opts)stringGenerates Kysely table interfaces for a subgraph's tables.
generateIndexSchema(target, opts)stringGenerates a typed schema for Index tables.
pgSchemaName(subgraphName)stringConvert a subgraph name to its PostgreSQL schema name.
deploySchema(db, def, handlerPath, opts){ action: "created" | "unchanged" |…Deploys a subgraph schema to the database.
diffSchema(existing, incoming)TableDiffCompare two multi-table subgraph schemas and return differences.
hasBreakingChanges(diff){ breaking: boolean; reasons: strin…Tells whether a schema diff is breaking, with the reasons.
renderDeployPlan(def, schemaName)DeployPlanRender the DDL a deploy would run, without executing.
Block sources
canSparseScan(subgraph)booleanTells whether a subgraph can skip blocks without its events.
isStreamsIndexEligible(subgraph)booleanTells whether a subgraph's sources can be read from the Streams index.
sparseProbeTargets(subgraph)SparseProbeTarget[]Lists the event types and contract scopes a subgraph's filters match.
Verification
computePin(input)stringContent hash over everything that shapes a subgraph's rows.
deriveVerification(def, findings)SubgraphVerificationDerive how far a subgraph's rows can be checked.
formatFinding(f)stringFormats a handler finding as file:line:column name: reason.
isDeterminismViolation(v)booleanTells whether a deploy must be refused for breaking determinism.
loadDeterministicDefinition(handlerCode)SubgraphDefinitionLoads a bundled handler into a fresh deterministic realm.
Replay
checkPin(served, def)PinCheckChecks a served pin against the served bundle.
replayBlocks(deps, opts)ReplayResultThe replay loop over an injected verifier.
replayContracts(def)string[] | { error: string }Lists the contracts a state subgraph's writes come from.
replaySubgraph(opts)ReplayResultVerifies every block in a range and recomputes the subgraph's tables.
compareRows(schema, replayed, served)RowComparisonCompares replayed tables with the served ones over a block range.
Testing
buildEvent(source, payload, options)EventForFilter<F>Builds a handler-shaped event for a source, typed by that source's filter.
buildEventPayload(filter, tx, event)Record<string, unknown>Build a typed event payload based on the source filter type.
createTestContext(schema, options)TestSubgraphContext<S>Builds an in-memory context for a subgraph's schema.
probeHandlers(def, samples)ProbeHandlersResultRun handlers against pre-built sample payloads in memory (no Index, no Postgres).
runSubgraphTest(input)SubgraphTestResultApply local handlers to Index rows in memory.
toContractCallPayload(row)Record<string, unknown>Map an Index contract-call row onto the ContractCallEvent handler shape.
toHandlerPayload(_filter, row)Record<string, unknown>Map an Index event row onto the payload shape a handler expects.

Define a subgraph

Declare sources, tables and handlers, and check them before a deploy.

import from @secondlayer/subgraphs

Declares a schema you can hoist out of defineSubgraph() and reuse.

It keeps the schema's literal types, so table names and row columns stay checked in helpers typed TypedSubgraphContext<typeof schema>. as const alone does not work: it produces readonly arrays that a mutable uniqueKeys rejects, and a bare literal widens type to string.

defineSchema<const S extends SubgraphSchema>(schema: S): S

Parameters

hover a row to find it in the code

Required

schemarequiredS

Declares a subgraph and keeps its source and schema literal types.

Handlers are typed per source, and the schema drives query-client inference. It returns its argument unchanged.

defineSubgraph<const Sources extends Record<string, SubgraphFilter>, const S extends SubgraphSchema>(
def: TypedSubgraphDefinition<Sources, S>,
): TypedSubgraphDefinition<Sources, S>

Parameters

hover a row to find it in the code

Required

namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
sourcesrequiredSources
schemarequiredS
handlersrequiredTypedHandlers<Sources, S>

Options

descriptionstringHuman description
startBlocknumberBlock height to start indexing from (default: 1)
backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
Returns TypedSubgraphDefinition<Sources, S>
name
Unique subgraph name (lowercase, alphanumeric + hyphens)
description
Human description
startBlock
Block height to start indexing from (default: 1)
backfillMode
'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
sources
Sources
schema
S
handlers
TypedHandlers<Sources, S>

Lists the fields each source filter type accepts.

The list is derived from SubgraphFilterSchema. Agent-facing surfaces such as the MCP filters reference used to keep a parallel copy by hand; deriving it means adding a field to a filter can never leave the documentation behind.

filterFieldsByType(): { type: string; fields: string[] }[]

Validates a subgraph definition and returns the parsed result.

It throws when the definition is invalid.

validateSubgraphDefinition(def: unknown): SubgraphDefinition

Parameters

hover a row to find it in the code

Required

defrequiredunknown
Returns SubgraphDefinition
name
Unique subgraph name (lowercase, alphanumeric + hyphens)
description
Human description
startBlock
Block height to start indexing from (default: 1)
backfillMode
'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
sources
Named source filters — keys become handler keys
schema
Tables in this subgraph
handlers
Handler functions, one per source, keyed by source name

Camelizes object keys recursively, the way print payloads are.

It uses the print-payload helper, not ABI toCamelCase, which strips -STX and prefixes leading digits.

camelizeKeys(obj: unknown): unknown

Parameters

hover a row to find it in the code

Required

objrequiredunknown

Infers per-topic field schemas from sampled print events. Counts and height bounds cover ALL rows of a topic (cheap); only a bounded subset is deserialized for typing. Rows with missing/undecodable hex still count but contribute nothing to typing.

inferPrintTopics(samples: PrintSample[]): InferredTopicSchema[]

Parameters

hover a row to find it in the code

Required

samplesrequiredPrintSample[]
Returns InferredTopicSchema[]
topic
string
count
number
first_height
number
last_height
number
non_tuple
boolean
fields
InferredPrintField[]

Handler context

The ctx every handler receives: write rows, read them back, call contracts.

import from @secondlayer/subgraphs

Reads a contract's read-only functions at the block being processed.

Results are untyped here. Call it through readContractAt to get typed results from the contract's ABI.

ctx.client.contract(
contractId: string,
abi: unknown,
options?: ChainReadOptions,
): { read: Record<string, (args?: Record<string, unknown>) => Promise<unknown>> }

Parameters

hover a row to find it in the code

Required

contractIdrequiredstring
abirequiredunknown

Options

options.cacheChainReadCacheMode

Queues a new row for a table.

Writes are batched and applied together when the block commits. _block_height and _tx_id are filled in for you.

ctx.insert<T extends TableName<S>>(table: T, row: WriteRow<S[T]>): void

Parameters

hover a row to find it in the code

Required

tablerequiredT
rowrequiredWriteRow<S[T]>

Queues an update to the rows matching where.

set holds only the columns to change.

ctx.update<T extends TableName<S>>(
table: T,
where: RowWhere<S, T>,
set: Partial<{ [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? never : K]: WriteColumnType<T["columns"][K]>; } & { [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? K : never]?: WriteColumnType<T["columns"][K]>; }>,
): void

Parameters

hover a row to find it in the code

Required

tablerequiredT
whererequiredRowWhere<S, T>
setrequiredPartial<{ [K in keyof T["columns"] as IsOptionalColumn<…

Inserts a row, or updates the one with the same key.

key should match a uniqueKeys constraint of the table. Without one the write falls back to a lookup first and logs a warning.

ctx.upsert<T extends TableName<S>, K extends Partial<WriteRow<S[T]>>>(
table: T,
key: K,
row: Omit<{ [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? never : K]: WriteColumnType<T["columns"][K]>; } & { [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? K : never]?: WriteColumnType<T["columns"][K]>; }, keyof K>
& Partial<{ [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? never : K]: WriteColumnType<T["columns"][K]>; } & { [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? K : never]?: WriteColumnType<T["columns"][K]>; }>,
): void

Parameters

hover a row to find it in the code

Required

tablerequiredT
keyrequiredK
rowrequiredOmit<{ [K in keyof T["columns"] as IsOptionalColumn<T["…

Queues deletion of the rows matching where.

ctx.delete<T extends TableName<S>>(table: T, where: RowWhere<S, T>): void

Parameters

hover a row to find it in the code

Required

tablerequiredT
whererequiredRowWhere<S, T>

Adds signed deltas to a counter row, creating it if missing.

It compiles to INSERT … ON CONFLICT (key) DO UPDATE SET col = COALESCE(col,0) + delta. Deltas commute, so it is the reorg-safe way to keep running totals such as per-cycle aggregates and balances. It needs a uniqueKeys constraint matching key, and deltas may be number or bigint.

ctx.increment<T extends TableName<S>>(
table: T,
key: Partial<{ [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? never : K]: WriteColumnType<T["columns"][K]>; } & { [K in keyof T["columns"] as IsOptionalColumn<T["columns"][K]> extends true ? K : never]?: WriteColumnType<T["columns"][K]>; }>,
deltas: Record<string, Record<ColumnName<S, T>, number | bigint>[string] | undefined>,
): void

Parameters

hover a row to find it in the code

Required

tablerequiredT
keyrequiredPartial<{ [K in keyof T["columns"] as IsOptionalColumn<…
deltasrequiredRecord<string, Record<ColumnName<S, T>, number | bigint…

Returns the first row matching where, or null.

Reads see the writes queued earlier in the same block, and rows come back in insertion order.

ctx.findOne<T extends TableName<S>>(
table: T,
where: RowWhere<S, T>,
): Promise<SystemRow & { [K in keyof T["columns"]]: InferColumnType<T["columns"][K]>; } | null>

Parameters

hover a row to find it in the code

Required

tablerequiredT
whererequiredRowWhere<S, T>
Returns Promise<SystemRow & { [K in keyof T["columns"]]: InferC…
_id
string
_blockHeight
bigint
_txId
string
_createdAt
string

Returns every row matching where, in insertion order.

Reads see the writes queued earlier in the same block.

ctx.findMany<T extends TableName<S>>(
table: T,
where: RowWhere<S, T>,
): Promise<SystemRow & { [K in keyof T["columns"]]: InferColumnType<T["columns"][K]>; }[]>

Parameters

hover a row to find it in the code

Required

tablerequiredT
whererequiredRowWhere<S, T>
Returns Promise<SystemRow & { [K in keyof T["columns"]]: InferC…
_id
string
_blockHeight
bigint
_txId
string
_createdAt
string

Contract reads

Read contract state from a handler, pinned to the block being processed.

import from @secondlayer/subgraphs

Reads a contract's read-only functions from a handler, with typed results.

Pass ctx, the contract id and its ABI. Every call is pinned to the block being processed. Set options.cache to "contract-constant" to fetch a value once per contract instead of once per block.

readContractAt<const C extends AbiContract>(
ctx: { client: ErasedChainReadClient },
contractId: string,
abi: C,
options?: ChainReadOptions,
): { read: ChainReadMethods<C> }

Parameters

hover a row to find it in the code

Required

ctx.clientrequiredErasedChainReadClient
contractIdrequiredstring
abirequiredC

Options

options.cacheChainReadCacheMode

Schema and SQL

The SQL and typed clients a subgraph's tables turn into, and the diff a deploy runs.

import from @secondlayer/subgraphs

Generates PostgreSQL DDL statements for a subgraph definition. Creates a dedicated schema subgraph_<name> with one table per schema entry, each with auto-columns and indexes.

generateSubgraphSQL(def: SubgraphDefinition, schemaNameOverride?: string): GeneratedSQL

Parameters

hover a row to find it in the code

Required

def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
schemaNameOverridestring
Returns GeneratedSQL
statements
string[]
hash
string

Generates a Prisma schema for a subgraph's tables.

Set modelsOnly to emit just the model blocks, to compose with your own schema.

generatePrismaSchema(def: SubgraphDefinition, opts?: PrismaGenOptions): string

Parameters

hover a row to find it in the code

Required

def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
opts.schemaNamestringPostgres schema the tables live in (account-scoped).
opts.datasourceEnvstringenv var the datasource url reads from.
opts.modelsOnlybooleanEmit only the model blocks (no datasource/generator). Lets users compose these models with their own schema via Prisma's prismaSchemaFolder.

Generates Drizzle table definitions for a subgraph's tables.

generateDrizzleSchema(def: SubgraphDefinition, opts?: DrizzleGenOptions): string

Parameters

hover a row to find it in the code

Required

def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
opts.schemaNamestring

Generates Kysely table interfaces for a subgraph's tables.

generateKyselySchema(def: SubgraphDefinition, opts?: KyselyGenOptions): string

Parameters

hover a row to find it in the code

Required

def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
opts.schemaNamestringPostgres schema the tables live in (account-scoped).

Generates a typed schema for Index tables.

target picks Prisma, Kysely, Drizzle or JSON Schema. Use tables to limit the output. Prisma omits tables with no read-set key.

generateIndexSchema(target: IndexCodegenTarget, opts?: IndexCodegenOptions): string

Parameters

hover a row to find it in the code

Required

targetrequiredIndexCodegenTarget

Options

opts.tablesstring[]Restrict output to these Index tables (default: all).
opts.schemaNamestringPostgres schema to qualify table names with (default: bare names).
opts.datasourceEnvstringPrisma only: datasource url env var (default DATABASE_URL).

Convert a subgraph name to its PostgreSQL schema name.

pgSchemaName(subgraphName: string): string

Parameters

hover a row to find it in the code

Required

subgraphNamerequiredstring

Deploys a subgraph schema to the database.

A new subgraph gets its schema and tables created and registered. The same schema hash only updates the handler path. An additive change adds columns and tables in place, and a breaking change reindexes by dropping and recreating the tables.

deploySchema(
db: AnyDb,
def: SubgraphDefinition,
handlerPath: string,
opts?: {
forceReindex?: boolean;
apiKeyId?: string;
accountId?: string;
schemaName?: string;
handlerCode?: string;
sourceCode?: string;
verification?: SubgraphVerification;
network?: string;
},
): Promise<{ action: "created" | "unchanged" | "handler_updated" | "updated" | "reindexed"; subgraphId: string; version: string; pin?: string; diff?: DeployDiff; warnings?: string[] }>

Parameters

hover a row to find it in the code

Required

dbrequiredAnyDb
def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name
handlerPathrequiredstring

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
opts.forceReindexboolean
opts.apiKeyIdstring
opts.accountIdstring
opts.schemaNamestring
opts.handlerCodestring
3 more options
opts.sourceCodestring
opts.verificationSubgraphVerification
opts.networkstring

Compare two multi-table subgraph schemas and return differences.

Index-only changes (indexed/search flags, composite indexes) are reported separately from data-shape changes: an index never requires a rebuild, so it must never land in changed/breaking — flipping indexed: true on a populated column used to cost a full reindex.

diffSchema(existing: SubgraphSchema, incoming: SubgraphSchema): TableDiff

Parameters

hover a row to find it in the code

Required

existingrequiredSubgraphSchema
incomingrequiredSubgraphSchema
Returns TableDiff
addedTables
Tables added to the schema
removedTables
Tables removed from the schema
tables
Per-table column diffs (only for tables present in both)
constraints
Per-table uniqueKeys / composite-index diffs (tables present in both)

Tells whether a schema diff is breaking, with the reasons.

Breaking means removed tables, removed columns, changed column types or removed uniqueKeys, which handler upserts conflict on. Index-flag and composite-index changes are never breaking.

hasBreakingChanges(diff: TableDiff): { breaking: boolean; reasons: string[] }

Parameters

hover a row to find it in the code

Required

addedTablesrequiredstring[]Tables added to the schema
removedTablesrequiredstring[]Tables removed from the schema
tablesrequiredRecord<string, ColumnDiff>Per-table column diffs (only for tables present in both)
constraintsrequiredRecord<string, ConstraintDiff>Per-table uniqueKeys / composite-index diffs (tables present in both)

Render the DDL a deploy would run, without executing. Powers --dry-run: the user reviews exactly what a deploy does first.

renderDeployPlan(def: SubgraphDefinition, schemaName?: string): DeployPlan

Parameters

hover a row to find it in the code

Required

def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
schemaNamestring
Returns DeployPlan
schemaName
string
dropStatement
DROP SCHEMA … CASCADE a destructive rebuild would run first.
statements
DDL the deploy will run.

Block sources

Which block source a subgraph's filters allow.

import from @secondlayer/subgraphs

Tells whether a subgraph can skip blocks without its events.

Sparse scanning is sound only when every source is an event-type filter. A contract_call or contract_deploy source matches transactions, which the event probe cannot see.

canSparseScan(subgraph: SubgraphDefinition): boolean

Parameters

hover a row to find it in the code

Required

namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
schemarequiredSubgraphSchemaTables in this subgraph
handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

descriptionstringHuman description
startBlocknumberBlock height to start indexing from (default: 1)
backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.

Tells whether a subgraph's sources can be read from the Streams index.

Every source must be a known event-type, contract_call or contract_deploy filter, and array-style sources never qualify because they leak the unreconstructable _eventId. Trait scope is allowed: trait resolution reads the contract registry on the platform database, which the processor always holds. Everything else stays on the database tap.

isStreamsIndexEligible(subgraph: SubgraphDefinition): boolean

Parameters

hover a row to find it in the code

Required

namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
schemarequiredSubgraphSchemaTables in this subgraph
handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

descriptionstringHuman description
startBlocknumberBlock height to start indexing from (default: 1)
backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.

Lists the event types and contract scopes a subgraph's filters match.

Each target is a decoded event type with an optional contract scope. One function feeds both the sparse probe and the event walks, so a new contract-pinning filter field is added here once.

Per event type: if ANY filter for it is unscoped, a single unscoped target covers everything (today's behavior); otherwise one target per distinct pinned contract.

sparseProbeTargets(subgraph: SubgraphDefinition): SparseProbeTarget[]

Parameters

hover a row to find it in the code

Required

namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
schemarequiredSubgraphSchemaTables in this subgraph
handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

descriptionstringHuman description
startBlocknumberBlock height to start indexing from (default: 1)
backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
Returns SparseProbeTarget[]
eventType
string
contractId
string

Verification

The pin and the findings that say how far a subgraph's rows can be checked.

import from @secondlayer/subgraphs

Content hash over everything that shapes a subgraph's rows. Unlike schema_hash, it changes with the handler, startBlock and runtime, so two deploys with the same pin produce the same rows from the same chain.

computePin(input: PinInput): string

Parameters

hover a row to find it in the code

Required

schemaHashrequiredstringExisting schema_hash (name + schema + sources).
handlerCoderequiredstringBundled ESM exactly as stored in subgraphs.handler_code.
networkrequiredstring

Options

startBlocknumber | null
runtimestringDefaults to SUBGRAPHS_RUNTIME.

Derive how far a subgraph's rows can be checked. Level = the highest any source or handler read needs. reasons lists everything standing between the subgraph and checkable rows; a state subgraph with any reason is refused at deploy, so every stored state row has none. Never configured, never a flag.

deriveVerification(
def: {
sources: Record<string, SubgraphFilter>;
backfillMode?: "blocking" | "concurrent";
},
findings?: readonly HandlerFinding[],
): SubgraphVerification

Parameters

hover a row to find it in the code

Required

def.sourcesrequiredRecord<string, SubgraphFilter>

Options

def.backfillMode"blocking" | "concurrent"
findingsreadonly HandlerFinding[]

Formats a handler finding as file:line:column name: reason.

formatFinding(f: HandlerFinding): string

Parameters

hover a row to find it in the code

Required

kindrequired"nondeterministic" | "needs-events"
namerequiredstringWhat was used, e.g. Date, Math.random, import(), ctx.client.
reasonrequiredstring
filerequiredstringOriginal source file when a sourcemap was available, else the bundle.
linerequirednumber1-based.
columnrequirednumber1-based.

Tells whether a deploy must be refused for breaking determinism.

It is refused when the sources are provable (state), so the determinism contract is enforced, and the scan broke it.

isDeterminismViolation(v: SubgraphVerification): boolean

Parameters

hover a row to find it in the code

Required

vrequiredSubgraphVerification

Loads a bundled handler into a fresh deterministic realm.

The bundle is ESM, as stored in handler_code. The returned definition has every handler wrapped to receive the deterministicContext facade.

loadDeterministicDefinition(handlerCode: string): Promise<SubgraphDefinition>

Parameters

hover a row to find it in the code

Required

handlerCoderequiredstring
Returns Promise<SubgraphDefinition>
name
Unique subgraph name (lowercase, alphanumeric + hyphens)
description
Human description
startBlock
Block height to start indexing from (default: 1)
backfillMode
'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.
sources
Named source filters — keys become handler keys
schema
Tables in this subgraph
handlers
Handler functions, one per source, keyed by source name

Constants

SUBGRAPHS_RUNTIME
Runtime identity baked into every pin: matcher, decode and flush semantics. string

Replay

Re-run a subgraph from the archive and compare it with what is served.

import from @secondlayer/subgraphs/verify

Checks a served pin against the served bundle.

The preimage must hash to the pin, and name this handler code, this schema and this startBlock. A runtime other than this package's is only a note, since rows can still match.

checkPin(
served: { pin: string | null; pinPreimage: string | null; handlerCode: string },
def: SubgraphDefinition,
): PinCheck

Parameters

hover a row to find it in the code

Required

served.pinrequiredstring | null
served.pinPreimagerequiredstring | null
served.handlerCoderequiredstring
def.namerequiredstringUnique subgraph name (lowercase, alphanumeric + hyphens)
def.sourcesrequiredRecord<string, SubgraphFilter>Named source filters — keys become handler keys
def.schemarequiredSubgraphSchemaTables in this subgraph
def.handlersrequiredRecord<string, SubgraphHandler>Handler functions, one per source, keyed by source name

Options

def.descriptionstringHuman description
def.startBlocknumberBlock height to start indexing from (default: 1)
def.backfillMode"blocking" | "concurrent"'concurrent' = tip-first: live at tip now, history backfills behind. Requires order-tolerant handlers. Default 'blocking'.

The replay loop over an injected verifier.

replayBlocks(
deps: ReplayDeps,
opts: {
handlerCode: string;
from: number;
to: number;
startBlock?: number;
onProgress?: (height: number) => void;
}
& { bitcoinCheckpoint?: number },
): Promise<ReplayResult>

Parameters

hover a row to find it in the code

Required

deps.verifyrequired(height: number) => Promise<BlockVerification>
deps.burnHeightHintrequired(consensusHash: string) => Promise<number>The source's (unproven) burn height for a consensus hash.
opts.handlerCoderequiredstring
opts.fromrequirednumber
opts.torequirednumber

Options

opts.startBlocknumber
opts.onProgress(height: number) => void
opts.bitcoinCheckpointnumber
Returns Promise<ReplayResult>
ok
boolean
inconclusive
Set when the range starts after startBlock and a handler read or merged earlier rows.
from
number
to
number
blocks
number
txs
number
writes
Named writes to the subgraph's contracts.
events
Events offered to a handler.
handlerErrors
number
marfProofs
MARF proofs the walk took (one per window top below the checkpoint).
tables
The memory store at to.
notes
string[]
failures
The first entry is the first broken link.

Lists the contracts a state subgraph's writes come from.

It returns an error when a source does not fix one, because completeness would then need every contract's writes.

replayContracts(
def: { sources: Record<string, SubgraphFilter> },
): string[] | { error: string }

Parameters

hover a row to find it in the code

Required

sourcesrequiredRecord<string, SubgraphFilter>

Verifies every block in a range and recomputes the subgraph's tables.

The range is [from, to], and blocks are verified from the checkpoint.

replaySubgraph(opts: ReplayOptions): Promise<ReplayResult>

Parameters

hover a row to find it in the code

Required

sourcerequiredProofSourceNeeds getStateWrites: the names are what feed the handlers.
handlerCoderequiredstringThe bundled ESM as stored in handler_code.
fromrequirednumber
torequirednumber

Options

checkpointVerifyCheckpointDefaults to MAINNET_CHECKPOINT.
startBlocknumberDefaults to the definition's startBlock.
onProgress(height: number) => void
Returns Promise<ReplayResult>
ok
boolean
inconclusive
Set when the range starts after startBlock and a handler read or merged earlier rows.
from
number
to
number
blocks
number
txs
number
writes
Named writes to the subgraph's contracts.
events
Events offered to a handler.
handlerErrors
number
marfProofs
MARF proofs the walk took (one per window top below the checkpoint).
tables
The memory store at to.
notes
string[]
failures
The first entry is the first broken link.

Compares replayed tables with the served ones over a block range.

Rows are matched on _block_height within [from, to]. Keyed tables, by their first uniqueKeys tuple, compare row by row, and unkeyed tables compare as multisets.

compareRows(
schema: SubgraphSchema,
replayed: { from: number; to: number; tables: Map<string, Row[]> },
served: Map<string, Row[]>,
): RowComparison

Parameters

hover a row to find it in the code

Required

schemarequiredSubgraphSchema
replayed.fromrequirednumber
replayed.torequirednumber
replayed.tablesrequiredMap<string, Row[]>
servedrequiredMap<string, Row[]>
Returns RowComparison
failures
ReplayFailure[]
tables
TableComparison[]

Constants

REPLAY_WINDOW
Verify blocks 512 at a time: the top by one MARF jump, the rest by parent links. 512

Testing

Run a handler against fixture events without deploying.

import from @secondlayer/subgraphs/testing

Builds a handler-shaped event for a source, typed by that source's filter.

data supplies the decoded, camelCased fields the runtime would have produced. Transaction metadata is filled in.

buildEvent<const F extends SubgraphFilter>(
source: F,
payload: Record<string, unknown>,
options?: {
tx?: {
txId?: string;
sender?: string;
type?: string;
status?: string;
contractId?: string | null;
functionName?: string | null;
};
},
): EventForFilter<F>

Parameters

hover a row to find it in the code

Required

sourcerequiredF
payloadrequiredRecord<string, unknown>

Options

options.tx{ txId?: string; sender?: string; type?: string; status…

Build a typed event payload based on the source filter type. Returns the payload the handler will receive.

buildEventPayload(
filter: SubgraphFilter,
tx: TxRecord,
event: EventRecord | null,
): Record<string, unknown>

Parameters

hover a row to find it in the code

Required

filterrequiredSubgraphFilter
tx.tx_idrequiredstring
tx.typerequiredstring
tx.senderrequiredstring
tx.statusrequiredstring
eventrequiredEventRecord | null

Options

tx.tx_indexnumberPosition within the block — the runner sorts dispatch into chain order.
tx.contract_idstring | null
tx.function_namestring | null
tx.function_argsunknown
tx.raw_resultstring | null

Builds an in-memory context for a subgraph's schema.

It is the real handler context with its row store swapped for memory, so reads see earlier writes. Use ctx.rows(table) to inspect what a handler wrote.

createTestContext<const S extends SubgraphSchema>(
schema: S,
options?: {
block?: {
height?: number;
hash?: string;
timestamp?: number;
burnBlockHeight?: number;
indexBlockHash?: string | null;
};
tx?: {
txId?: string;
sender?: string;
type?: string;
status?: string;
contractId?: string | null;
functionName?: string | null;
};
reads?: Record<string, unknown>;
},
): TestSubgraphContext<S>

Parameters

hover a row to find it in the code

Required

schemarequiredS

Options

options.block{ height?: number; hash?: string; timestamp?: number; b…
options.tx{ txId?: string; sender?: string; type?: string; status…
options.readsRecord<string, unknown>
Returns TestSubgraphContext<S>
rows
Current rows of table, INCLUDING writes queued by the handler you just ran (the same read-your-writes overlay the runtime uses). Drop straight into toMatchInlineSnapshot().
commit
Commit pending writes into the in-memory store, as an end-of-block flush would — call between blocks when a test spans several.
setTx
Swap the transaction metadata handlers see (ctx.tx).
pendingInsertsSince
Insert ops queued since opsCheckpoint(), for preview IN/OUT traces.
opsCheckpoint
Length of the pending-ops queue (same as runtime opsCheckpoint).
tx
TxMeta
block
{ height: number; hash: string; timestamp: number; burnBlockHeight: number; ind…
client
Read-only contract calls pinned to this block. Erased here for the same reason as on SubgraphContext: an ABI-generic method on a structurally compared interface exceeds TS's instantiation budget. Wrap it with readContractAt(ctx, id, abi) for full ABI typing at the call site.
insert
Queues a new row for a table. Writes are batched and applied together when the block commits. _block_height and _tx_id are filled in for you.
update
Queues an update to the rows matching where. set holds only the columns to change.
upsert
Inserts a row, or updates the one with the same key. key should match a uniqueKeys constraint of the table. Without one the write falls back to a lookup first and logs a warning.
delete
Queues deletion of the rows matching where.
increment
Adds signed deltas to a counter row, creating it if missing. It compiles to INSERT … ON CONFLICT (key) DO UPDATE SET col = COALESCE(col,0) + delta. Deltas commute, so it is the reorg-safe way to keep running totals such as per-cycle aggregates and balances. It needs a uniqueKeys constraint matching key, and deltas may be number or bigint.
findOne
Returns the first row matching where, or null. Reads see the writes queued earlier in the same block, and rows come back in insertion order.
findMany
Returns every row matching where, in insertion order. Reads see the writes queued earlier in the same block.

Run handlers against pre-built sample payloads in memory (no Index, no Postgres). Deploy uses this with print-schema dummy events to refuse EMPTY_MAPPING before DDL apply.

probeHandlers(
def: ProbeHandlersDef,
samples: { source: string; event: Record<string, unknown> }[],
): Promise<ProbeHandlersResult>

Parameters

hover a row to find it in the code

Required

def.schemarequiredSubgraphSchema
def.sourcesrequiredRecord<string, { type: string; [key: string]: unknown }>
def.handlersrequiredRecord<string, unknown>
samplesrequired{ source: string; event: Record<string, unknown> }[]
Returns Promise<ProbeHandlersResult>
matched
number
written
number
tables
string[]
firstEventKeys
string[]

Apply local handlers to Index rows in memory. Fail-closed: matched events with zero written rows is EMPTY_MAPPING (the bns-names field-mapping shape).

runSubgraphTest(input: RunSubgraphTestInput): Promise<SubgraphTestResult>

Parameters

hover a row to find it in the code

Required

schemarequiredSubgraphSchema
handlersrequiredRecord<string, unknown>
sourcesrequiredRecord<string, SubgraphTestSource>
eventsrequiredRecord<string, IndexTestRow[]>

Options

tracebooleanCollect per-event IN/OUT traces (preview).
Returns Promise<SubgraphTestResult>
ok
boolean
code
"EMPTY_MAPPING" | "NO_EVENTS" | "NO_SOURCES"
matched
number
written
number
tables
{ name: string; rows: number; sampleRow?: Record<string, unknown> }[]
firstEvent
{ source: string; data?: unknown }
hint
string
traces
Present when trace: true.
unusedInKeys
Keys present on every matched IN and never written to any OUT column.

Map an Index contract-call row onto the ContractCallEvent handler shape.

toContractCallPayload(row: IndexContractCallRow): Record<string, unknown>

Parameters

hover a row to find it in the code

Required

senderrequiredstring
tx_idrequiredstring
statusrequiredstring

Options

contract_idstring | null
function_namestring | null
argsunknown
resultunknown
result_hexunknown
cursorstring

Map an Index event row onto the payload shape a handler expects.

toHandlerPayload(
_filter: { type: string } | undefined,
row: IndexEventRow,
): Record<string, unknown>

Parameters

hover a row to find it in the code

Required

_filterrequired{ type: string } | undefined
row.event_typerequiredstring

Options

row.contract_idstring | null
row.payloadunknownPrint events carry { topic, value }; other types vary.
row.senderunknown
row.recipientunknown
row.amountunknown
row.asset_identifierunknown
row.valueunknown
row.cursorstring
AggregateResult
Result shape inferred from an AggregateSpec.
AnyEvent
Union of every event payload.
ChainReadCacheMode
How long a read stays valid.
ChainReadError
A handler asked for something no node can answer: a function missing from the ABI, or one that isn't read-only.
ChainReadMethods
The camelCased read methods of an ABI.
ChainReadOptions
Options for a contract read, such as cache mode.
ColumnDiff
added, removed, changed, indexChanged
ColumnToTS
Maps a ColumnType string literal to its TypeScript equivalent
ColumnType
Supported column types for subgraph schemas
ComparisonFilter
Comparison operators for a scalar value
ConstraintDiff
addedUniqueKeys, removedUniqueKeys, addedIndexes, removedIndexes
ContractCallEvent
Event shape passed to contract_call handlers.
ContractCallFilter
Contract event filters
ContractCallPayload
Contract-call payload.
ContractDeployFilter
type, deployer, contractName
ContractDeployPayload
contractId, deployer, tx
DeployDiff
The tables and columns a deploy adds, removes, or breaks.
DeployPlan
The schema name and SQL a deploy will run.
DrizzleGenOptions
Options for generating Drizzle table definitions.
ErasedChainReadClient
ChainReadClient with the ABI generic erased.
EventForFilter
The event payload a handler receives for a given source filter.
EventRecord
An event as a subgraph source matcher reads it.
EventTrace
Per-event IN/OUT trace for subgraphs preview.
FactoryScope
Scope a source to a DYNAMIC address set discovered from another source's events, instead of a fixed contract list.
FindManyOptions
where, orderBy, limit, cursor, fields
FindManyPage
A page of rows from findMany, keyset-paginated.
FtBurnFilter
type, assetIdentifier, sender, minAmount, trait
FtBurnPayload
sender, amount, assetIdentifier, tx
FtMintFilter
type, assetIdentifier, recipient, minAmount, trait
FtMintPayload
recipient, amount, assetIdentifier, tx
FtTransferFilter
FT event filters
FtTransferPayload
sender, recipient, amount, assetIdentifier, tx
GeneratedSQL
The SQL statements for a subgraph's tables and their hash.
HandlerFinding
One thing the deploy scan found in a bundled handler.
HandlerReadError
A ctx read failed for a reason outside the handler: the database under a verifiable subgraph, or a chain read (ctx.client) on any subgraph that had no node, a node that didn't answer, or a cache error.
IndexCodegenOptions
Options for generating the Index schema: tables, schema name.
IndexCodegenTarget
A portable, typed schema target for the public Index read domain.
IndexContractCallRow
Structural Index contract-call row.
IndexEventRow
Structural Index event row (avoids an SDK dep on this package).
IndexTestRow
IndexEventRow | IndexContractCallRow
InferColumnType
Infer TS type for a single column definition, respecting nullable
InferContext
The typed handler context for a subgraph definition — for annotating helpers extracted out of a handler:
InferredPrintField
name, camel_name, clarity_type, ts_type, column_type, always_present, …
InferredTopicSchema
The fields seen in one print topic, with counts and heights.
InferSubgraphClient
Infer a typed client object from a subgraph definition shape
InferTableRow
Infer the row shape for a single table definition
KyselyGenOptions
Options for generating Kysely table interfaces.
MapDeleteEvent
contractId, map, rawKey, key, tx
MapDeletePayload
F extends { abi: infer A extends AbiContract; map: infer N extends string; } ? N extends ExtractMapNames<A> ?…
MapWriteEvent
contractId, map, rawKey, key, rawValue, value, …
MapWriteFilter
type, contractId, map, abi, trait, factory
MapWritePayload
F extends { abi: infer A extends AbiContract; map: infer N extends string; } ? N extends ExtractMapNames<A> ?…
NestedContractCallFilter
type, contractId, functionName, caller, sender, trait, …
NestedContractCallPayload
contractId, sender, caller, functionName, args, arguments, …
NftBurnFilter
type, assetIdentifier, sender, trait
NftBurnPayload
sender, tokenId, assetIdentifier, tx
NftMintFilter
type, assetIdentifier, recipient, trait
NftMintPayload
recipient, tokenId, assetIdentifier, tx
NftTransferFilter
NFT event filters
NftTransferPayload
sender, recipient, tokenId, assetIdentifier, tx
NondeterminismError
A handler broke the determinism contract at runtime.
PinCheck
Whether a served pin matches the served bundle.
PinInput
What a pin is computed from: schema hash, handler code, network.
PinnedPrintEventFilter
Pinned to one or more contract ids with no trait — prints is required so event.data is typed and empty-table handlers cannot typecheck.
PrintDataOf
event.data for one topic: declared fields typed, with { optional: true } fields made optional KEYS (a field observed on only some events must not be typed as always present).
PrintEventFilter
Print-event filter.
PrintEventFor
Print event typed per topic.
PrintEventPayload
contractId, topic, data, tx
PrintEventPrints
Per-topic field schema for a print_event source.
PrintField
One declared field of a print payload.
PrintFieldToTS
TS type of one declared print field (nested tuples and lists included).
PrintSample
Empirical print-payload schema inference.
PrintValidationResult
{ ok: true; } | { ok: false; reason: string; }
PrismaGenOptions
Options for generating Prisma models.
ProbeHandlersDef
A subgraph's schema, sources, and handlers, for a probe run.
ProbeHandlersResult
How many events matched and rows a probe wrote.
ReplayDeps
Seams for tests: the block verifier and the burn height hint.
ReplayFailure
step, message, height, table, key, unavailable
ReplayOptions
What to replay: proof source, handler bundle, and block range.
ReplayResult
The outcome of a replay: pass or fail, blocks covered, failures.
ReplayStep
"pin" | "blocks" | "txs" | "inputs" | "handlers" | "rows"
Row
A table row as a plain column-to-value record.
RowComparison
Replayed tables compared with served tables, and any mismatches.
RowValue
Scalar types that can appear as row values
RunSubgraphTestInput
A subgraph's schema, handlers, sources, and the events to run.
SparseProbeTarget
A (decoded event type, optional contract scope) pair the sparse probe checks.
StxBurnFilter
type, sender, minAmount
StxBurnPayload
sender, amount, tx
StxLockFilter
type, lockedAddress, minAmount
StxLockPayload
lockedAddress, lockedAmount, unlockHeight, tx
StxMintFilter
type, recipient, minAmount
StxMintPayload
recipient, amount, tx
StxTransferFilter
STX event filters
StxTransferPayload
sender, recipient, amount, memo, tx
SubgraphColumn
Column definition in a subgraph table
SubgraphContext
Context passed to subgraph handlers during event processing
SubgraphDefinition
Complete subgraph definition
SubgraphFilter
A subgraph filter, discriminated on type.
SubgraphHandler
Handler function that processes events and writes to the subgraph
SubgraphSchema
Maps table names to table definitions.
SubgraphTable
Table definition within a subgraph schema.
SubgraphTestResult
The outcome of a test run: matches, writes, and table rows.
SubgraphTestSource
type, contractId, topic, where, functionName
SubgraphTip
Wire tip: chain height, subgraph's processed height, and the gap between them — same object on every /v1 row/page response.
SubscribeOptions
Options for the realtime row stream (SSE).
SystemOrderByAliases
blockHeight, txId, createdAt, id
SystemRow
Shape of system columns on every returned row (camelCase, underscore-prefixed)
SystemWhereAliases
No-prefix aliases for system columns accepted in where/orderBy inputs.
TableComparison
table, equal, superseded, digest, servedDigest
TableDiff
Tables added, removed, or changed between two schemas.
TestSubgraphContext
An in-memory subgraph context for tests.
TraitPrintEventFilter
Trait-scoped print source (optionally narrowed by contractId).
TxMeta
Transaction metadata available in handlers
TxRecord
A transaction as a subgraph source matcher reads it.
TypedHandlers
Handlers keyed by source name, one per source.
TypedSubgraphContext
Schema-typed handler context.
TypedSubgraphDefinition
A subgraph definition that keeps sources and schema literals typed.
UnpinnedPrintEventFilter
Unpinned / factory-only print source (no contractId).
VarSetEvent
contractId, varName, rawValue, value, tx
VarSetFilter
type, contractId, varName, abi, trait, factory
VarSetPayload
State-write payloads.
WhereInput
Where clause — each column accepts a scalar (eq) or comparison object
WriteRow
Row shape accepted by ctx.insert.
Edit on GitHub