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

# Subgraphs API

Read your subgraph's tables, describe them, and deploy and operate them. Reading is under `/v1/subgraphs`; deploying and operating is the write plane under `/api/subgraphs` on a self-hosted instance. Envelope and filters are in [REST conventions](/docs/rest-api).

## Read rows

Query, count, aggregate and stream a table.

### List a table's rows

`GET /v1/subgraphs/{name}/{table}`

Rows from one subgraph table, keyset-paginated. By default pages walk `_id`; `_sort` walks one column instead, with `_id` as the tiebreaker. `next_cursor` is set on every page that returned rows and is `null` only on an empty page: pass it back as `cursor` to continue, or tail new rows with `cursor = next_cursor ?? cursor`. `_offset` is refused (400): deep OFFSET scans hurt on big tables. `_limit` defaults to 50.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

#### Filters

| Name | Type | Description |
| --- | --- | --- |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `_limit` | integer · 1 to 1000 | Page size, 1 to 1000. A non-integer, 0, a negative, or anything over 1000 is 400, not clamped. |
| `cursor` | string | The previous page's `next_cursor`, verbatim. Opaque: its shape depends on `_sort`, so never build one. A cursor issued under one `_sort`/`_order` is 400 under another. |
| `_sort` | string | One column to sort by. A comma list and `jsonb` columns are 400. Omit to page by `_id`. |
| `_order` | enum | Direction of the `_sort` column, or of `_id` without `_sort`. Anything but `asc` or `desc` is 400. Values: `asc`, `desc`. |
| `_fields` | string | Comma-separated columns to return. An unknown name is 400 `INVALID_COLUMN`. The cursor still works: `_id` and the `_sort` column are read for it and dropped from rows you did not ask them for. |

#### Returns

One `SubgraphRowsPage` object.

#### Responses

- `200` One page of rows
- `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
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d filters=[object Object]
```

```json
{
  "rows": [
    {
      "_id": "48138",
      "_block_height": "9048926",
      "_tx_id": "0x926f5ff4326155dea077126d21810fc7954568647898dbfb2f66a28b880dc5c9",
      "_created_at": "2026-09-23T15:10:29.114Z",
      "sender": "SP1TPYSSG52FKCDHHN8Y76ABMA12P069BZBFAYEAF",
      "recipient": "SP3EWCDA3V8HCP64CSETSNYXZ25WC4AJ95EC0ZEST.zr-d-0",
      "amount": "14000",
      "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  ],
  "next_cursor": "48138",
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  }
}
```

### Count a table's rows

`GET /v1/subgraphs/{name}/{table}/count`

How many rows match the filters. Takes the same column filters and `_search` as the row list.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `_count` | enum | `exact` runs COUNT(*). `estimate` reads the planner's row estimate instead, with no table scan, but only when there are no filters and no `_search`; otherwise it counts exactly. The estimate can be -1 before the table's first ANALYZE. Values: `exact`, `estimate`. |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

#### Returns

One `SubgraphRowCount` object.

#### Responses

- `200` The count
- `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
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/count \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d filters=[object Object]
```

```json
{
  "count": 48138
}
```

### Aggregate a table's rows

`GET /v1/subgraphs/{name}/{table}/aggregate`

Scalar aggregates over the rows that match the filters: count, distinct counts, and sum, min and max of numeric columns. With no aggregate param it returns `{ "count": n }`. Up to 32 aggregate columns per request. Takes the same column filters and `_search` as the row list.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `_count` | string | Include `count`. Any value but `false` turns it on. `count` is also returned when no other aggregate is asked for. |
| `_countDistinct` | string | Comma-separated columns to count distinct values of. Any column. |
| `_sum` | string | Comma-separated columns to sum. Numeric only: `uint` or `int` columns, or `_block_height`; others are 400 `NON_NUMERIC_COLUMN`. |
| `_min` | string | Comma-separated numeric columns to take the minimum of. |
| `_max` | string | Comma-separated numeric columns to take the maximum of. |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

#### Returns

One `SubgraphAggregates` object.

#### Responses

- `200` The aggregates
- `400` A column or filter was refused: `INVALID_COLUMN`, `NON_NUMERIC_COLUMN`, `TOO_MANY_AGGREGATES`, or `VALIDATION_ERROR`
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/aggregate \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d filters=[object Object]
```

```json
{
  "count": 3,
  "countDistinct": {
    "sender": 3
  },
  "sum": {
    "amount": "42000"
  },
  "max": {
    "amount": "14000"
  }
}
```

### Stream a table's new rows over SSE

`GET /v1/subgraphs/{name}/{table}/stream`

Server-Sent Events of rows as they are indexed. Starts at the table's newest row and sends only rows after it; `since` replays from a block height first, then keeps tailing. The server polls every 1.5 seconds, sends each matching row in `_id` order, and sends a `ping` event after 20 idle seconds. Column filters and `_search` apply; `_limit`, `_sort`, `_order` and `_fields` are checked but have no effect, and rows always come back whole. Nothing is stored between connections, and `Last-Event-ID` is not read: to resume, reconnect with `since`.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `since` | integer | Block height to replay from: rows with `_block_height` at or above it, then the live tail. A value that is not a number is ignored. With nothing at or above it yet, the stream tails live. |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

#### Responses

- `200` An open event stream. Each row arrives as one message: `id` is the row's `_id`, `data` is the row as JSON, in the same shape as the row list. Idle keepalives are `event: ping` with empty data.
- `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
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/stream \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d since=9048900 \
  -d filters=[object Object]
