Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Filters

Describe a chain event once, then hand the same filter to Index, Streams, Webhooks, or Subgraphs.

@secondlayer/stacks/filters normalizes the three things that differ per surface: event-type names (print vs print_event), amount encoding (bigint in code, strings on the wire), and which fields each surface can actually filter on. You write one validated filter; each to*() projection emits the exact request shape that surface expects, and refuses to emit one it cannot honor.

The Four Surfaces

This module only matters alongside a Secondlayer instance. If you use @secondlayer/stacks purely as a chain client, skip it. The surfaces are Secondlayer platform products reached through the @secondlayer/sdk client (sl below); the platform-side write-up is secondlayer.tools/docs/filters.

  • Index pulls decoded history over HTTP, paginated and cursor-based. sl.index.events.list(f.toIndexParams()). Docs →
  • Streams consumes events as they land: ordered, resumable, reorg-aware. sl.streams.events.consume(f.toStreamsParams()). Docs →
  • Webhooks push matching events to your URL; a chain trigger is the filter half of a webhook. sl.webhooks.create({ triggers: [f.toChainTrigger()] }). Docs →
  • Subgraphs turn event sources plus handlers into a queryable dataset. defineSubgraph({ sources: { x: f.toSubgraphSource() } }). Docs →

Build a Filter

on.* has one factory per event type. Fields are validated on construction: principals, contract ids, and asset identifiers that are malformed throw immediately.

import { on } from "@secondlayer/stacks/filters";
 
const USDC = "SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-aeusdc::aeUSDC";
 
const usdc = on.ftTransfer({ assetIdentifier: USDC, minAmount: 1_000_000n });
const prints = on.print({ contractId: "SP3Y2...token-aeusdc", topic: "transfer" });
const calls = on.contractCall({ contractId: "SP3Y2...token-aeusdc", functionName: "transfer" });
const deposits = on.sbtcDeposit({ minAmount: 100_000n });

makeChainEventFilter(type, fields) is the generic form; on.ftTransfer(f) is makeChainEventFilter("ft_transfer", f).

Project to a Surface

The same filter feeds all four surfaces.

import { SecondLayer } from "@secondlayer/sdk";
import { defineSubgraph } from "@secondlayer/subgraphs";
 
const sl = new SecondLayer({ baseUrl: "https://your-instance.example" });
 
await sl.index.events.list(usdc.toIndexParams({ limit: 100 }));                 // pull history
await sl.streams.events.consume({ ...usdc.toStreamsParams(), onBatch });        // live firehose
await sl.webhooks.create({ name: "usdc", url, triggers: [usdc.toChainTrigger()] }); // push
defineSubgraph({ sources: { usdc: usdc.toSubgraphSource() }, schema, handlers }); // dataset
ProjectionOutputNotes
toIndexParams(extra?){ eventType, ...fields, ...extra }print_event becomes print
toStreamsParams(extra?){ types: [type], ...fields, ...extra }print_event becomes print
toChainTrigger(){ type, ...fields }bigint amounts stringified for JSON
toSubgraphSource()the spec, bigint preservedkeeps abi / prints literals for handler typing

A surface a member cannot reach is a missing method, which is a compile error, not a runtime surprise:

const sbtc = on.sbtcDeposit({ minAmount: 100_000n });
sbtc.toChainTrigger();   // ok: { type: "sbtc_deposit", minAmount: "100000" }
sbtc.toIndexParams();    // type error: sBTC lifecycle filters are Webhooks-only

What Throws Where

A field the target cannot express throws at projection time, naming the surface that can.

const usdc = on.ftTransfer({ assetIdentifier: USDC, minAmount: 1_000_000n });
usdc.toIndexParams();
// Error: minAmount cannot be expressed on Index events — amount predicates are
// Webhooks/Subgraphs-only; filter client-side on the decoded rows.
FieldIndexStreamsWebhooksSubgraphs
minAmount / maxAmountthrowsthrowsokok
topic (print)throwsthrowsokok
wildcard * in a principal or assetthrowsthrowsokok
traitok, not with contractIdthrowsokok, ANDed with contractId
contractId as a setokokthrowsok
factorythrowsthrowsthrowsok
callerthrows (use index.contractCalls)okokok

Event Types

import { CHAIN_EVENT_FILTER_TYPES, DECODED_EVENT_TYPES } from "@secondlayer/stacks/filters";

DECODED_EVENT_TYPES is the ten-member set Index and Streams share: stx_transfer, stx_mint, stx_burn, stx_lock, ft_transfer, ft_mint, ft_burn, nft_transfer, nft_mint, nft_burn, print.

CHAIN_EVENT_FILTER_TYPES adds the contract-shaped members (contract_call, contract_deploy, print_event) and the five sBTC lifecycle members (sbtc_deposit, sbtc_withdrawal_create, sbtc_withdrawal_accept, sbtc_withdrawal_reject, sbtc_withdrawal_swept_confirmed).

Spec Fields

FactoryFields
on.stxTransfersender, recipient, minAmount, maxAmount
on.stxMint / on.stxBurn / on.stxLockrecipient / sender / lockedAddress, minAmount
on.ftTransferassetIdentifier, sender, recipient, minAmount, trait
on.ftMint / on.ftBurnassetIdentifier, recipient / sender, minAmount, trait
on.nftTransfer / on.nftMint / on.nftBurnassetIdentifier, sender, recipient, trait
on.contractCallcontractId (one or up to 20), functionName, caller, abi, trait, factory
on.contractDeploydeployer, contractName
on.printcontractId (one or up to 20), topic, prints, trait, factory
on.sbtcDepositsender, minAmount, maxAmount, bitcoinTxid, requestId
on.sbtcWithdrawalCreatesender, minAmount, maxAmount, requestId
on.sbtcWithdrawalAccept / SweptConfirmedrequestId, sweepTxid
on.sbtcWithdrawalRejectrequestId

abi and prints are type-only decorations: on.contractCall({ abi }) keeps event.input typed inside defineSubgraph, and on.print({ prints }) narrows event.data per topic. Both are dropped from every projection except toSubgraphSource.

Validation

The same validators the factories use, for form and config code.

import {
  assetId,
  isPrincipal,
  hasWildcard,
  assertPrincipalish,
  assertContractId,
  assertAssetIdentifier,
} from "@secondlayer/stacks/filters";
 
assetId("SP3Y2...token-aeusdc::aeUSDC");   // narrows to AssetIdentifier, throws if malformed
isPrincipal("SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF"); // true
hasWildcard("SP3Y2*");                      // true; wildcards skip validation and are Webhooks/Subgraphs-only
assertContractId("contractId", "SP3Y2...token-aeusdc::aeUSDC"); // throws: that is an asset id, not a contract id

Round-Tripping Subgraph Sources

fromSubgraphSource rebuilds a filter from a subgraph-source object. toSubgraphSource(fromSubgraphSource(s)) deep-equals s for every production source, which is how existing subgraph definitions migrate onto the shared vocabulary.

import { fromSubgraphSource } from "@secondlayer/stacks/filters";
 
const filter = fromSubgraphSource({ type: "ft_transfer", assetIdentifier: USDC, minAmount: 1_000_000n });
filter.toChainTrigger(); // now also a webhook trigger