<!-- source: https://secondlayer.tools/docs/api/streams -->

# Streams API

The raw event firehose in chain order, a live tail over server-sent events, and the reorg history. Envelope and cursors are in [REST conventions](/docs/rest-api); most apps want the [Index API](/docs/api/index) instead.

## Firehose

Raw events in chain order, paged by cursor.

### Streams discovery

`GET /v1/streams`

Lists the Streams routes, the event types and filters `/v1/streams/events` accepts, and the cursor format. Served ahead of Streams auth, so it never needs a credential.

#### Returns

One `StreamsDiscovery` object.

#### Responses

- `200` Route and filter catalog

#### Example

```bash
curl https://api.secondlayer.tools/v1/streams \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "routes": [
    {
      "path": "/v1/streams/events",
      "method": "GET",
      "description": "Raw event firehose. Cursor-paginated. Returns events[], next_cursor, tip, reorgs[].",
      "event_types": [
        "stx_transfer",
        "stx_mint",
        "stx_burn",
        "stx_lock",
        "ft_transfer",
        "ft_mint",
        "ft_burn",
        "nft_transfer",
        "nft_mint",
        "nft_burn",
        "print"
      ],
      "filters": [
        {
          "name": "types",
          "type": "event_type[]",
          "description": "Event types to include"
        },
        {
          "name": "contract_id",
          "type": "principal | comma-list"
        },
        {
          "name": "limit",
          "type": "number (max 1000)"
        }
      ],
      "auth": "bearer required, metered per row"
    },
    {
      "path": "/v1/streams/tip",
      "method": "GET",
      "description": "Current chain tip: { block_height, block_hash, burn_block_height, finalized_height, lag_seconds }."
    }
  ],
  "cursor": {
    "format": "<block_height>:<event_index>",
    "semantics": "opaque resume token; pass back unchanged to continue. Equals last event's cursor (inclusive on output, exclusive on input)."
  },
  "reorgs_shape": {
    "detected_at": "ISO 8601",
    "new_canonical_tip": "<block_height>:<event_index>",
    "new_canonical_height": "number",
    "new_canonical_event_index": "number"
  }
}
```

### Raw event firehose

`GET /v1/streams/events`

Canonical chain events in chain order: block height, then transaction position, then event position. Pages are capped at the tip minus a 2-block reorg margin. With no cursor or `from_height`, the read starts 1000 blocks behind the tip. An empty filtered page still returns a `next_cursor` past `to_height`, so a consumer never stalls. Pages at or below `tip.finalized_height` are immutable: they carry an `ETag` and answer `If-None-Match` with 304.

#### Filters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |
| `types` | array of enum | Comma-separated event types to include. Default: all of them. |
| `event_type` | enum | One event type, as an alias for `types`. Mutually exclusive with `types`; a set must use `types`. Values: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. |
| `not_types` | array of enum | Comma-separated event types to exclude, applied after `types`. |
| `contract_id` | string | Contract principal, or a comma-separated set. Matches print events by the emitting contract and ft/nft events by the asset's contract. Other event types never match. |
| `sender` | string | Principal, or a comma-separated set, matched against the payload's `sender`. Event types without a sender never match. |
| `recipient` | string | Principal, or a comma-separated set, matched against the payload's `recipient`. |
| `asset_identifier` | string | Exact `<contract>::<asset>` match on the payload. |
| `filters` | string | JSON object of up to 8 labelled filter groups: `{ "<label>": { types?, contractId?, sender?, recipient?, assetIdentifier? } }`. Each value is a string or array of strings. Groups OR together, fields inside a group AND, and the flat filters above still apply to the whole scan. Each event lists the labels it matched in `matched`. Labels are letters, digits, `-` and `_`, 32 characters at most. |
| `rune` | string | chain=bitcoin only. A RuneRef: an id (`840000:3`) or a name, spacers/case ignored (`DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon`, `doggotothemoon`). |
| `address` | string | chain=bitcoin only. A mainnet address. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Events per page. Default 100. Values above 1000 are clamped to 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes strictly after it. Mutually exclusive with `from_cursor` and `from_height`. |
| `from_cursor` | string | Same as `cursor`. `0:0` starts at genesis. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts 1000 blocks behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. Always clamped to the tip minus a 2-block reorg margin, which is also the default. |

#### Returns

A cursor envelope; `events` holds `StreamsEvent` objects.

#### Responses