```

### Get one row by id

`GET /v1/subgraphs/{name}/{table}/{id}`

One row by its `_id`, wrapped as `{ data }`. An id that isn't an integer, or no row with it, is a 404.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |
| `id` | string, required | The row's `_id`. |

#### Responses

- `200` The row.
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph, table, or row with that name or id (`SUBGRAPH_NOT_FOUND`, `TABLE_NOT_FOUND`, `ROW_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/48138 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": {
    "_id": "48138",
    "_block_height": "9048926",
    "_tx_id": "0x926f5ff4326155dea077126d21810fc7954568647898dbfb2f66a28b880dc5c9",
    "_created_at": "2026-09-23T15:10:29.114Z",
    "sender": "SP1TPYSSG52FKCDHHN8Y76ABMA12P069BZBFAYEAF",
    "recipient": "SP3EWCDA3V8HCP64CSETSNYXZ25WC4AJ95EC0ZEST.zr-d-0",
    "amount": "14000",
    "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
  }
}
```

## Describe a subgraph

List subgraphs and read their schema and docs.

### List the subgraphs this instance serves

`GET /v1/subgraphs`

Every subgraph deployed on this instance, with its tables, sync position, and approximate row count. Hosted: your account's subgraphs, then every public one. The anonymous list is cached for 30 seconds and carries an `ETag`; send `If-None-Match` for a 304.

#### Returns

One `SubgraphDirectory` object.

#### Responses

- `200` The subgraph directory
- `304` Anonymous request whose `If-None-Match` matches the current `ETag`; no body
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

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

```json
{
  "subgraphs": [
    {
      "name": "sbtc-token",
      "description": null,
      "status": "active",
      "version": "1.0.0",
      "created_at": "2026-09-01T12:00:00.000Z",
      "total_rows": 48138,
      "sources": [
        "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
      ],
      "last_processed_block": 9048931,
      "blocks_behind": 0,
      "tables": [
        "transfers"
      ],
      "url": "/v1/subgraphs/sbtc-token"
    }
  ],
  "tip": {
    "block_height": 9048931
  },
  "envelope": {
    "rows": "GET /v1/subgraphs/:name/:table → { rows, next_cursor, tip }",
    "cursor": "_id keyset by default, or ?_sort=<column>&_order=asc|desc for a composite keyset; pass ?cursor=<next_cursor> to resume"
  }
}
```

### Get one subgraph's tables, columns, and sync tip

`GET /v1/subgraphs/{name}`

Metadata for one subgraph: each table's read endpoint and declared columns, how far indexing has reached, and links to its generated docs. `columns` lists only the definition's columns; the system columns `_id`, `_block_height`, `_tx_id` and `_created_at` exist on every table too.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

#### Returns

One `Subgraph` object.

#### Responses

- `200` Subgraph metadata
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "name": "sbtc-token",
  "description": null,
  "version": "1.0.0",
  "status": "active",
  "created_at": "2026-09-01T12:00:00.000Z",
  "sources": [
    "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
  ],
  "start_block": 0,
  "tables": {
    "transfers": {
      "endpoint": "/v1/subgraphs/sbtc-token/transfers",
      "columns": [
        "sender",
        "recipient",
        "amount",
        "asset_identifier"
      ],
      "column_types": {
        "sender": "principal",
        "recipient": "principal",
        "amount": "uint",
        "asset_identifier": "text"
      }
    }
  },
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  },
  "docs": {
    "openapi": "/v1/subgraphs/sbtc-token/openapi.json",
    "schema": "/v1/subgraphs/sbtc-token/schema.json",
    "markdown": "/v1/subgraphs/sbtc-token/docs.md"
  }
}
```

### Get a subgraph's OpenAPI document

`GET /v1/subgraphs/{name}/openapi.json`

An OpenAPI 3.1 document generated from the subgraph's definition: one list path and one count path per table, a row schema per table, and the column filters each table accepts. Feed it to a client generator.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `server` | string | Base URL written into the document's endpoints and examples. Defaults to this request's origin, honoring `X-Forwarded-Proto`. |

#### Returns

One `SubgraphOpenApiDocument` object.

#### Responses

