You write the loop, we hand you decoded events with cursors and reorgs already handled. Deploy it to Railway, Fly, or your own box. No node to run.
One typed filter describes the chain event. Each surface gets its own explicit projection — including the conversions and disagreements a spread would hide.
Read about filters →import { on } from "@secondlayer/stacks/filters";
const whales = on.ftTransfer({
assetIdentifier:
"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
minAmount: 100_000_000n, // ≥ 1 BTC
});sl.index.events.list(whales.toIndexParams({ limit: 50 }));sl.streams.events.list(whales.toStreamsParams());sl.subscriptions.create({ triggers: [whales.toChainTrigger()], url });on.sbtcDeposit({}).toIndexParams();
// Property 'toIndexParams' does not exist. Subscriptions only.Principals are validated at construction: a contract id where an asset identifier belongs throws naming the field, instead of quietly matching zero rows.
Give a source its contract's ABI and event.input is typed per function — names checked, integers as bigint, wrong fields caught before deploy.
import { marketplaceAbi } from "./marketplace.abi";
sources: {
sale: {
type: "contract_call",
contractId: MARKETPLACE,
functionName: "purchase-asset",
abi: marketplaceAbi,
},
},
handlers: {
sale: (event, ctx) => ctx.insert("sales", {
collection: event.input.collection,
token_id: event.input.tokenId,
amount: event.input.amount,
}),
},// marketplace.abi.ts — generated from the deployed contract
export const marketplaceAbi = {
functions: [{
name: "purchase-asset",
access: "public",
args: [
{ name: "collection", type: "principal" },
{ name: "token-id", type: "uint128" },
{ name: "price", type: "uint128" },
],
/* outputs … */
}],
} as const;kebab-case on chain, camelCase in your handler: token-id → tokenId. A uint128 arrives as bigint — never a truncated number.
Run real chain events through your local handler code before you deploy. The first run records a cassette, so every run after is free and offline.
Read about testing →import { buildEvent, createTestContext }
from "@secondlayer/subgraphs/testing";
test("registers a name from a nested tuple", async () => {
const ctx = createTestContext(bns.schema, {
block: { height: 167_484 },
});
await bns.handlers.bns!(
buildEvent(bns.sources.bns, {
topic: "name-register", data,
}),
ctx,
);
expect(await ctx.rows("names")).toMatchInlineSnapshot();
});Open data, open SDKs, honest infrastructure — decoded sBTC, PoX, and Clarity calls included.