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

# Instance API

Discovery and batch reads, plus the catalog, archive check and node reads of a self-hosted instance. Envelope and cursors are in [REST conventions](/docs/rest-api); running an instance is [Self-host](/docs/self-host).

## Discovery and batch

Surfaces, batched reads and this OpenAPI document.

### Surface discovery

`GET /v1`

Lists the public read surfaces on this instance, where this document lives, the list envelope shape, and the cursor format. A starting point for an agent that has only the base URL.

#### Returns

One `V1Discovery` object.

#### Responses

- `200` The surfaces this instance serves
- `401` This bind requires the instance token, and it was missing or wrong

#### Example

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

```json
{
  "surfaces": [
    {
      "name": "index",
      "path": "/v1/index",
      "description": "Decoded chain events via /v1/index/events?event_type=… (stx/ft/nft transfers, mints, burns, stacking locks, and contract prints), with typed ft-transfers/nft-transfers aliases.",
      "auth": "open on this instance"
    },
    {
      "name": "streams",
      "path": "/v1/streams",
      "description": "Raw, ordered, cursor-paginated firehose with reorg awareness.",
      "auth": "open on this instance"
    },
    {
      "name": "subgraphs",
      "path": "/v1/subgraphs",
      "description": "Custom indexed views. Unique local name. Cursor envelope: { rows, next_cursor, tip }.",
      "auth": "open on this instance"
    },
    {
      "name": "proofs",
      "path": "/v1/proofs",
      "description": "Chain proofs to check Index rows without trusting this API: state witnesses, signed blocks, MARF proofs, burn-block preimages, Bitcoin headers. Proxied unchanged from the node and its proof sidecar. Never metered.",
      "auth": "open on this instance"
    },
    {
      "name": "instance",
      "path": "/v1/instance",
      "description": "Local catalog: instance status, subgraphs, webhooks, and default features. No signup or pricing.",
      "auth": "open on this instance"
    }
  ],
  "openapi": "/v1/openapi.json",
  "envelope_examples": {
    "cursor_paginated": {
      "events": [
        "..."
      ],
      "next_cursor": "7960000:42",
      "tip": {
        "block_height": 7978686,
        "lag_seconds": 12
      },
      "reorgs": [
        {
          "detected_at": "2026-05-16T00:00:00Z",
          "new_canonical_tip": "7960000:42",
          "new_canonical_height": 7960000,
          "new_canonical_event_index": 42
        }
      ]
    }
  },
  "cursor_format": "<block_height>:<event_index> (opaque resume token)"
}
```

### Batch public reads

`POST /v1/batch`

Up to 10 GET reads under `/v1/index/`, `/v1/subgraphs`, `/v1/streams/` and `/v1/contracts` in one round trip. Items run in parallel through the full app, so auth, rate limits and retention apply per item; the request's `Authorization` header is forwarded to every item. Results come back in request order, each with its own HTTP status and JSON body. A refused or failed item does not fail the batch.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `requests` | array of object, required | The reads to run, 1 to 10. |

#### Returns

One `BatchResponse` object.

#### Responses