- `200` The generated OpenAPI document
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/openapi.json \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "sbtc-token Subgraph API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://127.0.0.1:3800"
    }
  ],
  "paths": {
    "/v1/subgraphs/sbtc-token/transfers": {
      "get": {
        "summary": "Query sbtc-token.transfers",
        "operationId": "query_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rows returned from the subgraph table.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/transfersRow"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tip": {
                      "type": "object",
                      "properties": {
                        "block_height": {
                          "type": "integer"
                        },
                        "subgraph_height": {
                          "type": "integer"
                        },
                        "blocks_behind": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/subgraphs/sbtc-token/transfers/count": {
      "get": {
        "summary": "Count sbtc-token.transfers",
        "operationId": "count_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Row count for the filtered table query.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "count"
                  ],
                  "example": {
                    "count": 48138
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "transfersRow": {
        "type": "object",
        "properties": {
          "sender": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "asset_identifier": {
            "type": "string"
          },
          "_id": {
            "type": "integer"
          },
          "_block_height": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "_tx_id": {
            "type": "string"
          },
          "_created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "example": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        }
      }
    }
  },
  "x-secondlayer-subgraph": "sbtc-token",
  "x-secondlayer-version": "1.0.0",
  "x-secondlayer-schema-hash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "x-secondlayer-generated-at": "2026-09-23T15:12:00.000Z",
  "x-secondlayer-sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "x-secondlayer-tables": [
    "transfers"
  ]
}
```

### Get a subgraph's agent schema

`GET /v1/subgraphs/{name}/schema.json`

A compact JSON description of the subgraph for agents and tools: per table, its endpoints, exact row count, columns, the query params and filters it accepts, and example requests.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `server` | string | Base URL written into the document's endpoints and examples. Defaults to this request's origin, honoring `X-Forwarded-Proto`. |

#### Returns

One `SubgraphAgentSchema` object.

#### Responses

- `200` The generated agent schema
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/schema.json \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "name": "sbtc-token",
  "version": "1.0.0",
  "schemaHash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "generatedAt": "2026-09-23T15:12:00.000Z",
  "serverUrl": "http://127.0.0.1:3800",
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "tables": {
    "transfers": {
      "endpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers",
      "countEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/count",
      "aggregateEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/aggregate",
      "streamEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/stream",
      "rowCount": 48138,
      "columns": {
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "amount": {
          "type": "uint"
        },
        "asset_identifier": {
          "type": "text",
          "indexed": true
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "query": {
        "parameters": [
          "_limit",
          "cursor",
          "_order",
          "_fields"
        ],
        "sortable": [],
        "selectable": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "searchable": [],
        "filters": [
          "sender",
          "sender.neq",
          "sender.like",
          "recipient",
          "recipient.neq",
          "recipient.like",
          "amount",
          "amount.neq",
          "amount.gt",
          "amount.gte",
          "amount.lt",
          "amount.lte",
          "asset_identifier",
          "asset_identifier.neq",
          "asset_identifier.like",
          "_id",
          "_id.neq",
          "_id.gt",
          "_id.gte",
          "_id.lt",
          "_id.lte",
          "_block_height",
          "_block_height.neq",
          "_block_height.gt",
          "_block_height.gte",
          "_block_height.lt",
          "_block_height.lte",
          "_tx_id",
          "_tx_id.neq",
          "_tx_id.like",
          "_created_at",
          "_created_at.neq",
          "_created_at.gt",
          "_created_at.gte",
          "_created_at.lt",
          "_created_at.lte",
          "_created_at.like"
        ]
      },
      "examples": {
        "list": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        },
        "count": {
          "count": 48138
        },
        "curl": "curl 'http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers?_limit=10&_order=desc'"
      }
    }
  }
}
```

### Get a subgraph's markdown docs

`GET /v1/subgraphs/{name}/docs.md`

The agent schema rendered as markdown: a section per table with its endpoints, row count, column table, query params, filters, and a curl example. Served as `text/markdown`.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `server` | string | Base URL written into the document's endpoints and examples. Defaults to this request's origin, honoring `X-Forwarded-Proto`. |

#### Responses

- `200` The generated markdown
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

#### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/docs.md \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

## Deploy and operate

Deploy, reindex, backfill and inspect a subgraph.

### List deployed subgraphs

`GET /api/subgraphs` (self-hosted instances only)

Every subgraph on this instance with its status, sync progress, row count and open gap count. Row counts come from Postgres table statistics, so they are approximate; the detail read counts exactly.

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `health` | integer | Also return each subgraph's hourly health for this many hours, the same shape as the health read. 1 to 168. |

#### Returns

A cursor envelope; `data` holds `DeployedSubgraph` objects.

#### Responses

- `200` Every deployed subgraph, unpaginated
- `401` This bind requires the instance token, and it was missing or wrong

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "data": [
    {
      "name": "sbtc-flows",
      "version": "1.0.2",
      "status": "active",
      "lastProcessedBlock": 9048712,
      "totalProcessed": 48210,
      "totalRows": 48138,
      "totalErrors": 0,
      "tables": [
        "transfers"
      ],
      "chainTip": 9048712,
      "sourceChainTip": 9048712,
      "targetBlock": 9048712,
      "progress": 1,
      "blocksRemaining": 0,
      "syncMode": "sync",
      "gapCount": 0,
      "integrity": "complete",
      "lastError": null,
      "lastErrorAt": null,
      "updatedAt": "2026-09-23T14:02:11.000Z",
      "createdAt": "2026-09-21T09:14:52.000Z"
    }
  ]
}
```

### Deploy or redeploy a subgraph

`POST /api/subgraphs` (self-hosted instances only)

Takes bundled handler code (see `/api/subgraphs/bundle`) plus the schema and sources extracted from it. The server reads the definition from the bundle without running it, validates it, lints print fields, then applies the schema. A new subgraph, or a breaking schema change, queues a reindex and returns its `operationId`. Redeploying the same name updates in place; `action` says what happened. With `dryRun` it returns the DDL plan and writes nothing.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name. Lowercase letters, digits and hyphens. Deploying an existing name updates it. |
| `sources` | object, required | Event sources keyed by name, as read from the handler module. At least one. |
| `schema` | object, required | Table definitions keyed by table name. |
| `handlerCode` | string, required | The bundled handler module. 1 MB max. |
| `startBlock` | integer | Overrides the definition's `startBlock` for this deploy and forces a reindex from it. Refused past the chain tip (`START_BLOCK_PAST_TIP`). |
| `description` | string | One line on what the subgraph indexes. |
| `sourceCode` | string | The unbundled TypeScript source, kept so `secondlayer subgraphs source` can return it. 1 MB max. |
| `dryRun` | boolean | Validate and return the DDL plan without writing anything. |

#### Responses

- `200` Redeployed in place (`action` is anything but `created`), or the dry-run plan when `dryRun` is set
- `201` Created. A reindex from `start_block` is queued; poll `operationId`
- `400` The body failed validation (here `error` is an object of field errors, not a string), the bundle could not be read (`HANDLER_IMPORT_FAILED`), the definition is invalid, or `startBlock` is past the chain tip (`START_BLOCK_PAST_TIP`)
- `401` This bind requires the instance token, and it was missing or wrong
- `409` A reindex or backfill is already queued or running for this subgraph (`OPERATION_IN_PROGRESS`). Wait for it, or stop it first.
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`
- `422` The definition deploys but would index wrong: a declared print field is never seen on-chain (`PRINT_FIELD_MISMATCH`), handlers write 0 rows against observed prints (`EMPTY_MAPPING`), a tip-first deploy has delta handlers (`TIP_FIRST_NON_REPLAYABLE_HANDLER`) or a breaking schema change (`TIP_FIRST_BREAKING_CHANGE`), or a subgraph with provable sources has a nondeterministic handler (`NONDETERMINISTIC_HANDLER`, with `verification.reasons` naming each use)

#### Example

```bash
curl -X POST http://127.0.0.1:3800/api/subgraphs \
  -H "Authorization: Bearer $INSTANCE_TOKEN" \
  --json '{
  "name": "sbtc-flows",
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "schema": {
    "transfers": {
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        }
      }
    }
  },
  "handlerCode": "// secondlayer-stub:@secondlayer/subgraphs\nfunction defineSubgraph(def) {\n  return def;\n}\n\n// <stdin>\nvar stdin_default = defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8e6,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\"\n    }\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" }\n      }\n    }\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    }\n  }\n});\nexport {\n  stdin_default as default\n};\n",
  "sourceCode": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n"
}'
```

```json
{
  "action": "handler_updated",
  "subgraphId": "b7e2d4a1-5c3f-4e8b-a0d9-2f6c1e8b7a34",
  "version": "1.0.3",
  "start_block": 8000000,
  "message": "Subgraph \"sbtc-flows\" handler_updated",
  "warnings": [
    "handler changed but schema did not: new logic applies from the current tip only. Rows already indexed were computed by the previous handler. To recompute history, redeploy with --reindex (or backfill a range)."
  ]
}
```

### Bundle subgraph source into deployable handler code

`POST /api/subgraphs/bundle` (self-hosted instances only)

Compiles a TypeScript subgraph module on the server and returns the bundle plus the metadata read from it, ready to POST to `/api/subgraphs`. Deploys nothing. The CLI and MCP bundle locally; this is for callers that cannot run esbuild.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `code` | string, required | The subgraph module's TypeScript source, with `defineSubgraph` as the default export. |

#### Returns

One `SubgraphBundle` object.

#### Responses

- `200` Bundled
- `400` `code` is missing or empty, or the module did not compile (`BUNDLE_FAILED`)
- `401` This bind requires the instance token, and it was missing or wrong
- `413` The bundle exceeds the size limit (`BUNDLE_TOO_LARGE`); `actualBytes` and `maxBytes` say by how much
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

#### Example

```bash
curl -X POST http://127.0.0.1:3800/api/subgraphs/bundle \
  -H "Authorization: Bearer $INSTANCE_TOKEN" \
  --json '{
  "code": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n"
}'
```

```json
{
  "ok": true,
  "name": "sbtc-flows",
  "description": null,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "schema": {
    "transfers": {
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        }
      }
    }
  },
  "handlerCode": "// secondlayer-stub:@secondlayer/subgraphs\nfunction defineSubgraph(def) {\n  return def;\n}\n\n// <stdin>\nvar stdin_default = defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8e6,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\"\n    }\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" }\n      }\n    }\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    }\n  }\n});\nexport {\n  stdin_default as default\n};\n",
  "sourceCode": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n",
  "bundleSize": 724
}
```

### Get a deployed subgraph

`GET /api/subgraphs/{name}` (self-hosted instances only)

Definition, health, sync progress and per-table columns and exact row counts. While a reindex or backfill is active, `sync` carries its queue position, event progress and ETA.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Returns

One `DeployedSubgraphDetail` object.

#### Responses

- `200` The subgraph
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name} \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "name": "sbtc-flows",
  "version": "1.0.2",
  "schemaHash": "4b9e1d27c83f0a65e2d7b14c9f06a38e5d21c7b90f4e6a83d15c2b7e09f4a6d1",
  "pin": "7f3c0d5e9b21a4c86f0e3d17b5a92c4e8d61f07a3b9c25e4d8f1a06c7b3e5a91e",
  "verification": {
    "level": "events",
    "reasons": [
      "ft_transfer source \"transfers\" needs event proofs"
    ],
    "unproven": []
  },
  "status": "active",
  "lastProcessedBlock": 9048712,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "definition": {
    "name": "sbtc-flows",
    "startBlock": 8000000,
    "sources": {
      "transfers": {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
      }
    },
    "schema": {
      "transfers": {
        "columns": {
          "amount": {
            "type": "uint"
          },
          "sender": {
            "type": "principal"
          },
          "recipient": {
            "type": "principal"
          }
        }
      }
    }
  },
  "health": {
    "totalProcessed": 48210,
    "totalErrors": 0,
    "errorRate": 0,
    "lastError": null,
    "lastErrorAt": null,
    "emptyMapping": false,
    "violationCount": 0
  },
  "sync": {
    "status": "synced",
    "mode": "sync",
    "startBlock": 8000000,
    "lastProcessedBlock": 9048712,
    "chainTip": 9048712,
    "sourceChainTip": 9048712,
    "targetBlock": 9048712,
    "blocksRemaining": 0,
    "processedBlocks": 1048713,
    "totalBlocks": 1048713,
    "progress": 1,
    "gaps": {
      "count": 0,
      "totalMissingBlocks": 0,
      "ranges": []
    },
    "integrity": "complete"
  },
  "tables": {
    "transfers": {
      "endpoint": "/subgraphs/sbtc-flows/transfers",
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "rowCount": 48138,
      "example": "/subgraphs/sbtc-flows/transfers?_sort=_block_height&_order=desc&_limit=10"
    }
  },
  "createdAt": "2026-09-21T09:14:52.000Z",
  "updatedAt": "2026-09-23T14:02:11.000Z"
}
```

### Delete a subgraph

`DELETE /api/subgraphs/{name}` (self-hosted instances only)

Requests cancellation of any queued or running operation, waits up to 30 seconds for the processor to release it, then drops the Postgres schema and the registry row. Irreversible.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `force` | boolean | Accepted and logged. The delete proceeds after the 30-second wait either way. |

#### Responses

- `200` Deleted
- `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
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