- `200` A page of events, the cursor to resume from, the tip it was read against, and any reorgs that touched it
- `304` Finalized page unchanged since the `ETag` sent in `If-None-Match`
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds
- `503` No canonical block indexed yet (`CHAIN_DATA_UNAVAILABLE`). Wait for the indexer to ingest its first block

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/streams/events \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token \
  -d asset_identifier=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex \
  -d filters={"alex":{"types":["ft_transfer"],"contractId":"SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token"},"stx":{"types":"stx_transfer"}}
```

```json
{
  "events": [
    {
      "cursor": "9048876:3",
      "block_height": 9048876,
      "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
      "burn_block_height": 968284,
      "tx_id": "0xb9edea3443a9adb603d856bfed2d1e630146bed128b07aaaa259dc97e3eae2ba",
      "tx_index": 2,
      "event_index": 3,
      "event_type": "ft_transfer",
      "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token",
      "payload": {
        "amount": "416515709472",
        "sender": "SP3R4NKXMGW6YXA44X2ESZPKJNV25X4ZN7DPW0RXR",
        "recipient": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.alex-vault",
        "asset_identifier": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex"
      },
      "ts": "2026-09-23T14:56:36.000Z",
      "canonical": true,
      "finalized": false
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Live tail

The same events over server-sent events.

### Tail the event firehose over SSE

`GET /v1/streams/events/stream`

Server-sent events over the same read as `/v1/streams/events`, with the same parameters. The server polls every 1.5 seconds by default (`STREAMS_SSE_POLL_MS`) and writes one frame per event: `id` is the event's cursor and `data` is `{ "event": <StreamsEvent> }`, or `{ "event", "sig", "key_id" }` with an ed25519 signature over the event's JSON when the instance has a signing key. After 20 seconds with no events it writes `event: ping` with empty data; ignore it. With no `cursor`, `from_cursor` or `from_height`, the tail starts at the current tip minus the reorg margin. `Last-Event-ID` is not read; to resume after a disconnect, reconnect with `from_cursor` set to the last `id` you processed.

#### Filters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |
| `types` | array of enum | Comma-separated event types to include. Default: all of them. |
| `event_type` | enum | One event type, as an alias for `types`. Mutually exclusive with `types`; a set must use `types`. Values: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. |
| `not_types` | array of enum | Comma-separated event types to exclude, applied after `types`. |
| `contract_id` | string | Contract principal, or a comma-separated set. Matches print events by the emitting contract and ft/nft events by the asset's contract. Other event types never match. |
| `sender` | string | Principal, or a comma-separated set, matched against the payload's `sender`. Event types without a sender never match. |
| `recipient` | string | Principal, or a comma-separated set, matched against the payload's `recipient`. |
| `asset_identifier` | string | Exact `<contract>::<asset>` match on the payload. |
| `filters` | string | JSON object of up to 8 labelled filter groups: `{ "<label>": { types?, contractId?, sender?, recipient?, assetIdentifier? } }`. Each value is a string or array of strings. Groups OR together, fields inside a group AND, and the flat filters above still apply to the whole scan. Each event lists the labels it matched in `matched`. Labels are letters, digits, `-` and `_`, 32 characters at most. |
| `rune` | string | chain=bitcoin only. A RuneRef: an id (`840000:3`) or a name, spacers/case ignored (`DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon`, `doggotothemoon`). |
| `address` | string | chain=bitcoin only. A mainnet address. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Most events written per poll. Default 100. Values above 1000 are clamped to 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes strictly after it. Mutually exclusive with `from_cursor` and `from_height`. |
| `from_cursor` | string | Same as `cursor`. `0:0` starts at genesis. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts 1000 blocks behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. Always clamped to the tip minus a 2-block reorg margin, which is also the default. |

#### Responses

- `200` An open `text/event-stream`. Frames: `id: <cursor>` plus `data: {"event":{...}}` per event, and `event: ping` after 20 seconds idle
- `400` An unknown query parameter. Other bad values close the stream without a frame
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/streams/events/stream \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token \
  -d asset_identifier=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex \
  -d filters={"alex":{"types":["ft_transfer"],"contractId":"SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token"},"stx":{"types":"stx_transfer"}}
```

## Reorgs and tip

Which blocks were replaced, and where the chain ends now.

### Chain reorg history

`GET /v1/streams/reorgs`

Reorgs the indexer recorded, oldest first. Page forward by passing `next_since` back as `since`.

#### Filters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |
| `since` | string, required | Where to start. An ISO timestamp returns reorgs detected after it. A `<block_height>:<event_index>` cursor returns reorgs whose orphaned range ends at or after it. A `next_since` value (`<detected_at>~<id>`) resumes after the last reorg delivered. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Reorgs per page. Default 100. Values above 1000 are clamped to 1000. |

#### Returns

A cursor envelope; `reorgs` holds `Reorg` objects.

#### Responses

- `200` Reorgs, oldest first
- `400` `since` is missing or is not a timestamp, cursor or `next_since` value, or `limit` is not a positive integer
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/streams/reorgs \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d since=2026-09-01T00:00:00Z
```

```json
{
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129468Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ],
  "next_since": "2026-09-15T15:39:38.129468Z~3f16c2c5-9551-4ae9-adb0-923668e5c65e"
}
```

### Canonical block by height

`GET /v1/streams/canonical/{height}`

The canonical Stacks block at one height and the Bitcoin block it anchors to. Finalized heights are cached for good and answer `If-None-Match` with 304; the `ETag` is the block hash.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `height` | integer, required | Block height (Stacks by default, Bitcoin with chain=bitcoin). |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |

#### Returns

One `StreamsCanonicalBlock` object.

#### Responses

- `200` The canonical block. chain=bitcoin returns a StreamsBitcoinCanonicalBlock (btc block hash, no burn-chain fields) instead.
- `304` Block unchanged since the `ETag` sent in `If-None-Match`
- `400` `height` is not a non-negative integer
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No canonical block at that height yet
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds

#### Example

```bash
curl https://api.secondlayer.tools/v1/streams/canonical/9048876 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "block_height": 9048876,
  "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
  "burn_block_height": 968284,
  "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
  "is_canonical": true
}
```

### Current chain tip

`GET /v1/streams/tip`

The newest canonical block this instance has indexed, the finality boundary, and how far back this caller can seek. Never cached.

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |

#### Returns

One `StreamsTip` object.

#### Responses

- `200` The tip. chain=bitcoin returns a StreamsBitcoinTip (no burn-chain or retention fields) instead.
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds
- `503` No canonical block indexed yet (`CHAIN_DATA_UNAVAILABLE`). Wait for the indexer to ingest its first block

#### Example

```bash
curl https://api.secondlayer.tools/v1/streams/tip \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "block_height": 9048928,
  "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
  "burn_block_height": 968284,
  "finalized_height": 9048671,
  "lag_seconds": 4,
  "oldest_seekable_height": null,
  "oldest_cursor": null
}
```

## Objects

### The Streams discovery object

Catalog of the Streams routes and filters.

| Field | Type | Description |
| --- | --- | --- |
| `routes` | array of object | Each route with its method, a one-line description, and for the event feed its event types and filters. |
| `cursor` | object | Cursor `format` and resume `semantics`. |
| `reorgs_shape` | object | Field names and types of a reorg record. |

```json
{
  "routes": [
    {
      "path": "/v1/streams/events",
      "method": "GET",
      "description": "Raw event firehose. Cursor-paginated. Returns events[], next_cursor, tip, reorgs[].",
      "event_types": [
        "stx_transfer",
        "stx_mint",
        "stx_burn",
        "stx_lock",
        "ft_transfer",
        "ft_mint",
        "ft_burn",
        "nft_transfer",
        "nft_mint",
        "nft_burn",
        "print"
      ],
      "filters": [
        {
          "name": "types",
          "type": "event_type[]",
          "description": "Event types to include"
        },
        {
          "name": "contract_id",
          "type": "principal | comma-list"
        },
        {
          "name": "limit",
          "type": "number (max 1000)"
        }
      ],
      "auth": "bearer required, metered per row"
    },
    {
      "path": "/v1/streams/tip",
      "method": "GET",
      "description": "Current chain tip: { block_height, block_hash, burn_block_height, finalized_height, lag_seconds }."
    }
  ],
  "cursor": {
    "format": "<block_height>:<event_index>",
    "semantics": "opaque resume token; pass back unchanged to continue. Equals last event's cursor (inclusive on output, exclusive on input)."
  },
  "reorgs_shape": {
    "detected_at": "ISO 8601",
    "new_canonical_tip": "<block_height>:<event_index>",
    "new_canonical_height": "number",
    "new_canonical_event_index": "number"
  }
}
```

### The Streams event object

One raw chain event. `payload` is the node's event body; its fields depend on `event_type`.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor` or `from_cursor`. |
| `block_height` | integer | Stacks block the event landed in. |
| `block_hash` | string | That block's hash. |
| `burn_block_height` | integer | Bitcoin block that block anchors to. |
| `tx_id` | string | Transaction that emitted the event. |
| `tx_index` | integer | Its position in the block. |
| `event_index` | integer | The event's position among the block's Streams events, from 0. |
| `event_type` | enum | The event's type. Values: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. |
| `contract_id` | string · nullable | Emitting contract for `print`, asset contract for ft/nft events, `null` otherwise. |
| `payload` | object | The event body. Prints carry `topic`, `value` and `contract_id`; transfers carry `sender`, `recipient`, `amount` or `value`, and `asset_identifier`. |
| `ts` | string · date-time | The block's timestamp. |
| `canonical` | boolean | Always `true`: the firehose serves the canonical chain only. |
| `matched` | array of string | Labels from `filters` this event satisfied. Present only when the request used `filters`. |
| `finalized` | boolean | `true` when the block is at or below `tip.finalized_height` and can no longer reorg. |

```json
{
  "cursor": "9048876:3",
  "block_height": 9048876,
  "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
  "burn_block_height": 968284,
  "tx_id": "0xb9edea3443a9adb603d856bfed2d1e630146bed128b07aaaa259dc97e3eae2ba",
  "tx_index": 2,
  "event_index": 3,
  "event_type": "ft_transfer",
  "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token",
  "payload": {
    "amount": "416515709472",
    "sender": "SP3R4NKXMGW6YXA44X2ESZPKJNV25X4ZN7DPW0RXR",
    "recipient": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.alex-vault",
    "asset_identifier": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex"
  },
  "ts": "2026-09-23T14:56:36.000Z",
  "canonical": true,
  "finalized": false
}
```

### The Reorg object

A fork the indexer rolled back. Rows inside `orphaned_range` were replaced; undo anything you committed from them.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Reorg id. |
| `detected_at` | string · date-time | When the indexer saw the fork. |
| `fork_point_height` | integer | First block height the new fork replaced. Undo rows at or above it. |
| `old_index_block_hash` | string · nullable | Index block hash of the abandoned tip. |
| `new_index_block_hash` | string · nullable | Index block hash of the winning tip. |
| `orphaned_range` | object | First and last cursor of the rows that were replaced. |
| `new_canonical_tip` | string | Cursor of the new canonical tip. |

```json
{
  "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
  "detected_at": "2026-09-15T15:39:38.129Z",
  "fork_point_height": 8996511,
  "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
  "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
  "orphaned_range": {
    "from": "8996511:0",
    "to": "8996512:2701"
  },
  "new_canonical_tip": "8996511:0"
}
```

### The Streams canonical block object

The canonical Stacks block at a height.

| Field | Type | Description |
| --- | --- | --- |
| `block_height` | integer | Stacks block height. |
| `block_hash` | string | Stacks block hash. |
| `burn_block_height` | integer | Bitcoin block it anchors to. |
| `burn_block_hash` | string · nullable | That Bitcoin block's hash. |
| `is_canonical` | boolean | Always `true`. |

```json
{
  "block_height": 9048876,
  "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
  "burn_block_height": 968284,
  "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
  "is_canonical": true
}
```

### The Streams tip object

The newest canonical block and how far back this caller can read.

| Field | Type | Description |
| --- | --- | --- |
| `block_height` | integer | Highest canonical Stacks block this instance has indexed. |
| `block_hash` | string | That block's hash. |
| `burn_block_height` | integer | The Bitcoin block it anchors to. |
| `finalized_height` | integer | Highest Stacks block whose Bitcoin anchor has 6 confirmations. Rows at or below it are immutable. |
| `lag_seconds` | integer | Seconds since the tip block was produced. |
| `oldest_seekable_height` | integer · nullable | Always `null` — every account reads full history. Rows past the monthly allowance are a paid read, not a blocked one. |
| `oldest_cursor` | string · nullable | Always `null` — see `oldest_seekable_height`. |

```json
{
  "block_height": 9048928,
  "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
  "burn_block_height": 968284,
  "finalized_height": 9048671,
  "lag_seconds": 4,
  "oldest_seekable_height": null,
  "oldest_cursor": null
}
```

## Notes for agents

- Firehose: 'GET $SECONDLAYER_API_URL/v1/streams/events' with 'Authorization: Bearer $SECONDLAYER_API_KEY'; filter by types, contract_id, sender; page forward with next_cursor.
- Live tail: 'GET /v1/streams/events/stream' is server-sent events; resume with the last cursor you handled.
- Undo rows named in reorgs[] from the fork point; 'GET /v1/streams/tip' is where the chain ends now.
- One endpoint as markdown: 'https://secondlayer.tools/docs/api-reference/<anchor>.md', e.g. list-stream-events.

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