- `200` One result per request, in request order
- `400` The body is not `{ requests: [...] }`, `requests` is empty, or it holds more than 10 items (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong

#### Example

```bash
curl -X POST https://api.secondlayer.tools/v1/batch \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "requests": [
    {
      "path": "/v1/streams/tip"
    },
    {
      "path": "/v1/streams/canonical/9048876"
    },
    {
      "path": "/api/subgraphs"
    }
  ]
}'
```

```json
{
  "results": [
    {
      "path": "/v1/streams/tip",
      "status": 200,
      "body": {
        "block_height": 9048928,
        "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
        "burn_block_height": 968284,
        "finalized_height": 9048671,
        "lag_seconds": 4,
        "oldest_seekable_height": null,
        "oldest_cursor": null
      }
    },
    {
      "path": "/v1/streams/canonical/9048876",
      "status": 200,
      "body": {
        "block_height": 9048876,
        "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
        "burn_block_height": 968284,
        "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
        "is_canonical": true
      }
    },
    {
      "path": "/api/subgraphs",
      "status": 400,
      "body": {
        "error": "Path not allowed in batch (public /v1 reads only)",
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}
```

### This document

`GET /v1/openapi.json`

The OpenAPI 3.1 description of this instance, as JSON. It describes the instance's own mode: a self-hosted instance includes the `/api` write plane, the hosted archive leaves it out.

#### Responses

- `200` The OpenAPI document
- `401` This bind requires the instance token, and it was missing or wrong

#### Example

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

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Secondlayer Public API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://127.0.0.1:3800",
      "description": "Local instance"
    }
  ],
  "tags": [
    {
      "name": "streams",
      "description": "Raw event firehose, chain tip, and reorg history"
    }
  ],
  "paths": {},
  "components": {}
}
```

## Your instance

The catalog, features and vitals of this instance.

### Local instance catalog

`GET /v1/instance` (self-hosted instances only)

What this instance is and holds: its mode and network, the feature manifest, the history it claims, and the subgraphs and webhooks deployed on it. No signup or pricing, ever.

#### Returns

One `InstanceCatalog` object.

#### Responses

- `200` The catalog. If the database is unreachable, lists come back empty rather than failing.
- `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

#### Example

```bash
curl http://127.0.0.1:3800/v1/instance
```

```json
{
  "mode": "oss",
  "network": "mainnet",
  "instance_id": "0b6f1c9e-4a52-4f0e-9d8a-2c1e7b3d5a90",
  "features": {
    "index": true,
    "subgraphs": true,
    "webhooks": true
  },
  "scope": {
    "network": "mainnet",
    "start_height": 8650000,
    "target_height": null,
    "bootstrap": {
      "source": "archive",
      "manifest_digest": "d1db4af398e9a0418095a48beef1c6c595070df3398f92ef16ef0fdb10daed1b",
      "genesis_hash": null
    }
  },
  "subgraphs": [
    {
      "name": "fastpool-signers",
      "status": "active",
      "start_block": 8665568,
      "last_processed_block": 9048707
    }
  ],
  "webhooks": [
    {
      "name": "pox5-stakes",
      "status": "active",
      "kind": "chain"
    }
  ],
  "console": {
    "signup": false,
    "pricing": false,
    "publicDirectory": false
  }
}
```

### Default feature manifest

`GET /v1/instance/features` (self-hosted instances only)

Which modules this instance runs, including which protocol decoders are on.

#### Returns

One `InstanceFeatures` object.

#### Responses

- `200` The declared mode and feature manifest.
- `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

#### Example

```bash
curl http://127.0.0.1:3800/v1/instance/features
```

```json
{
  "mode": "oss",
  "features": {
    "rawRest": true,
    "rawSse": true,
    "index": true,
    "subgraphs": true,
    "webhooks": true,
    "contractDiscovery": true,
    "verification": true,
    "protocolDatasets": {
      "sbtc": true,
      "pox": true,
      "bns": false
    },
    "signup": false,
    "pricing": false,
    "publicDirectory": false,
    "unsignedWebhooks": false
  }
}
```

### Operational vitals

`GET /v1/instance/metrics` (self-hosted instances only)

Process uptime, database size, the last 24 hours of webhook delivery outcomes, and rows processed per hour. A value that can't be measured comes back `null` instead of failing the request.

#### Returns

One `InstanceMetrics` object.

#### Responses

- `200` Current vitals.
- `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

#### Example

```bash
curl http://127.0.0.1:3800/v1/instance/metrics
```

```json
{
  "uptime_s": 86400,
  "db_size_bytes": 61203865600,
  "deliveries_24h": {
    "total": 460,
    "failed": 2,
    "dlq": 0
  },
  "rows_series": [
    {
      "t": "2026-09-23 13:00:00+00",
      "rows": 3202
    }
  ]
}
```