#### Example

```bash
curl -X DELETE http://127.0.0.1:3800/api/subgraphs/{name} \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "message": "Subgraph \"sbtc-flows\" deleted"
}
```

### Queue a full reindex

`POST /api/subgraphs/{name}/reindex` (self-hosted instances only)

Drops the subgraph's tables and rebuilds them from its start block to the chain tip. Takes no body. A `fromBlock`/`toBlock` is refused with 400 `REINDEX_RANGE_NOT_SUPPORTED` rather than ignored, since a ranged walk would destroy everything outside the range; use `backfill` for a range. Poll the returned `operationId`.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Returns

One `SubgraphOperationQueued` object.

#### Responses

- `200` Reindex queued
- `400` The body carried `fromBlock` or `toBlock` (`REINDEX_RANGE_NOT_SUPPORTED`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)
- `409` A reindex or backfill is already queued or running for this subgraph (`OPERATION_IN_PROGRESS`). Wait for it, or stop it first.
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

#### Example

```bash
curl -X POST http://127.0.0.1:3800/api/subgraphs/{name}/reindex \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "message": "Backfill queued for subgraph \"sbtc-flows\"",
  "fromBlock": 8950000,
  "toBlock": 9000000,
  "operationId": "9a4e7c21-0b3d-4f6a-8c15-e2d7b9f0a461",
  "status": "queued"
}
```

### Queue a backfill over a block range

`POST /api/subgraphs/{name}/backfill` (self-hosted instances only)

Re-runs handlers over `fromBlock` to `toBlock`, inclusive, without dropping data. A backfill that overlaps an earlier failed or cancelled one resumes from its last committed block. Refused when the handlers apply deltas (`ctx.increment`, `ctx.update`) or read-modify-write rows (`ctx.findOne`, `ctx.findMany`), which would double-count blocks already processed; reindex those instead.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `fromBlock` | integer, required | First block height to re-run, inclusive. |
| `toBlock` | integer, required | Last block height to re-run, inclusive. |

#### Returns

One `SubgraphOperationQueued` object.

#### Responses

- `200` Backfill queued
- `400` `fromBlock` or `toBlock` is missing, not a number, or 0 (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)
- `409` A reindex or backfill is already queued or running for this subgraph (`OPERATION_IN_PROGRESS`). Wait for it, or stop it first.
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`
- `422` The handlers apply deltas and cannot be replayed (`BACKFILL_NON_REPLAYABLE_HANDLER`)

#### Example

```bash
curl -X POST http://127.0.0.1:3800/api/subgraphs/{name}/backfill \
  -H "Authorization: Bearer $INSTANCE_TOKEN" \
  --json '{
  "fromBlock": 8950000,
  "toBlock": 9000000
}'
```

```json
{
  "message": "Backfill queued for subgraph \"sbtc-flows\"",
  "fromBlock": 8950000,
  "toBlock": 9000000,
  "operationId": "9a4e7c21-0b3d-4f6a-8c15-e2d7b9f0a461",
  "status": "queued"
}
```

### Stop the active reindex or backfill

`POST /api/subgraphs/{name}/stop` (self-hosted instances only)

Takes no body. Flags the queued or running operation for cancellation; the processor stops it at its next checkpoint and marks it `cancelled`. A stopped backfill resumes from its checkpoint when the same range is queued again.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Responses

- `200` Stop requested
- `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
- `404` No subgraph by this name (`SUBGRAPH_NOT_FOUND`), or nothing queued or running to stop (`NO_OPERATION`)
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

#### Example

```bash
curl -X POST http://127.0.0.1:3800/api/subgraphs/{name}/stop \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "message": "Stop requested for \"sbtc-flows\"",
  "operationId": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
  "status": "cancel_requested"
}
```

### List a subgraph's operations

`GET /api/subgraphs/{name}/operations` (self-hosted instances only)

The 20 most recent reindex and backfill operations, newest first, with progress. Queued ones carry an approximate `queuePosition`.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Returns

A cursor envelope; `operations` holds `SubgraphOperation` objects.

#### Responses

