.md

SDK

Talk to Secondlayer, hosted or your own instance, from TypeScript without hand-writing cursor walks, reorg rewind, or signature checks.

Every export, with signatures: SDK reference.

$bun add @secondlayer/sdk
import { SecondLayer } from "@secondlayer/sdk";
const sl = new SecondLayer({ apiKey: process.env.SECONDLAYER_API_KEY });

3

The client reads SECONDLAYER_API_URL for the host. Leave apiKey out and the key follows the host:

HostKey read
api.secondlayer.toolsSECONDLAYER_API_KEY
anywhere elseINSTANCE_TOKEN (Self-host)

accountKey is for archive operations only. See Keys and billing.

One client, every surface

  • sl.streams: raw, ordered chain events (cursor-paginated, replayable).
  • sl.index: decoded rows: FT/NFT transfers, all event types, contract calls, and printSchema(contractId).
  • sl.contracts: find deployed contracts by trait (SIP-009/010/013).
  • sl.subgraphs: your app-specific tables, plus open /v1 reads (rows).
  • sl.webhooks: create and manage webhooks on raw chain events.
const tip = await sl.streams.tip();
const page = await sl.streams.events.list({
types: ["ft_transfer"],
contractId: "SP000000000000000000002Q6VF78.sbtc-token",
limit: 10,
});

Each surface documents its own calls:

TaskCallDocumented in
Webhook on raw chain eventssl.webhooks.create({ name, url, triggers })Webhooks
Re-deliver historysl.webhooks.replay(id, { fromBlock, toBlock })Replay
Check a delivery signatureverifyWebhookSignature, verifySecondlayerSignatureReceive and verify
Prove a transaction is in a blockverifyTransactionProof, fetchRewardSetVerify
Tail the live firehosesl.streams.events.subscribe(options)Streams
Read subgraph tablessl.subgraphs.rows, sl.subgraphs.typedSubgraphs
Stream or roll up rowssubgraph.<table>.subscribe, .aggregateRead rows
Let a sink own the transactionkyselySink, drizzleSink, bunSqliteSinkSinks
Describe one event for every surfaceon.ftMint(...), .toIndexParams()Filters
  • A failed call throws ApiError.
  • ApiError.requestId is the request_id to quote when you report a problem.

trigger.* is not on.*

  • trigger.* (from @secondlayer/sdk) configures chain webhooks.
  • on.* (from @secondlayer/stacks) configures subgraph sources in a handler config.
  • @secondlayer/stacks is the typed chain client, documented at stacks.secondlayer.tools.

Checkpointed consumer

consume polls and commits a cursor so you never miss or double-process. Return the committed cursor from onBatch.

Available on:

  • sl.streams.events
  • sl.index.events (takes eventType)
  • sl.index.contractCalls
consumer.tsts
await sl.index.contractCalls.consume({
contractId: "SP...marketplace-v4",
functionName: "purchase-asset",
fromCursor: await loadCheckpoint(), // null on first run
fromHeight: 0,
^First run backfills as far as the instance holds.
onBatch: async (calls, ctx) => {
await commitRowsAndCheckpoint(calls, ctx.cursor);
return ctx.cursor;
^Return the cursor you committed.
},
onReorg: async (reorg) => {
^Fires after the cursor rewinds.
await rollbackFromHeight(reorg.fork_point_height); // inclusive of the fork block
},
});
  • Reorgs rewind the cursor to the fork point and fire onReorg.
  • Fold running balances in a sink's onRollback, not there: Sinks.
  • finalizedOnly holds delivery to rows at or below tip.finalized_height.
  • fromHeight: 0 backfills as far as your instance holds (archive restore is metered past that).
  • Full options and the batch envelope: Index.

Verifying responses

  • createStreamsClient({ apiKey }) verifies both REST reads (X-Signature) and SSE frames.
  • The key is fetched once from /public/streams/signing-key; a rotated X-Signature-KeyId triggers a single refresh.
  • Blocks, transactions, and subgraph rows: Verify.
verifyBehavior
default (lenient)Verify signed responses; pass unsigned through; throw on invalid
true (strict)A missing signature throws too
{ publicKey }Pin a known PEM
falseDisable verification

verify lives on createStreamsClient

new SecondLayer() does not accept verify.

Use createStreamsClient when you need it.

Filters

Describe a chain event once and get the exact shape Index, Streams, Webhooks, and Subgraphs each expect.

import { on } from "@secondlayer/stacks/filters";
const sbtcMints = on.ftMint({
assetIdentifier: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
minAmount: 100_000n,
});
await sl.index.events.list(sbtcMints.toIndexParams({ limit: 50 }));
await sl.streams.events.list(sbtcMints.toStreamsParams());
await sl.webhooks.create({ name: "sbtc-mints", url, triggers: [sbtcMints.toChainTrigger()] });

The surfaces disagree, and a spread hides it. Each projection converts explicitly:

DifferenceWhere
Print event is print on Index, print_event on SubgraphsIndex, Subgraphs
contract_call and contract_deploy live on a different Index endpoint than eventsIndex
Amounts are strings: a bigint in a JSON.stringify body throws at runtime, so toChainTrigger() turns 100_000n into "100000"Webhooks
trait scoping existsIndex, Subgraphs, not Streams

Unsupported members don't exist

A filter type a surface doesn't support lacks that method, so the error names the problem. Field gating comes second: trait is absent from toStreamsParams(), and wildcards are Webhooks and Subgraphs only.

on.sbtcDeposit({}).toChainTrigger(); // ok: trigger-only event
on.sbtcDeposit({}).toIndexParams();
✗ Property 'toIndexParams' does not exist

Principals are validated

A contract id where an asset identifier belongs is the most common copy-paste mistake, and it used to return zero rows. The factory now throws at construction, naming the field. The fields are still typed string, so it fails when the filter is built, not as a red squiggle.

on.ftTransfer({ assetIdentifier: "SP…token" });
^Throws at runtime: that's a contract id.
on.ftTransfer({ assetIdentifier: "SP…token::token" }); // ok

Subgraph sources

toSubgraphSource() and fromSubgraphSource() round-trip a source.

The abi literal survives, so event.input stays typed in a handler.

import { fromSubgraphSource } from "@secondlayer/stacks/filters";
const source = fromSubgraphSource(subgraph.sources.swaps);
const same = source.toSubgraphSource(); // the swaps source again, abi literal intact
Edit on GitHub