## Archive check

Verify this instance against a signed archive.

### Verify this instance against a signed archive

`POST /v1/archive/verify`

Compares identity digests of this instance's blocks, transactions and events against the range digests in a signed archive manifest. Read-only: the instance fetches the manifest, nothing is uploaded. Semantic replay (`--deep`) is CLI only. Without `from_block` or `to_block`, a manifest with more than 40 matching ranges is refused with 400. A missing key, an unreachable manifest, a failed signature or no matching digests answer 200 `unanchored` with a `reason`, never `clean`.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `against` | string · uri, required | https URL of `latest.json` or a snapshot manifest. Local paths and plain http are refused. |
| `target` | string | `raw` (default) or `all` check blocks, transactions and events. `decode:<name>` and `subgraph:<name>` parse, but only raw datasets have digests, so they answer `unanchored` with `no digests`. |
| `from_block` | integer | First height to check. Needed, with or without `to_block`, when the manifest has more than 40 matching ranges. |
| `to_block` | integer | Last height to check. Must be at least `from_block`. |
| `insecure` | boolean | Compare even when the signature does not verify. `signature.verified` stays `false`. |
| `public_key_pem` | string | Archive signing key to verify with. Without it the instance uses `ARCHIVE_SIGNING_PUBLIC_KEY` (or `STREAMS_SIGNING_PUBLIC_KEY`); the hosted API also fetches the published key. |

#### Returns

One `ArchiveVerifyResult` object.

#### Responses