- `200` Recent operations
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name}/operations \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "operations": [
    {
      "id": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
      "subgraphName": "sbtc-flows",
      "kind": "reindex",
      "status": "running",
      "weight": "light",
      "fromBlock": 8000000,
      "toBlock": 9048712,
      "processedBlocks": null,
      "cursorBlock": null,
      "estimatedEvents": 48210,
      "processedEvents": 24105,
      "progress": 0.5,
      "error": null,
      "startedAt": "2026-09-21T09:15:04.000Z",
      "finishedAt": null,
      "createdAt": "2026-09-21T09:14:53.000Z",
      "updatedAt": "2026-09-21T09:18:37.000Z"
    }
  ]
}
```

### Get an operation

`GET /api/subgraphs/{name}/operations/{operation_id}` (self-hosted instances only)

One reindex or backfill by id. Poll it after a deploy, reindex or backfill until `status` is `completed`, `failed` or `cancelled`.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |
| `operation_id` | string, required | Operation id (UUID) from a deploy, reindex or backfill response, or from the operations list. |

#### Returns

One `SubgraphOperation` object.

#### Responses

- `200` The operation
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name (`SUBGRAPH_NOT_FOUND`), or no operation with this id on it (`OPERATION_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name}/operations/{operation_id} \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "id": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
  "subgraphName": "sbtc-flows",
  "kind": "reindex",
  "status": "running",
  "weight": "light",
  "fromBlock": 8000000,
  "toBlock": 9048712,
  "processedBlocks": null,
  "cursorBlock": null,
  "estimatedEvents": 48210,
  "processedEvents": 24105,
  "progress": 0.5,
  "error": null,
  "startedAt": "2026-09-21T09:15:04.000Z",
  "finishedAt": null,
  "createdAt": "2026-09-21T09:14:53.000Z",
  "updatedAt": "2026-09-21T09:18:37.000Z"
}
```

### List a subgraph's gaps

`GET /api/subgraphs/{name}/gaps` (self-hosted instances only)

Block ranges the subgraph failed to process, ordered by start height. Open gaps only by default.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Filters

| Name | Type | Description |
| --- | --- | --- |
| `_offset` | integer | Gaps to skip. Default 0. |
| `resolved` | enum | `true` or `all` include resolved gaps with the open ones. Omit for open gaps only. Values: `true`, `all`. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `_limit` | integer | Page size, 1 to 1000. Default 50. |

#### Returns

A cursor envelope; `data` holds `SubgraphGap` objects.

#### Responses

- `200` A page of gaps
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name}/gaps \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "data": [
    {
      "start": 8412207,
      "end": 8412207,
      "size": 1,
      "reason": "processing_error",
      "detectedAt": "2026-09-22T18:40:03.000Z",
      "resolvedAt": null
    }
  ],
  "meta": {
    "total": 1,
    "totalMissingBlocks": 1,
    "limit": 50,
    "offset": 0
  }
}
```

### Get a subgraph's hourly health

`GET /api/subgraphs/{name}/health` (self-hosted instances only)

Hourly buckets from the snapshot the subgraph records every minute: blocks behind the chain tip, rows written, handler errors and events handled. Snapshots are kept 7 days. Oldest hour first; an hour with no snapshots has `lagBlocks: null` and zero counts.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `hours` | integer | Hours to return, 1 to 168. Default 24. |

#### Returns

One `SubgraphHealth` object.

#### Responses

- `200` Hourly health
- `400` `hours` is not an integer from 1 to 168 (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name}/health \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "hours": [
    {
      "hour": "2026-10-16T13:00:00.000Z",
      "lagBlocks": 0,
      "rowsWritten": 1840,
      "errors": 0,
      "eventsHandled": 2213
    }
  ],
  "errors24h": 0
}
```

### List a subgraph's handler errors

`GET /api/subgraphs/{name}/errors` (self-hosted instances only)

Handler errors grouped by message and stack frame, most frequent first. A bug that throws on every event is one group with a count, plus a sample transaction to reproduce it.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer | Groups to return, 1 to 50. Default 20. |

#### Responses

- `200` Error groups
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name}/errors \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "data": [
    {
      "message": "Cannot read properties of undefined (reading 'amount')",
      "frame": "handlers/swap.ts:14:22",
      "eventType": "print",
      "sourceName": "swaps",
      "count": 312,
      "sampleTxId": "0x8d2f6a91c4e07b35d1a8f2e69c0b47d3a5e18f62b9c04d7e3a16f58b2c9d0e41",
      "sampleBlockHeight": 9031477,
      "firstSeen": "2026-10-16T09:02:11.000Z",
      "lastSeen": "2026-10-16T13:41:56.000Z"
    }
  ]
}
```

### List a subgraph's print violations

`GET /api/subgraphs/{name}/violations` (self-hosted instances only)

Print events a handler skipped because the payload did not match the source's declared `prints` schema, newest first. The instance keeps the last 100 per subgraph.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

#### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1 to 100. Default 50. |

#### Returns

A cursor envelope; `data` holds `SubgraphViolation` objects.

#### Responses

- `200` Recent violations
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

#### Example

```bash
curl http://127.0.0.1:3800/api/subgraphs/{name}/violations \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "data": [
    {
      "id": "c41d8e27-6f0a-4b93-a5e2-7d19b3c8f056",
      "sourceName": "swaps",
      "blockHeight": 9031477,
      "txId": "0x8d2f6a91c4e07b35d1a8f2e69c0b47d3a5e18f62b9c04d7e3a16f58b2c9d0e41",
      "reason": "missing declared field \"amount-in\"",
      "samplePayload": {
        "topic": "swap",
        "data": {
          "amount-out": "1500000",
          "sender": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9"
        }
      },
      "seenAt": "2026-09-23T11:27:45.000Z"
    }
  ],
  "meta": {
    "total": 1,
    "limit": 50
  }
}
```

## Objects

### The Subgraph directory object

| Field | Type | Description |
| --- | --- | --- |
| `subgraphs` | array of SubgraphSummary | Every subgraph you can read. Not paginated. |
| `tip` | object | The chain tip this list was read against. |
| `envelope` | object | Fixed reminder of the row-read shape and cursor rules, for agents reading the directory cold. |

```json
{
  "subgraphs": [
    {
      "name": "sbtc-token",
      "description": null,
      "status": "active",
      "version": "1.0.0",
      "created_at": "2026-09-01T12:00:00.000Z",
      "total_rows": 48138,
      "sources": [
        "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
      ],
      "last_processed_block": 9048931,
      "blocks_behind": 0,
      "tables": [
        "transfers"
      ],
      "url": "/v1/subgraphs/sbtc-token"
    }
  ],
  "tip": {
    "block_height": 9048931
  },
  "envelope": {
    "rows": "GET /v1/subgraphs/:name/:table → { rows, next_cursor, tip }",
    "cursor": "_id keyset by default, or ?_sort=<column>&_order=asc|desc for a composite keyset; pass ?cursor=<next_cursor> to resume"
  }
}
```

### The Subgraph object

Metadata for one subgraph.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `description` | string · nullable | The definition's `description`, if it has one. |
| `version` | string | Deployed version. |
| `status` | string | Indexing state: `active` (following the chain), `reindexing`, `paused`, or `error`. |
| `created_at` | string · date-time | First deploy. |
| `sources` | array of string | Contracts the definition reads from: each source's `contractId`, and the contract half of each `assetIdentifier`. |
| `start_block` | integer | Block indexing starts from. 0 means genesis. |
| `tables` | object | Each table by name. |
| `tip` | SubgraphTip |  |
| `docs` | object | Generated docs for this subgraph. |

