# Token scans

Source: https://www.scanner.net/docs/api/reference/token-scans

> Detect an address type, scan a token's holders or traders for bundles, compare several tokens, and read cached token scans, on Solana and Robinhood Chain.

A token scan looks at a token's holders (or, on Solana, its early traders), groups wallets that were funded together or bought together into bundles, and follows proxy wallets and profit collectors. It is the same scan as the token scan in the dashboard.

13 routes: 7 under `/contracts` for Solana and 6 under `/rh/contracts` for Robinhood Chain. Every path below is served under `https://api.scanner.net`, and every request carries the key header, so those two are not repeated per route. Shared errors, limits and streaming are in [Requests and errors](/docs/api/reference/conventions).

A token scan costs 1 scan. A cached result is free, and so is joining a scan of the same token that is already running. A scan that fails is refunded. The detect, history, cached and rebuild routes are free.

Solana token detect [#solana-token-detect]

```http
GET /contracts/{address}/detect
```

Says whether an address is a token, a program or a wallet. Free. For several addresses or either chain, use [`/detect`](/docs/api/reference/lookups#detect-address).

| Parameter | In   | Type   | Required | Notes           |
| --------- | ---- | ------ | -------- | --------------- |
| `address` | path | string | yes      | Solana address. |

```bash
curl "https://api.scanner.net/contracts/MINT_ADDRESS/detect" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Response:** `{ isToken, isContract, isWallet }` booleans, plus `chain`, `family`, `address`, `exists`, and `owner`, `executable` and `balance` when known. An address that can't be detected, including an invalid one, returns all three flags `false`.

| Status | Code          | When                        |
| ------ | ------------- | --------------------------- |
| 502    | `SCAN_FAILED` | The lookup failed upstream. |

Solana token scan [#solana-token-scan]

```http
POST /contracts/{mint}/scan
```

Scans a Solana token's holders and returns the result when it finishes. No body.

| Parameter | In    | Type   | Required | Notes                                                                    |
| --------- | ----- | ------ | -------- | ------------------------------------------------------------------------ |
| `mint`    | path  | string | yes      | Token mint address.                                                      |
| `force`   | query | string | no       | `true` skips the cache and scans again. Read from the query string only. |

```bash
curl -X POST "https://api.scanner.net/contracts/MINT_ADDRESS/scan" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Response:** a `ContractScanResult`, with no wrapper. Key fields:

* `mint` and `tokenProfile`: the token and its metadata.
* `holders`: the holders scanned, with their bundle and funding flags.
* `bundleGroups` and `totalBundles`: groups of wallets that act together.
* `transferLinks`, `fundingSources`, `profitCollectors`: how the holders are connected.
* `proxyChains` and `staggeredScoring`, when found.
* `bundleSnapshotId`: pass it to [Rebuild bundles](#solana-rebuild-bundles).
* `scanComplete`: `false` when the result is partial.

**Cost:** a cached scan less than an hour old is returned free. Otherwise 1 scan.

| Status | Code                   | When                                                         |
| ------ | ---------------------- | ------------------------------------------------------------ |
| 400    | `INVALID_MINT`         | Not a valid Solana address.                                  |
| 402    | `INSUFFICIENT_CREDITS` | Not enough scans left.                                       |
| 503    | `INTERNAL_ERROR`       | Token data is temporarily unavailable. The scan is refunded. |
| 504    | `TIMEOUT`              | The scan ran out of time. The scan is refunded.              |

Solana token scan stream [#solana-token-scan-stream]

```http
GET /contracts/{mint}/scan/stream
```

A Solana token scan as [server-sent events](/docs/api/reference/conventions#streaming-routes). Unlike the JSON route, it can scan traders as well as holders.

| Parameter | In    | Type   | Required | Notes                                   |
| --------- | ----- | ------ | -------- | --------------------------------------- |
| `mint`    | path  | string | yes      | Token mint address.                     |
| `mode`    | query | string | no       | `holders` (default) or `traders`.       |
| `force`   | query | string | no       | `true` skips the cache and scans again. |

```bash
curl -N "https://api.scanner.net/contracts/MINT_ADDRESS/scan/stream?mode=traders" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Events:** on a cache hit, `cache_hit` (`scannedAt`, `isStale`, `history`) and then `complete`. On a live scan, `status`, `token_metadata`, `holders`, `entry_analysis`, `transfer_analysis`, `funding_analysis`, `staggered_analysis`, `proxy_detection`, `bundle_detection`, `bundle_groups`, `proxy_chains`, `staggered_scoring`, `heartbeat` every 15 seconds, then `complete` with the `ContractScanResult`. A failure sends `error` with code `TIMEOUT` or `INTERNAL_ERROR`.

**Cost:** any cached scan is returned free, even one over an hour old (`isStale: true`). Pass `force=true` for a new scan at 1 scan. A failed scan is refunded, unless the connection was closed after results had already arrived.

| Status | Code                   | When                        |
| ------ | ---------------------- | --------------------------- |
| 400    | `INVALID_MINT`         | Not a valid Solana address. |
| 402    | `INSUFFICIENT_CREDITS` | Not enough scans left.      |

Solana token scan history [#solana-token-scan-history]

```http
GET /contracts/{mint}/scan/history
```

Up to 3 recent cached scans of a token. Free.

| Parameter | In    | Type   | Required | Notes                             |
| --------- | ----- | ------ | -------- | --------------------------------- |
| `mint`    | path  | string | yes      | Token mint address.               |
| `mode`    | query | string | no       | `holders` (default) or `traders`. |

```bash
curl "https://api.scanner.net/contracts/MINT_ADDRESS/scan/history?mode=holders" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Response:** `{ "history": [{ "id", "scannedAt", "isStale" }] }`. Pass an `id` to [Cached token scan](#solana-cached-token-scan). An empty list means there is nothing cached, or the lookup failed; this route doesn't return errors.

Solana cached token scan [#solana-cached-token-scan]

```http
GET /contracts/scan/cache/{id}
```

One cached token scan by its history `id`. Free.

| Parameter | In   | Type   | Required | Notes                                                    |
| --------- | ---- | ------ | -------- | -------------------------------------------------------- |
| `id`      | path | string | yes      | An `id` from [scan history](#solana-token-scan-history). |

```bash
curl "https://api.scanner.net/contracts/scan/cache/CACHE_ID" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Response:** `{ scanResult, scannedAt, isStale }`, where `scanResult` is a `ContractScanResult`.

| Status | When                                                                                                 |
| ------ | ---------------------------------------------------------------------------------------------------- |
| 404    | No cached scan with this `id`. The body is `{ "error": "Cached scan not found" }`, without a `code`. |

Solana rebuild bundles [#solana-rebuild-bundles]

```http
POST /contracts/{mint}/bundles/rebuild
```

Regroups the bundles of an earlier scan with some wallets hidden, for example a known exchange or a wallet you've already cleared. Free. It doesn't rescan.

| Parameter    | In   | Type      | Required | Notes                                                     |
| ------------ | ---- | --------- | -------- | --------------------------------------------------------- |
| `mint`       | path | string    | yes      | Token mint address.                                       |
| `mode`       | body | string    | yes      | `holders` or `traders`, matching the scan.                |
| `snapshotId` | body | string    | yes      | `bundleSnapshotId` from the scan result.                  |
| `hidden`     | body | string\[] | yes      | Solana addresses to leave out, at most 100. Can be empty. |

```bash
curl -X POST "https://api.scanner.net/contracts/MINT_ADDRESS/bundles/rebuild" \
  -H "x-api-key: $SCANNER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"holders","snapshotId":"SNAPSHOT_ID","hidden":["WALLET_ADDRESS"]}'
```

**Response:** `{ "scanResult": { ... } }`, the `ContractScanResult` with bundles regrouped.

| Status | Code                         | When                                                                                    |
| ------ | ---------------------------- | --------------------------------------------------------------------------------------- |
| 400    | `INVALID_MINT`               | Not a valid Solana address.                                                             |
| 400    | `INVALID_INPUT`              | Bad `mode`, `snapshotId` or `hidden`, or the snapshot belongs to another token or mode. |
| 410    | `BUNDLE_SNAPSHOT_MISSING`    | The snapshot has expired. Run the scan again.                                           |
| 503    | `BUNDLE_REBUILD_UNAVAILABLE` | Rebuilding is briefly unavailable.                                                      |

Multi-token scan [#multi-token-scan]

```http
POST /contracts/multi/scan/stream
```

Scans 2 to 5 Solana tokens, holders and traders for each, and compares them: wallets they share, bundles that repeat, shared funding sources and profit collectors, and transfers between them. Answers with [server-sent events](/docs/api/reference/conventions#streaming-routes).

| Parameter | In   | Type      | Required | Notes                                                           |
| --------- | ---- | --------- | -------- | --------------------------------------------------------------- |
| `mints`   | body | string\[] | yes      | 2 to 5 different token mint addresses.                          |
| `force`   | body | boolean   | no       | `true` scans every token again instead of using cached results. |

```bash
curl -N -X POST "https://api.scanner.net/contracts/multi/scan/stream" \
  -H "x-api-key: $SCANNER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mints":["MINT_ADDRESS_1","MINT_ADDRESS_2"]}'
```

**Events:** `preflight`, `multi_start` (`mints`, `creditsRequired`), then per token `target_start`, `mode_start`, `mode_cache_hit` or `mode_progress`, `mode_complete` and `target_complete`, then `cross_analysis`. `complete` holds a `MultiContractScanResult`: `mints`, `targets`, `commonWallets`, `repeatedBundles`, `sharedFundingSources`, `sharedProfitCollectors`, `crossTransfers` and a `summary` with `creditsRequired`, `creditsCharged` and a `verdict` (`high`, `medium`, `low` or `none`).

**Cost:** 1 scan for each token that has no cached result, charged after the stream opens. Any failure refunds the whole charge.

| Status | Code            | When                                |
| ------ | --------------- | ----------------------------------- |
| 400    | `INVALID_INPUT` | Fewer than 2 or more than 5 tokens. |
| 400    | `INVALID_MINT`  | An address isn't valid.             |

After the stream opens, problems arrive as an `error` event: `INVALID_MINT` when an address isn't a token, `INSUFFICIENT_CREDITS` (with `credits` and `required`) when there aren't enough scans, and `INTERNAL_ERROR`. A lack of scans is reported this way, not as an HTTP `402`.

Robinhood Chain token detect [#robinhood-chain-token-detect]

```http
GET /rh/contracts/{address}/detect
```

Says whether a Robinhood Chain address is a token, a contract or a wallet. Free.

| Parameter | In   | Type   | Required | Notes         |
| --------- | ---- | ------ | -------- | ------------- |
| `address` | path | string | yes      | `0x` address. |

```bash
curl "https://api.scanner.net/rh/contracts/0xTOKEN_ADDRESS/detect" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Response:** `{ isToken, isContract, isWallet }`, plus `contractName` and `supply` when known.

| Status | Code            | When                        |
| ------ | --------------- | --------------------------- |
| 400    | `INVALID_INPUT` | Not a `0x` address.         |
| 502    | `SCAN_FAILED`   | The lookup failed upstream. |

Robinhood Chain token scan [#robinhood-chain-token-scan]

```http
POST /rh/contracts/{address}/scan
```

Scans a Robinhood Chain token's holders and returns the result when it finishes.

| Parameter | In            | Type    | Required | Notes                                                                 |
| --------- | ------------- | ------- | -------- | --------------------------------------------------------------------- |
| `address` | path          | string  | yes      | Token `0x` address.                                                   |
| `force`   | query or body | boolean | no       | `true` (query `force=true` or body `{"force":true}`) skips the cache. |

```bash
curl -X POST "https://api.scanner.net/rh/contracts/0xTOKEN_ADDRESS/scan" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Response:** a `RobinhoodContractScanResult`, with no wrapper. Key fields: `address`, `tokenProfile`, `holders`, `bundleGroups`, `totalBundles`, `transferLinks`, `fundingSources`, `profitCollectors`, `scanComplete`, and `degraded` when some data was missing.

**Cost:** a cached scan is returned free unless `force` is `true`. Otherwise 1 scan.

| Status | Code                   | When                                                         |
| ------ | ---------------------- | ------------------------------------------------------------ |
| 400    | `INVALID_MINT`         | Not a `0x` address.                                          |
| 402    | `INSUFFICIENT_CREDITS` | Not enough scans left.                                       |
| 503    | `INTERNAL_ERROR`       | Token data is temporarily unavailable. The scan is refunded. |
| 504    | `TIMEOUT`              | The scan ran out of time. The scan is refunded.              |

Robinhood Chain token scan stream [#robinhood-chain-token-scan-stream]

```http
GET /rh/contracts/{address}/scan/stream
```

A Robinhood Chain token scan as [server-sent events](/docs/api/reference/conventions#streaming-routes). Holders only.

| Parameter | In    | Type   | Required | Notes                                                                                          |
| --------- | ----- | ------ | -------- | ---------------------------------------------------------------------------------------------- |
| `address` | path  | string | yes      | Token `0x` address.                                                                            |
| `force`   | query | string | no       | `true` skips the cache.                                                                        |
| `reuse`   | query | string | no       | `true` also replays a cached result that was degraded or made by an older version of the scan. |

```bash
curl -N "https://api.scanner.net/rh/contracts/0xTOKEN_ADDRESS/scan/stream" \
  -H "x-api-key: $SCANNER_API_KEY"
```

**Events:** the same pattern as the [Solana stream](#solana-token-scan-stream): `cache_hit` then `complete` for a cached result, otherwise progress events, `heartbeat`, and `complete` with the `RobinhoodContractScanResult`.

**Cost:** a complete cached scan is free, even when stale. Otherwise 1 scan, refunded on failure.

| Status | Code                   | When                   |
| ------ | ---------------------- | ---------------------- |
| 400    | `INVALID_MINT`         | Not a `0x` address.    |
| 402    | `INSUFFICIENT_CREDITS` | Not enough scans left. |

Robinhood Chain token scan history [#robinhood-chain-token-scan-history]

```http
GET /rh/contracts/{address}/scan/history
```

Recent cached holder scans of a token. Free. The response has the same shape as the [Solana history](#solana-token-scan-history), and an empty list on any problem.

```bash
curl "https://api.scanner.net/rh/contracts/0xTOKEN_ADDRESS/scan/history" \
  -H "x-api-key: $SCANNER_API_KEY"
```

Robinhood Chain cached token scan [#robinhood-chain-cached-token-scan]

```http
GET /rh/contracts/scan/cache/{id}
```

One cached Robinhood Chain token scan by its history `id`. Free. Returns `{ scanResult, scannedAt, isStale }`, or `404` without a `code` when there is no such scan.

```bash
curl "https://api.scanner.net/rh/contracts/scan/cache/CACHE_ID" \
  -H "x-api-key: $SCANNER_API_KEY"
```

Robinhood Chain multi-token scan [#robinhood-chain-multi-token-scan]

```http
POST /rh/contracts/multi/scan/stream
```

Compares 2 to 5 Robinhood Chain tokens, like the [Solana multi-token scan](#multi-token-scan). Answers with server-sent events.

| Parameter   | In   | Type      | Required | Notes                                                               |
| ----------- | ---- | --------- | -------- | ------------------------------------------------------------------- |
| `addresses` | body | string\[] | yes      | 2 to 5 token `0x` addresses. The field is `addresses`, not `mints`. |
| `force`     | body | boolean   | no       | `true` scans every token again.                                     |

```bash
curl -N -X POST "https://api.scanner.net/rh/contracts/multi/scan/stream" \
  -H "x-api-key: $SCANNER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"addresses":["0xTOKEN_1","0xTOKEN_2"]}'
```

**Events:** `multi_start`, then per token `target_start`, `target_cache_hit` or `target_progress`, and `target_complete`, then `cross_analysis`, `heartbeat`, and `complete` with `addresses`, `targets` and a `summary`.

**Cost:** 1 scan for each token without a cached result, charged before the stream opens and refunded in full on failure.

| Status | Code                   | When                                                         |
| ------ | ---------------------- | ------------------------------------------------------------ |
| 400    | `INVALID_INPUT`        | Fewer than 2 or more than 5 addresses.                       |
| 400    | `INVALID_MINT`         | An address isn't valid.                                      |
| 402    | `INSUFFICIENT_CREDITS` | Not enough scans left. Unlike Solana, this is an HTTP `402`. |