- `200` The comparison, or `unanchored` with the reason it could not run
- `400` The body is not JSON (`INVALID_JSON`), `against` is missing or not https, a field has the wrong type, `target` is not recognised, `to_block` is below `from_block`, or more than 40 ranges matched without a height window (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong

#### Example

```bash
curl -X POST https://api.secondlayer.tools/v1/archive/verify \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "against": "https://archive.secondlayer.tools/latest.json",
  "target": "raw",
  "from_block": 0,
  "to_block": 99999
}'
```

```json
{
  "status": "diverged",
  "target": "raw",
  "against": "https://archive.secondlayer.tools/latest.json",
  "signature": {
    "verified": true
  },
  "coverage": {
    "from_block": 0,
    "to_block": 99999
  },
  "ranges": [
    {
      "dataset": "blocks",
      "from_block": 0,
      "to_block": 49999,
      "status": "match",
      "expected_digest": "5d41402abc4b2a76b9719d911017c592",
      "actual_digest": "5d41402abc4b2a76b9719d911017c592"
    },
    {
      "dataset": "blocks",
      "from_block": 50000,
      "to_block": 99999,
      "status": "digest-mismatch",
      "expected_digest": "7d793037a0760186574b0282f2f435e7",
      "actual_digest": "e2fc714c4727ee9395f324cd2e7f331f"
    }
  ]
}
```

## Node

Reads proxied from the local Stacks node.

### Contract ABI from the local Stacks node

`GET /api/node/contracts/{contract_id}/abi` (self-hosted instances only)

Fetches the contract's interface from the Stacks node this instance follows (`/v2/contracts/interface`) and returns it unchanged. Nothing is cached. Self-hosted only; needs the instance token whenever one is set.

#### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `contract_id` | string, required | Contract principal, `<address>.<name>`. A value without a `.` does not match the route and gets 404. |

#### Returns

One `ContractAbi` object.

#### Responses

- `200` The contract's ABI as the node reports it
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The node has no contract with that id, or the id has no `.`
- `502` The node could not be reached or answered with an error; `error` carries its message

#### Example

```bash
curl http://127.0.0.1:3800/api/node/contracts/SP3D23QJG4S1G6TBXSPFR0N3PMF53WXA50XTX9W0M.zeus-a/abi \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "functions": [
    {
      "name": "check-is-owner",
      "access": "private",
      "args": [],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    },
    {
      "name": "start-zeus",
      "access": "public",
      "args": [
        {
          "name": "dx",
          "type": "uint128"
        },
        {
          "name": "min-dy",
          "type": "uint128"
        },
        {
          "name": "offset",
          "type": "uint128"
        }
      ],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    }
  ],
  "variables": [
    {
      "name": "ERR-NOT-AUTHORIZED",
      "type": {
        "response": {
          "ok": "none",
          "error": "uint128"
        }
      },
      "access": "constant"
    },
    {
      "name": "contract-owner",
      "type": "principal",
      "access": "variable"
    }
  ],
  "maps": [],
  "fungible_tokens": [],
  "non_fungible_tokens": [],
  "epoch": "Epoch2_05",
  "clarity_version": "Clarity1"
}
```

## Objects

### The API discovery object

The public read surfaces and how to page them.

| Field | Type | Description |
| --- | --- | --- |
| `surfaces` | array of object | Each surface's `name`, base `path`, a one-line `description`, and its `auth` rule. |
| `openapi` | string | Path of this OpenAPI document. |
| `envelope_examples` | object | An illustrative cursor-paginated list envelope. |
| `cursor_format` | string | Index and Streams cursor format. |

```json
{
  "surfaces": [
    {
      "name": "index",
      "path": "/v1/index",
      "description": "Decoded chain events via /v1/index/events?event_type=… (stx/ft/nft transfers, mints, burns, stacking locks, and contract prints), with typed ft-transfers/nft-transfers aliases.",
      "auth": "open on this instance"
    },
    {
      "name": "streams",
      "path": "/v1/streams",
      "description": "Raw, ordered, cursor-paginated firehose with reorg awareness.",
      "auth": "open on this instance"
    },
    {
      "name": "subgraphs",
      "path": "/v1/subgraphs",
      "description": "Custom indexed views. Unique local name. Cursor envelope: { rows, next_cursor, tip }.",
      "auth": "open on this instance"
    },
    {
      "name": "proofs",
      "path": "/v1/proofs",
      "description": "Chain proofs to check Index rows without trusting this API: state witnesses, signed blocks, MARF proofs, burn-block preimages, Bitcoin headers. Proxied unchanged from the node and its proof sidecar. Never metered.",
      "auth": "open on this instance"
    },
    {
      "name": "instance",
      "path": "/v1/instance",
      "description": "Local catalog: instance status, subgraphs, webhooks, and default features. No signup or pricing.",
      "auth": "open on this instance"
    }
  ],
  "openapi": "/v1/openapi.json",
  "envelope_examples": {
    "cursor_paginated": {
      "events": [
        "..."
      ],
      "next_cursor": "7960000:42",
      "tip": {
        "block_height": 7978686,
        "lag_seconds": 12
      },
      "reorgs": [
        {
          "detected_at": "2026-05-16T00:00:00Z",
          "new_canonical_tip": "7960000:42",
          "new_canonical_height": 7960000,
          "new_canonical_event_index": 42
        }
      ]
    }
  },
  "cursor_format": "<block_height>:<event_index> (opaque resume token)"
}
```

### The Batch response object

The batch results.

| Field | Type | Description |
| --- | --- | --- |
| `results` | array of object | One entry per request, in order: the item's `path`, the HTTP `status` it got, and its JSON `body`. A path outside the allowed prefixes gets status 400; an item that throws gets status 500 with code `INTERNAL_ERROR`. |

```json
{
  "results": [
    {
      "path": "/v1/streams/tip",
      "status": 200,
      "body": {
        "block_height": 9048928,
        "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
        "burn_block_height": 968284,
        "finalized_height": 9048671,
        "lag_seconds": 4,
        "oldest_seekable_height": null,
        "oldest_cursor": null
      }
    },
    {
      "path": "/v1/streams/canonical/9048876",
      "status": 200,
      "body": {
        "block_height": 9048876,
        "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
        "burn_block_height": 968284,
        "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
        "is_canonical": true
      }
    },
    {
      "path": "/api/subgraphs",
      "status": 400,
      "body": {
        "error": "Path not allowed in batch (public /v1 reads only)",
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}
```

### The Instance catalog object

What this instance is and holds.

| Field | Type | Description |
| --- | --- | --- |
| `mode` | enum | The mode the operator declared in `INSTANCE_MODE`. Values: `oss`, `platform`, `archive`. |
| `network` | string | `mainnet`, `testnet` or `devnet`. |
| `instance_id` | string · nullable | This instance's id, or `null` before first boot. |
| `features` | object | Same manifest as `/v1/instance/features`. |
| `scope` | object · nullable | The history this instance claims: `start_height`, optional `target_height`, and how it was acquired (`bootstrap.source` is `archive`, `genesis` or `import`, with the archive manifest digest when restored). `null` on an instance older than the scope table. |
| `subgraphs` | array of object | Deployed subgraphs: `name`, `status`, `start_block`, `last_processed_block`. |
| `webhooks` | array of object | Webhooks: `name`, `status`, `kind` (`chain`). |
| `console` | object | Console flags. Always false on a self-hosted instance. |

```json
{
  "mode": "oss",
  "network": "mainnet",
  "instance_id": "0b6f1c9e-4a52-4f0e-9d8a-2c1e7b3d5a90",
  "features": {
    "index": true,
    "subgraphs": true,
    "webhooks": true
  },
  "scope": {
    "network": "mainnet",
    "start_height": 8650000,
    "target_height": null,
    "bootstrap": {
      "source": "archive",
      "manifest_digest": "d1db4af398e9a0418095a48beef1c6c595070df3398f92ef16ef0fdb10daed1b",
      "genesis_hash": null
    }
  },
  "subgraphs": [
    {
      "name": "fastpool-signers",
      "status": "active",
      "start_block": 8665568,
      "last_processed_block": 9048707
    }
  ],
  "webhooks": [
    {
      "name": "pox5-stakes",
      "status": "active",
      "kind": "chain"
    }
  ],
  "console": {
    "signup": false,
    "pricing": false,
    "publicDirectory": false
  }
}
```

### The Instance feature manifest object

The declared mode and the modules this instance runs.

| Field | Type | Description |
| --- | --- | --- |
| `mode` | enum | The mode the operator declared in `INSTANCE_MODE`. Values: `oss`, `platform`, `archive`. |
| `features` | object | Module flags. `protocolDatasets` says which protocol decoders are on; `signup`, `pricing` and `publicDirectory` are always false here. |

```json
{
  "mode": "oss",
  "features": {
    "rawRest": true,
    "rawSse": true,
    "index": true,
    "subgraphs": true,
    "webhooks": true,
    "contractDiscovery": true,
    "verification": true,
    "protocolDatasets": {
      "sbtc": true,
      "pox": true,
      "bns": false
    },
    "signup": false,
    "pricing": false,
    "publicDirectory": false,
    "unsignedWebhooks": false
  }
}
```

### The Instance metrics object

Operational vitals.

| Field | Type | Description |
| --- | --- | --- |
| `uptime_s` | integer | Seconds since the API process started. |
| `db_size_bytes` | integer · nullable | Postgres database size, or `null` if it can't be read. |
| `deliveries_24h` | object · nullable | Webhook deliveries in the last 24 hours: `total`, `failed` (no status or 400+), and `dlq` (events currently dead-lettered). |
| `rows_series` | array of object | Rows processed across all subgraphs, per hour over the last 24 hours: `{ t, rows }`. |

```json
{
  "uptime_s": 86400,
  "db_size_bytes": 61203865600,
  "deliveries_24h": {
    "total": 460,
    "failed": 2,
    "dlq": 0
  },
  "rows_series": [
    {
      "t": "2026-09-23 13:00:00+00",
      "rows": 3202
    }
  ]
}
```

### The Archive verification object

The outcome of an archive comparison.

| Field | Type | Description |
| --- | --- | --- |
| `status` | enum | `clean`: every range matched. `diverged`: at least one did not. `unanchored`: the comparison could not be trusted or run; see `reason`. Values: `clean`, `diverged`, `unanchored`. |
| `target` | string | The target that ran, normalised (`raw`, `all`, `decode:<name>`, `subgraph:<name>`). |
| `against` | string | The manifest URL, as sent. |
| `signature` | object | Whether the manifest signature verified, and why not when it did not. |
| `coverage` | object | Lowest and highest height compared. Absent when nothing was compared. |
| `ranges` | array of object | One row per compared range. `status` is `match`, `digest-mismatch`, `count-mismatch`, or `missing` when this instance has no rows there. Digests are `null` for an empty range. |
| `reason` | string | Why the result is `unanchored`: `no public key`, the fetch error, the signature failure, or `no digests`. |

```json
{
  "status": "diverged",
  "target": "raw",
  "against": "https://archive.secondlayer.tools/latest.json",
  "signature": {
    "verified": true
  },
  "coverage": {
    "from_block": 0,
    "to_block": 99999
  },
  "ranges": [
    {
      "dataset": "blocks",
      "from_block": 0,
      "to_block": 49999,
      "status": "match",
      "expected_digest": "5d41402abc4b2a76b9719d911017c592",
      "actual_digest": "5d41402abc4b2a76b9719d911017c592"
    },
    {
      "dataset": "blocks",
      "from_block": 50000,
      "to_block": 99999,
      "status": "digest-mismatch",
      "expected_digest": "7d793037a0760186574b0282f2f435e7",
      "actual_digest": "e2fc714c4727ee9395f324cd2e7f331f"
    }
  ]
}
```

### The Contract ABI object

A Clarity contract interface, exactly as the Stacks node's `/v2/contracts/interface` returns it.

| Field | Type | Description |
| --- | --- | --- |
| `functions` | array of object | Every function: `name`, `access` (`public`, `read_only` or `private`), `args` and `outputs`. |
| `variables` | array of object | Data vars and constants: `name`, `type`, and `access` (`variable` or `constant`). |
| `maps` | array of object | Data maps: `name`, `key` type and `value` type. |
| `fungible_tokens` | array of object | Fungible tokens the contract defines, by `name`. |
| `non_fungible_tokens` | array of object | Non-fungible tokens the contract defines: `name` and `type`. |
| `epoch` | string | Stacks epoch the contract was deployed in, e.g. `Epoch2_05`. |
| `clarity_version` | string | Clarity version it was deployed with, e.g. `Clarity1`. |

```json
{
  "functions": [
    {
      "name": "check-is-owner",
      "access": "private",
      "args": [],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    },
    {
      "name": "start-zeus",
      "access": "public",
      "args": [
        {
          "name": "dx",
          "type": "uint128"
        },
        {
          "name": "min-dy",
          "type": "uint128"
        },
        {
          "name": "offset",
          "type": "uint128"
        }
      ],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    }
  ],
  "variables": [
    {
      "name": "ERR-NOT-AUTHORIZED",
      "type": {
        "response": {
          "ok": "none",
          "error": "uint128"
        }
      },
      "access": "constant"
    },
    {
      "name": "contract-owner",
      "type": "principal",
      "access": "variable"
    }
  ],
  "maps": [],
  "fungible_tokens": [],
  "non_fungible_tokens": [],
  "epoch": "Epoch2_05",
  "clarity_version": "Clarity1"
}
```

## Notes for agents

- Discovery: 'GET $SECONDLAYER_API_URL/v1' lists every surface this instance serves; 'GET /v1/openapi.json' is the live OpenAPI document.
- Batch several public reads in one request with 'POST /v1/batch'.
- Instance endpoints use $INSTANCE_TOKEN on a self-hosted box; a loopback instance needs no key.
- One endpoint as markdown: 'https://secondlayer.tools/docs/api-reference/<anchor>.md', e.g. batch-read.

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