```json
{
  "name": "sbtc-token",
  "description": null,
  "version": "1.0.0",
  "status": "active",
  "created_at": "2026-09-01T12:00:00.000Z",
  "sources": [
    "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
  ],
  "start_block": 0,
  "tables": {
    "transfers": {
      "endpoint": "/v1/subgraphs/sbtc-token/transfers",
      "columns": [
        "sender",
        "recipient",
        "amount",
        "asset_identifier"
      ],
      "column_types": {
        "sender": "principal",
        "recipient": "principal",
        "amount": "uint",
        "asset_identifier": "text"
      }
    }
  },
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  },
  "docs": {
    "openapi": "/v1/subgraphs/sbtc-token/openapi.json",
    "schema": "/v1/subgraphs/sbtc-token/schema.json",
    "markdown": "/v1/subgraphs/sbtc-token/docs.md"
  }
}
```

### The Subgraph OpenAPI document object

OpenAPI 3.1 document generated from the definition.

| Field | Type | Description |
| --- | --- | --- |
| `openapi` | string | Always `3.1.0`. |
| `info` | object | Title, the subgraph's version, and its description. |
| `servers` | array of object | The `server` param, or this request's origin. |
| `paths` | object | A list path and a count path per table. |
| `components` | object | A `<table>Row` schema per table. |
| `x-secondlayer-subgraph` | string | Subgraph name. |
| `x-secondlayer-version` | string | Deployed version. |
| `x-secondlayer-schema-hash` | string | sha256 of the definition's name, schema and sources. Changes only when they do. |
| `x-secondlayer-pin` | string | sha256 over the schema hash, bundled handler, startBlock, network and runtime. Absent for subgraphs deployed before pins existed. |
| `x-secondlayer-generated-at` | string · date-time | When this document was generated. |
| `x-secondlayer-sources` | object | The definition's `sources`, as written. |
| `x-secondlayer-tables` | array of string | Table names. |

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "sbtc-token Subgraph API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://127.0.0.1:3800"
    }
  ],
  "paths": {
    "/v1/subgraphs/sbtc-token/transfers": {
      "get": {
        "summary": "Query sbtc-token.transfers",
        "operationId": "query_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rows returned from the subgraph table.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/transfersRow"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tip": {
                      "type": "object",
                      "properties": {
                        "block_height": {
                          "type": "integer"
                        },
                        "subgraph_height": {
                          "type": "integer"
                        },
                        "blocks_behind": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/subgraphs/sbtc-token/transfers/count": {
      "get": {
        "summary": "Count sbtc-token.transfers",
        "operationId": "count_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Row count for the filtered table query.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "count"
                  ],
                  "example": {
                    "count": 48138
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "transfersRow": {
        "type": "object",
        "properties": {
          "sender": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "asset_identifier": {
            "type": "string"
          },
          "_id": {
            "type": "integer"
          },
          "_block_height": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "_tx_id": {
            "type": "string"
          },
          "_created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "example": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        }
      }
    }
  },
  "x-secondlayer-subgraph": "sbtc-token",
  "x-secondlayer-version": "1.0.0",
  "x-secondlayer-schema-hash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "x-secondlayer-generated-at": "2026-09-23T15:12:00.000Z",
  "x-secondlayer-sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "x-secondlayer-tables": [
    "transfers"
  ]
}
```

### The Subgraph agent schema object

Compact description of a subgraph for agents and tools.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `version` | string | Deployed version. |
| `description` | string | The definition's `description`. Absent without one. |
| `schemaHash` | string | sha256 of the definition's name, schema and sources. |
| `pin` | string | sha256 over the schema hash, bundled handler, startBlock, network and runtime. |
| `generatedAt` | string · date-time | When this schema was generated. |
| `serverUrl` | string | The `server` param, or this request's origin. |
| `sources` | object | The definition's `sources`, as written. |
| `tables` | object | Per table: absolute endpoints, exact `rowCount`, columns including system columns, accepted `query` params and filters, and example requests. |

```json
{
  "name": "sbtc-token",
  "version": "1.0.0",
  "schemaHash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "generatedAt": "2026-09-23T15:12:00.000Z",
  "serverUrl": "http://127.0.0.1:3800",
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "tables": {
    "transfers": {
      "endpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers",
      "countEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/count",
      "aggregateEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/aggregate",
      "streamEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/stream",
      "rowCount": 48138,
      "columns": {
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "amount": {
          "type": "uint"
        },
        "asset_identifier": {
          "type": "text",
          "indexed": true
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "query": {
        "parameters": [
          "_limit",
          "cursor",
          "_order",
          "_fields"
        ],
        "sortable": [],
        "selectable": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "searchable": [],
        "filters": [
          "sender",
          "sender.neq",
          "sender.like",
          "recipient",
          "recipient.neq",
          "recipient.like",
          "amount",
          "amount.neq",
          "amount.gt",
          "amount.gte",
          "amount.lt",
          "amount.lte",
          "asset_identifier",
          "asset_identifier.neq",
          "asset_identifier.like",
          "_id",
          "_id.neq",
          "_id.gt",
          "_id.gte",
          "_id.lt",
          "_id.lte",
          "_block_height",
          "_block_height.neq",
          "_block_height.gt",
          "_block_height.gte",
          "_block_height.lt",
          "_block_height.lte",
          "_tx_id",
          "_tx_id.neq",
          "_tx_id.like",
          "_created_at",
          "_created_at.neq",
          "_created_at.gt",
          "_created_at.gte",
          "_created_at.lt",
          "_created_at.lte",
          "_created_at.like"
        ]
      },
      "examples": {
        "list": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        },
        "count": {
          "count": 48138
        },
        "curl": "curl 'http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers?_limit=10&_order=desc'"
      }
    }
  }
}
```

### The Subgraph rows page object

One page of rows from `GET /v1/subgraphs/{name}/{table}`.

| Field | Type | Description |
| --- | --- | --- |
| `rows` | array of SubgraphRow | The page, in `_id` order or `_sort` order. |
| `next_cursor` | string · nullable | Pass back as `cursor` to resume after this page. Set on every page that returned rows, including a short last page; `null` only on an empty page, which means you are caught up. To tail, keep the last cursor you got: `cursor = next_cursor ?? cursor`. Without `_sort` it is the last row's `_id`; with `_sort` it is an opaque token. |
| `tip` | SubgraphTip |  |

```json
{
  "rows": [
    {
      "_id": "48138",
      "_block_height": "9048926",
      "_tx_id": "0x926f5ff4326155dea077126d21810fc7954568647898dbfb2f66a28b880dc5c9",
      "_created_at": "2026-09-23T15:10:29.114Z",
      "sender": "SP1TPYSSG52FKCDHHN8Y76ABMA12P069BZBFAYEAF",
      "recipient": "SP3EWCDA3V8HCP64CSETSNYXZ25WC4AJ95EC0ZEST.zr-d-0",
      "amount": "14000",
      "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  ],
  "next_cursor": "48138",
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  }
}
```

### The Subgraph row count object

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Rows matching the filters, or the planner's estimate for an unfiltered `_count=estimate`. |

```json
{
  "count": 48138
}
```

### The Subgraph aggregates object

Only the aggregates you asked for are present. Sums, minimums and maximums are strings so large values stay exact.

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Rows matching the filters. Present with `_count`, or when no other aggregate was asked for. |
| `countDistinct` | object | Distinct values per `_countDistinct` column. |
| `sum` | object | Sum per `_sum` column. `"0"` over no rows. |
| `min` | object | Minimum per `_min` column. `null` over no rows. |
| `max` | object | Maximum per `_max` column. `null` over no rows. |

```json
{
  "count": 3,
  "countDistinct": {
    "sender": 3
  },
  "sum": {
    "amount": "42000"
  },
  "max": {
    "amount": "14000"
  }
}
```

### The Deployed subgraph object

One deployed subgraph, as the list returns it.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `version` | string | Deployed version. |
| `status` | enum | Processor state. Values: `active`, `reindexing`, `paused`, `error`. |
| `lastProcessedBlock` | integer | Highest block processed. |
| `totalProcessed` | integer | Events handled since deploy. |
| `totalRows` | integer | Approximate rows across tables, from table statistics. |
| `totalErrors` | integer | Handler errors since deploy. |
| `tables` | array of string | Table names. |
| `chainTip` | integer | Progress denominator: the chain tip, or the reindex target while reindexing. |
| `sourceChainTip` | integer | The instance's chain tip. |
| `targetBlock` | integer | Height this sync or reindex is walking toward. |
| `progress` | number · 0 to 1 | Fraction of blocks processed, 0 to 1. |
| `blocksRemaining` | integer | Blocks left to `targetBlock`. |
| `syncMode` | enum | `reindex` while a reindex walk runs. Values: `sync`, `reindex`. |
| `gapCount` | integer | Open gaps. |
| `integrity` | enum | `gaps_detected` when any gap is open. Values: `complete`, `gaps_detected`. |
| `lastError` | string · nullable | Most recent processing error. |
| `lastErrorAt` | string · date-time · nullable | When it happened. |
| `updatedAt` | string · date-time · nullable | Last registry update. |
| `createdAt` | string · date-time | First deploy. |
| `health` | SubgraphHealth | Hourly health, present only when the list is requested with `health`. |

```json
{
  "name": "sbtc-flows",
  "version": "1.0.2",
  "status": "active",
  "lastProcessedBlock": 9048712,
  "totalProcessed": 48210,
  "totalRows": 48138,
  "totalErrors": 0,
  "tables": [
    "transfers"
  ],
  "chainTip": 9048712,
  "sourceChainTip": 9048712,
  "targetBlock": 9048712,
  "progress": 1,
  "blocksRemaining": 0,
  "syncMode": "sync",
  "gapCount": 0,
  "integrity": "complete",
  "lastError": null,
  "lastErrorAt": null,
  "updatedAt": "2026-09-23T14:02:11.000Z",
  "createdAt": "2026-09-21T09:14:52.000Z"
}
```

### The Subgraph bundle object

A compiled subgraph. Everything but `ok` and `bundleSize` is the body for `POST /api/subgraphs`.

| Field | Type | Description |
| --- | --- | --- |
| `ok` | enum | Always `true`. Values: `true`. |
| `name` | string | Name from the definition. |
| `description` | string · nullable | Description from the definition, if set. |
| `sources` | object | Event sources by name. |
| `schema` | object | Table definitions by name. |
| `handlerCode` | string | The bundled handler module. |
| `sourceCode` | string | The `code` you sent, echoed back. |
| `bundleSize` | integer | Size of `handlerCode` in bytes. |

```json
{
  "ok": true,
  "name": "sbtc-flows",
  "description": null,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "schema": {
    "transfers": {
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        }
      }
    }
  },
  "handlerCode": "// secondlayer-stub:@secondlayer/subgraphs\nfunction defineSubgraph(def) {\n  return def;\n}\n\n// <stdin>\nvar stdin_default = defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8e6,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\"\n    }\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" }\n      }\n    }\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    }\n  }\n});\nexport {\n  stdin_default as default\n};\n",
  "sourceCode": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n",
  "bundleSize": 724
}
```

### The Deployed subgraph detail object

One deployed subgraph in full.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `version` | string | Deployed version. |
| `schemaHash` | string | Hash of the table schema. Changes when the schema does. |
| `pin` | string · nullable | Hash of everything that shapes rows: schema hash, bundled handler, startBlock, network, runtime. Null for subgraphs deployed before pins existed. |
| `verification` | object | Derived verification level. Null for subgraphs deployed before levels were derived. |
| `status` | enum | Processor state. Values: `active`, `reindexing`, `paused`, `error`. |
| `lastProcessedBlock` | integer | Highest block processed. |
| `description` | string | From the definition, when set. |
| `sources` | object | Event sources by name, from the definition. |
| `definition` | object · nullable | The stored definition: `name`, `description`, `startBlock`, `sources`, `schema`. |
| `health` | object | `totalProcessed` and `totalErrors` since deploy, `errorRate`, the last error and when, `emptyMapping` (events matched but every table is empty), and `violationCount` (print violations kept). |
| `sync` | DeployedSubgraphSync |  |
| `tables` | object | By table name: read `endpoint`, `columns` (with the system columns `_id`, `_block_height`, `_tx_id`, `_created_at`), exact `rowCount`, an `example` query, and `indexes` and `uniqueKeys` when declared. |
| `createdAt` | string · date-time | First deploy. |
| `updatedAt` | string · date-time | Last registry update. |

```json
{
  "name": "sbtc-flows",
  "version": "1.0.2",
  "schemaHash": "4b9e1d27c83f0a65e2d7b14c9f06a38e5d21c7b90f4e6a83d15c2b7e09f4a6d1",
  "pin": "7f3c0d5e9b21a4c86f0e3d17b5a92c4e8d61f07a3b9c25e4d8f1a06c7b3e5a91e",
  "verification": {
    "level": "events",
    "reasons": [
      "ft_transfer source \"transfers\" needs event proofs"
    ],
    "unproven": []
  },
  "status": "active",
  "lastProcessedBlock": 9048712,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "definition": {
    "name": "sbtc-flows",
    "startBlock": 8000000,
    "sources": {
      "transfers": {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
      }
    },
    "schema": {
      "transfers": {
        "columns": {
          "amount": {
            "type": "uint"
          },
          "sender": {
            "type": "principal"
          },
          "recipient": {
            "type": "principal"
          }
        }
      }
    }
  },
  "health": {
    "totalProcessed": 48210,
    "totalErrors": 0,
    "errorRate": 0,
    "lastError": null,
    "lastErrorAt": null,
    "emptyMapping": false,
    "violationCount": 0
  },
  "sync": {
    "status": "synced",
    "mode": "sync",
    "startBlock": 8000000,
    "lastProcessedBlock": 9048712,
    "chainTip": 9048712,
    "sourceChainTip": 9048712,
    "targetBlock": 9048712,
    "blocksRemaining": 0,
    "processedBlocks": 1048713,
    "totalBlocks": 1048713,
    "progress": 1,
    "gaps": {
      "count": 0,
      "totalMissingBlocks": 0,
      "ranges": []
    },
    "integrity": "complete"
  },
  "tables": {
    "transfers": {
      "endpoint": "/subgraphs/sbtc-flows/transfers",
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "rowCount": 48138,
      "example": "/subgraphs/sbtc-flows/transfers?_sort=_block_height&_order=desc&_limit=10"
    }
  },
  "createdAt": "2026-09-21T09:14:52.000Z",
  "updatedAt": "2026-09-23T14:02:11.000Z"
}
```

### The Queued operation object

A reindex or backfill accepted into the queue.

| Field | Type | Description |
| --- | --- | --- |
| `message` | string | One-line summary. |
| `fromBlock` | integer | First block. For a reindex, the subgraph's start block. |
| `toBlock` | integer | Last block. For a reindex, the string `"chain tip"`: it runs to whatever the tip is when it gets there. |
| `operationId` | string · uuid | Poll `/api/subgraphs/{name}/operations/{operation_id}`. |
| `status` | enum | Always `queued`. Values: `queued`. |

```json
{
  "message": "Backfill queued for subgraph \"sbtc-flows\"",
  "fromBlock": 8950000,
  "toBlock": 9000000,
  "operationId": "9a4e7c21-0b3d-4f6a-8c15-e2d7b9f0a461",
  "status": "queued"
}
```

### The Subgraph operation object

A reindex or backfill. One per subgraph can be queued or running at a time.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Operation id. |
| `subgraphName` | string | Subgraph it runs on. |
| `kind` | enum | `reindex` drops and rebuilds; `backfill` re-runs a range in place. Values: `reindex`, `backfill`. |
| `status` | enum | `queued` until a processor claims it. `completed`, `failed` and `cancelled` are final. Values: `queued`, `running`, `completed`, `failed`, `cancelled`. |
| `weight` | enum | `light` for contract-scoped sources, `heavy` for broad ones. The processor limits how many heavy operations run at once. Values: `light`, `heavy`. |
| `fromBlock` | integer · nullable | First block. `null` on a reindex queued from `/reindex`, which starts at the subgraph's start block. |
| `toBlock` | integer · nullable | Last block. `null` means the chain tip. |
| `processedBlocks` | integer · nullable | Blocks covered. Set when the operation finishes. |
| `cursorBlock` | integer · nullable | Backfills only: last committed block. A requeued backfill resumes after it. |
| `estimatedEvents` | integer · nullable | Events expected, computed at enqueue for contract-scoped sources. |
| `processedEvents` | integer · nullable | Events processed so far. |
| `progress` | number · 0 to 1 · nullable | Fraction done, 0 to 1, from events when estimated, else blocks. `null` before any progress is reported. |
| `queuePosition` | integer | Queued only: approximate 1-based position. Show it as `~N`. |
| `error` | string · nullable | Why it failed. |
| `startedAt` | string · date-time · nullable | When a processor claimed it. |
| `finishedAt` | string · date-time · nullable | When it reached a final status. |
| `createdAt` | string · date-time | When it was queued. |
| `updatedAt` | string · date-time | Last change. |

```json
{
  "id": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
  "subgraphName": "sbtc-flows",
  "kind": "reindex",
  "status": "running",
  "weight": "light",
  "fromBlock": 8000000,
  "toBlock": 9048712,
  "processedBlocks": null,
  "cursorBlock": null,
  "estimatedEvents": 48210,
  "processedEvents": 24105,
  "progress": 0.5,
  "error": null,
  "startedAt": "2026-09-21T09:15:04.000Z",
  "finishedAt": null,
  "createdAt": "2026-09-21T09:14:53.000Z",
  "updatedAt": "2026-09-21T09:18:37.000Z"
}
```

### The Subgraph gap object

A block range the subgraph did not process.

| Field | Type | Description |
| --- | --- | --- |
| `start` | integer | First missing block. |
| `end` | integer | Last missing block, inclusive. |
| `size` | integer | Blocks in the range. |
| `reason` | enum | `block_missing`: the block was not in the index. `processing_error`: a handler threw. Values: `block_missing`, `processing_error`. |
| `detectedAt` | string · date-time | When it was recorded. |
| `resolvedAt` | string · date-time · nullable | When a later walk covered it. `null` while open. |

```json
{
  "start": 8412207,
  "end": 8412207,
  "size": 1,
  "reason": "processing_error",
  "detectedAt": "2026-09-22T18:40:03.000Z",
  "resolvedAt": null
}
```

### The Subgraph health object

Hourly health buckets and the newest 24 hours' handler errors.

| Field | Type | Description |
| --- | --- | --- |
| `hours` | array of object | One bucket per hour, oldest first. |
| `errors24h` | integer | Handler errors across the newest 24 hours. |

```json
{
  "hours": [
    {
      "hour": "2026-10-16T13:00:00.000Z",
      "lagBlocks": 0,
      "rowsWritten": 1840,
      "errors": 0,
      "eventsHandled": 2213
    }
  ],
  "errors24h": 0
}
```

### The Print validation skip object

A print event skipped because its payload did not match the declared `prints` schema.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Violation id. |
| `sourceName` | string | Source whose `prints` schema refused it. |
| `blockHeight` | integer | Block of the event. |
| `txId` | string | Transaction of the event. |
| `reason` | string | Which field was missing or had the wrong type. |
| `samplePayload` | object | The decoded print, as `topic` and `data`. |
| `seenAt` | string · date-time | When it was skipped. |

```json
{
  "id": "c41d8e27-6f0a-4b93-a5e2-7d19b3c8f056",
  "sourceName": "swaps",
  "blockHeight": 9031477,
  "txId": "0x8d2f6a91c4e07b35d1a8f2e69c0b47d3a5e18f62b9c04d7e3a16f58b2c9d0e41",
  "reason": "missing declared field \"amount-in\"",
  "samplePayload": {
    "topic": "swap",
    "data": {
      "amount-out": "1500000",
      "sender": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9"
    }
  },
  "seenAt": "2026-09-23T11:27:45.000Z"
}
```

## Notes for agents

- Read: 'GET $SECONDLAYER_API_URL/v1/subgraphs/<name>/<table>' with 'Authorization: Bearer $SECONDLAYER_API_KEY'; page with next_cursor.
- Describe: '/v1/subgraphs/<name>/schema.json' is the machine-readable schema; '/openapi.json' is a per-subgraph OpenAPI document.
- Deploy and operate: '/api/subgraphs' is the self-hosted write plane; authenticate with $INSTANCE_TOKEN.
- One endpoint as markdown: 'https://secondlayer.tools/docs/api-reference/<anchor>.md', e.g. list-subgraph-rows.

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