# Dappa API Skill

Query the **Dappa public indexer API** for ERC-8004 agent records, collections,
and onchain events.

## Base URL

```
https://dappa-indexer.onrender.com
```

All endpoints are **public GET requests** — no API key, no bearer token, no
auth headers.

## The agent model — one unified record per NFT

There is exactly **one agent record per NFT** `(chainId, tokenContract,
tokenId)`. Counterfactual is a **flag**, not a separate record type:

- `counterfactual: false` — a real ERC-8004 registration backs this NFT. The
  record carries `agentId`; `registrationHash` is null.
- `counterfactual: true` — only a counterfactual registration exists. The
  record carries `registrationHash`; `agentId` is null.

**A real registration always overrides a counterfactual one.** Once a real
registration appears on-chain, the unified record shows the real data and
`counterfactual` flips to false. There is no `recordType` field — filter and
branch on the `counterfactual` boolean.

## Identifiers and number handling

Values that can exceed JavaScript's safe integer range are **strings**:
`agentId`, `tokenId`, `blockNumber`, `timestamp` (Unix seconds). Addresses and
hashes are lowercase `0x` hex.

`agentUri` is the registry-backed URI registered through Adapter8004 — distinct
from the NFT's raw `tokenUri`. The indexer also resolves the NFT token metadata
into `tokenName`, `tokenDescription`, `tokenImage`, and `tokenAttributes`, with
a `tokenMetadataStatus` of `missing | pending | resolved | failed`.

## Cursor pagination

List endpoints return:

```json
{
  "items": [ /* ... */ ],
  "pageInfo": { "nextCursor": "eyJpZCI6...", "hasNextPage": true }
}
```

Pass `?cursor=<nextCursor>` for the next page and `?limit=<1-100>` to size it.

## Endpoints

| Endpoint | Purpose |
| -------- | ------- |
| `GET /api/health` | Indexer status + record counts. |
| `GET /api/stats` | Public counters. `agents.total`, `agents.counterfactual`, `agents.real`. |
| `GET /api/chains` | Supported chains (1 Ethereum, 8453 Base, 11155111 Sepolia). |
| `GET /api/collections` | Collection list. `?chainId`, cursor params. |
| `GET /api/collections/:chainId/:tokenContract` | One collection. |
| `GET /api/collections/:chainId/:tokenContract/agents` | Agents in a collection. `?counterfactual`. |
| `GET /api/agents` | Agent list — one row per NFT. `?chainId`, `?counterfactual`, `?tokenContract`, cursor. |
| `GET /api/agents/by-token/:chainId/:tokenContract/:tokenId` | The single unified record for one NFT — also the canonical detail endpoint. |
| `GET /api/agents/by-wallet/:address` | Agents by agent-wallet. `?chainId`, `?counterfactual`. |
| `GET /api/agents/by-metadata` | Agents by metadata key/value. (See caveat below.) |
| `GET /api/events` | Event feed / post-transaction polling. |
| `GET /api/metadata/keys` | Known metadata keys. |

Headline totals in `/api/stats` and `/api/health` count mainnet only
(Ethereum + Base); Sepolia is a testnet and is excluded from the totals while
still being fully queryable through every endpoint.

## Querying by collection and token

A collection id is `"<chainId>-<lowercase tokenContract>"`.

```
GET /api/collections/8453/0x270d25d2c59a8bca1b0f40ad95ff7806c0025c27/agents?counterfactual=true&limit=20
```

`by-token` returns **one** record (or null) — there is one agent per NFT:

```
GET /api/agents/by-token/1/0x9eb6e2025b64f340691e424b7fe7022ffde12438/93
```

```json
{
  "agent": {
    "counterfactual": false,
    "chainId": 1,
    "agentId": "32340",
    "registrationHash": null,
    "tokenContract": "0x9eb6e2025b64f340691e424b7fe7022ffde12438",
    "tokenId": "93",
    "standard": "erc721",
    "agentUri": null,
    "agentWallet": null,
    "metadata": [],
    "collectionId": "1-0x9eb6e2025b64f340691e424b7fe7022ffde12438",
    "createdAt": { "blockNumber": "25088233", "timestamp": "1778699687", "txHash": "0x66c8..." },
    "updatedAt": { "blockNumber": "25088233", "timestamp": "1778699687", "txHash": "0x66c8..." }
  }
}
```

## Example: agent list response

```
GET /api/agents?chainId=1&counterfactual=false&limit=2
```

```json
{
  "items": [
    {
      "counterfactual": false,
      "chainId": 1,
      "agentId": "32340",
      "registrationHash": null,
      "tokenContract": "0x9eb6e2025b64f340691e424b7fe7022ffde12438",
      "tokenId": "93",
      "standard": "erc721",
      "agentUri": null,
      "agentWallet": null,
      "metadata": [],
      "collectionId": "1-0x9eb6e2025b64f340691e424b7fe7022ffde12438",
      "createdAt": { "blockNumber": "25088233", "timestamp": "1778699687", "txHash": "0x66c8..." },
      "updatedAt": { "blockNumber": "25088233", "timestamp": "1778699687", "txHash": "0x66c8..." }
    }
  ],
  "pageInfo": { "nextCursor": "eyJpZCI6IjEtMzIzNDAifQ", "hasNextPage": true }
}
```

Metadata entries are `{ "key", "valueHex", "valueText" }` where `valueText` is
null when the bytes are not valid UTF-8.

## The counterfactual upgrade path

A counterfactual record's data — `agentUri`, `metadata`, `agentWallet`,
`tokenUri` — stays fully queryable through the API while `counterfactual` is
true. A registration/upgrade flow reads it from the API and submits it on-chain
during real registration. After the real registration is indexed the record's
`counterfactual` flips to false and shows the real data.

## Polling after a transaction

`/api/events` is the post-write signal. Start with the narrowest coordinates:

```
GET /api/events?chainId=8453&txHash=0x...                    # right after submitting
GET /api/events?chainId=8453&tokenContract=0x...&tokenId=7   # by NFT
```

Poll roughly every 3s for the first minute, then back off. Treat an event match
as a signal to refetch `by-token` — do not build state from event `details`
alone.

## Known caveat

`GET /api/agents/by-metadata` requires a `key`; with no metadata rows indexed
it can currently return HTTP 500. Do not make a UI hard-depend on metadata
search until the indexer is patched.
