<!-- source: https://www.secondlayer.tools/docs/sdk -->

# 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](/docs/sdk-reference).

## Install

```bash
bun add @secondlayer/sdk
```

```ts
import { SecondLayer } from "@secondlayer/sdk";

const sl = new SecondLayer({ apiKey: process.env.SECONDLAYER_API_KEY });
```

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

| Host | Key read |
| --- | --- |
| `api.secondlayer.tools` | `SECONDLAYER_API_KEY` |
| anywhere else | `INSTANCE_TOKEN` ([Self-host](/docs/self-host#use-these-docs-on-your-box)) |

`accountKey` is for archive operations only. See [Keys and billing](/docs/authentication).

### 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.

```ts
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:

| Task | Call | Documented in |
| --- | --- | --- |
| Webhook on raw chain events | `sl.webhooks.create({ name, url, triggers })` | [Webhooks](/docs/webhooks#triggers) |
| Re-deliver history | `sl.webhooks.replay(id, { fromBlock, toBlock })` | [Replay](/docs/webhooks#replay) |
| Check a delivery signature | `verifyWebhookSignature`, `verifySecondlayerSignature` | [Receive and verify](/docs/webhooks#verify-the-signature) |
| Prove a transaction is in a block | `verifyTransactionProof`, `fetchRewardSet` | [Verify](/docs/verification#prove-a-transaction) |
| Tail the live firehose | `sl.streams.events.subscribe(options)` | [Streams](/docs/streams#real-time-firehose-sse) |
| Read subgraph tables | `sl.subgraphs.rows`, `sl.subgraphs.typed` | [Subgraphs](/docs/subgraphs#read-your-table) |
| Stream or roll up rows | `subgraph.<table>.subscribe`, `.aggregate` | [Read rows](/docs/subgraphs#read-rows) |
| Let a sink own the transaction | `kyselySink`, `drizzleSink`, `bunSqliteSink` | [Sinks](/docs/sinks) |
| Describe one event for every surface | `on.ftMint(...)`, `.toIndexParams()` | [Filters](#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](https://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`

```ts consumer.ts
await sl.index.contractCalls.consume({
  contractId: "SP...marketplace-v4",
  functionName: "purchase-asset",
  fromCursor: await loadCheckpoint(), // null on first run
  // First run backfills as far as the instance holds.
  fromHeight: 0,
  onBatch: async (calls, ctx) => {
    await commitRowsAndCheckpoint(calls, ctx.cursor);
    // Return the cursor you committed.
    return ctx.cursor;
  },
  // Fires after the cursor rewinds.
  onReorg: async (reorg) => {
    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](/docs/sinks#rollback-is-height-stamped).
- `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](/docs/index#build-your-index-on-it).

### 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](/docs/verification).

| `verify` | Behavior |
| --- | --- |
| default (lenient) | Verify signed responses; pass unsigned through; throw on invalid |
| `true` (strict) | A missing signature throws too |
| `{ publicKey }` | Pin a known PEM |
| `false` | Disable 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.

```ts
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:

| Difference | Where |
| --- | --- |
| Print event is `print` on Index, `print_event` on Subgraphs | Index, Subgraphs |
| `contract_call` and `contract_deploy` live on a different Index endpoint than events | Index |
| Amounts are strings: a `bigint` in a `JSON.stringify` body throws at runtime, so `toChainTrigger()` turns `100_000n` into `"100000"` | Webhooks |
| `trait` scoping exists | Index, 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.

```ts
on.sbtcDeposit({}).toChainTrigger();   // ok: trigger-only event
on.sbtcDeposit({}).toIndexParams();
```

#### 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.

```ts
// Throws at runtime: that's a contract id.
on.ftTransfer({ assetIdentifier: "SP…token" });
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.

```ts
import { fromSubgraphSource } from "@secondlayer/stacks/filters";

const source = fromSubgraphSource(subgraph.sources.swaps);
const same = source.toSubgraphSource(); // the swaps source again, abi literal intact
```

## Notes for agents

- Client: new SecondLayer() reads SECONDLAYER_API_URL; with only SECONDLAYER_API_KEY set, or an sk-sl_ apiKey passed, it uses the hosted API. Subgraph rows: sl.subgraphs.rows(name, table, opts) returns { rows, next_cursor, tip }; typed table client: sl.subgraphs.typed(def).
- The key follows the host: SECONDLAYER_API_KEY on the hosted API, INSTANCE_TOKEN on a self-hosted box. accountKey is only for archive operations.
- Verify webhook signatures with verifyWebhookSignature from @secondlayer/sdk before processing the payload; reject anything that does not validate.
- Transaction proof: fetch '/v1/index/transactions/<tx_id>/proof', run verifyTransactionProof(proof) server-side, then re-check fully trustlessly with fetchRewardSet({ nodeUrl, cycle }) from the user's own node.
- Checkpointed consumer: consume() with rows written inside onBatch, the committed cursor returned, safe resume on restart.

## Setup

Ensure setup once, skipping any step already done:
- Skill: `bunx skills add ryanwaits/secondlayer --skill secondlayer -y`
- CLI: `bun add -g @secondlayer/cli`
- Instance: `secondlayer init` writes `INSTANCE_TOKEN`. Loopback `/v1` reads need no key.
- Hosted/archive: export `SECONDLAYER_API_KEY` (`sk-sl_*`) or buy credits with `secondlayer credits buy` (`balance`, `refill`), then `secondlayer whoami`
