API documentation
The BananaMaps API returns the holder intelligence behind the app — cluster graphs, ranked holders, wallet labels and risk scores — as JSON. Everything is served from the cached snapshot of a token, so responses are fast and identical to what the app shows at that moment.
| Base URL | https://www.bananamaps.io/v1 |
| Format | JSON. Success: { data, meta }. Error: { error: { code, message, status, docs } }. |
| Methods | GET only (plus OPTIONS for CORS preflight). |
| CORS | Open (Access-Control-Allow-Origin: *); rate-limit and quota headers are exposed to browsers. |
| Versioning | The version is in the path. Fields may be added to v1 responses; none will be removed or renamed without a v2. |
| Chains | eth Ethereum · bsc BNB Smart Chain · base Base · sol Solana · robinhood Robinhood Chain |
Authentication
Every call carries your API key as a Bearer token. Keys start with bm_live_ and are created in the dashboard; a verified free account gets its first key immediately.
Authorization: Bearer bm_live_YOUR_KEYrevoked_key — and create a new one. You can hold up to five active keys.Quickstart
Fetch the top holders of PEPE on Ethereum. The same shape works for any token that has a snapshot.
curl "https://www.bananamaps.io/v1/holders/eth/0x6982508145454ce325ddbe47a25d4ec3d2311933" \
-H "Authorization: Bearer bm_live_YOUR_KEY"const res = await fetch(
"https://www.bananamaps.io/v1/holders/eth/0x6982508145454ce325ddbe47a25d4ec3d2311933",
{ headers: { Authorization: "Bearer " + process.env.BANANAMAPS_KEY } }
);
if (!res.ok) {
const { error } = await res.json();
throw new Error(error.code + ": " + error.message);
}
const { data, meta } = await res.json();
console.log(data.token.symbol, data.holders.length, "holders; snapshot", meta.computedAt);Validation happens before metering: a 400 for a bad chain or address does not count against your quota. Every authorized response, including errors, carries the rate-limit and quota headers.
Graph
The full cluster graph of a token: the mapped holders and the counterparties that connect them (nodes), the transfers observed between them (edges, indexes into nodes) and the connected-wallet clusters. This is the payload the app renders.
| Parameter | In | Description |
|---|---|---|
chain | path | One of eth, bsc, base, sol, robinhood. |
address | path | Token contract address. EVM: 0x + 40 hex characters (case does not matter; responses lowercase it). Solana: base58 mint, 32–44 characters, case-sensitive. |
Response fields
| Field | Type | Description |
|---|---|---|
data.token | object | Token summary — see token object. |
data.nodes[] | array | address, kind (holder | counterparty), rank (1-based among holders, null for counterparties), pct of supply, balance, usdValue, label, labelType, cluster id or null, degree (edges touching the node). |
data.edges[] | array | from and to are indexes into nodes; count is the number of transfers in that direction. |
data.clusters[] | array | id, members (node indexes), combined pct of supply. |
meta | object | chain, address, computedAt, snapshotAgeSeconds. |
Example
curl "https://www.bananamaps.io/v1/graph/eth/0x6982508145454ce325ddbe47a25d4ec3d2311933" \
-H "Authorization: Bearer bm_live_YOUR_KEY"{
"data": {
"token": {
"chain": "eth",
"address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
"name": "Pepe",
"symbol": "PEPE",
"logo": "https://dd.dexscreener.com/ds-data/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933.png",
"price": 0.0000112,
"priceChange24h": -3.4,
"marketCap": 4712000000,
"volume24h": 812000000,
"totalHolders": 418223,
"holdersMapped": 150,
"computedAt": "2026-10-27T09:14:03.000Z"
},
"nodes": [
{
"address": "0xf977814e90da44bfa03b6295a0616a897441acec",
"kind": "holder",
"rank": 1,
"pct": 6.84,
"balance": 28775196000000,
"usdValue": 322282195,
"label": "Binance 8",
"labelType": "cex",
"cluster": null,
"degree": 1
},
{
"address": "0x3a4d2a7e6c1f0b9d8e5f4a3b2c1d0e9f8a7b6c5d",
"kind": "holder",
"rank": 2,
"pct": 2.41,
"balance": 10138629000000,
"usdValue": 113552645,
"label": null,
"labelType": null,
"cluster": 3,
"degree": 3
},
{
"address": "0x9e2c7b1a0f3d4e5c6b7a8d9e0f1a2b3c4d5e6f7a",
"kind": "holder",
"rank": 3,
"pct": 1.97,
"balance": 8287593000000,
"usdValue": 92821042,
"label": null,
"labelType": null,
"cluster": 3,
"degree": 2
},
{
"address": "0x7c1e5b9a2d4f6e8c0b3a5d7f9e1c3b5a7d9f1e3c",
"kind": "counterparty",
"rank": null,
"pct": 0,
"balance": null,
"usdValue": null,
"label": null,
"labelType": null,
"cluster": 3,
"degree": 2
}
],
"edges": [
{
"from": 3,
"to": 1,
"count": 4
},
{
"from": 3,
"to": 2,
"count": 2
},
{
"from": 1,
"to": 2,
"count": 1
},
{
"from": 1,
"to": 0,
"count": 1
}
],
"clusters": [
{
"id": 3,
"members": [
1,
2,
3
],
"pct": 4.38
}
]
},
"meta": {
"chain": "eth",
"address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
"computedAt": "2026-10-27T09:14:03.000Z",
"snapshotAgeSeconds": 412
}
}Holders
The top 150 holders of a token, ranked by balance, with their share of supply, USD value, label and cluster.
| Parameter | In | Description |
|---|---|---|
chain | path | One of eth, bsc, base, sol, robinhood. |
address | path | Token contract address. EVM: 0x + 40 hex characters (case does not matter; responses lowercase it). Solana: base58 mint, 32–44 characters, case-sensitive. |
Response fields
| Field | Type | Description |
|---|---|---|
data.token | object | Token summary — see token object. |
data.holders[] | array | Up to 150 entries: rank, address, pct (percent of total supply), balance (token units), usdValue, label, labelType (cex | dex | burn | contract | null), cluster id or null. |
meta | object | chain, address, computedAt, snapshotAgeSeconds. |
Example
curl "https://www.bananamaps.io/v1/holders/eth/0x6982508145454ce325ddbe47a25d4ec3d2311933" \
-H "Authorization: Bearer bm_live_YOUR_KEY"{
"data": {
"token": {
"chain": "eth",
"address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
"name": "Pepe",
"symbol": "PEPE",
"logo": "https://dd.dexscreener.com/ds-data/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933.png",
"price": 0.0000112,
"priceChange24h": -3.4,
"marketCap": 4712000000,
"volume24h": 812000000,
"totalHolders": 418223,
"holdersMapped": 150,
"computedAt": "2026-10-27T09:14:03.000Z"
},
"holders": [
{
"rank": 1,
"address": "0xf977814e90da44bfa03b6295a0616a897441acec",
"pct": 6.84,
"balance": 28775196000000,
"usdValue": 322282195,
"label": "Binance 8",
"labelType": "cex",
"cluster": null
},
{
"rank": 2,
"address": "0x3a4d2a7e6c1f0b9d8e5f4a3b2c1d0e9f8a7b6c5d",
"pct": 2.41,
"balance": 10138629000000,
"usdValue": 113552645,
"label": null,
"labelType": null,
"cluster": 3
},
{
"rank": 3,
"address": "0x9e2c7b1a0f3d4e5c6b7a8d9e0f1a2b3c4d5e6f7a",
"pct": 1.97,
"balance": 8287593000000,
"usdValue": 92821042,
"label": null,
"labelType": null,
"cluster": 3
}
]
},
"meta": {
"chain": "eth",
"address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
"computedAt": "2026-10-27T09:14:03.000Z",
"snapshotAgeSeconds": 412
}
}The token object
Graph, holders and risk responses share data.token:
| Field | Description |
|---|---|
chain, address | As requested; EVM addresses lowercased. |
name, symbol, logo | Token metadata; logo may be null. |
price, priceChange24h | USD price and 24 h change in percent at snapshot time; null when unknown. |
marketCap, volume24h | USD; null when unknown. |
totalHolders | On-chain holder count where the chain exposes it (EVM); null otherwise. |
holdersMapped | How many holders are in the snapshot (up to 150). |
computedAt | ISO timestamp of the snapshot. |
Labels
The label for one wallet address: centralized exchange, DEX or AMM contract, burn address or other contract. Labels come from our curated list first, then from labels resolved while building maps, then from address patterns (burn addresses).
| Parameter | In | Description |
|---|---|---|
chain | path | One of eth, bsc, base, sol, robinhood. |
wallet | path | The wallet address to label (same address rules as tokens). Does not need a snapshot. |
Response fields
| Field | Type | Description |
|---|---|---|
data.address | string | The wallet, normalized. |
data.label | string | null | Human label such as Binance 8 or Burn Address. |
data.labelType | enum | null | cex, dex, burn, contract, or null when unlabeled. |
data.source | enum | null | curated (our list), resolved (learned while mapping), pattern (address shape), or null. |
meta | object | chain, address. |
Example
curl "https://www.bananamaps.io/v1/labels/eth/0xf977814e90da44bfa03b6295a0616a897441acec" \
-H "Authorization: Bearer bm_live_YOUR_KEY"{
"data": {
"address": "0xf977814e90da44bfa03b6295a0616a897441acec",
"label": "Binance 8",
"labelType": "cex",
"source": "curated"
},
"meta": {
"chain": "eth",
"address": "0xf977814e90da44bfa03b6295a0616a897441acec"
}
}Risk
A 0–100 score of how concentrated and clustered a token's supply is, with the factors behind it and a summary of each cluster. Higher is healthier. Exchange, DEX and burn wallets are excluded from the concentration factors; labeled contracts (vesting, treasuries) count as holders, as in the app. clusterCount counts every cluster; meaningfulClusterCount is the number the Cluster Activity factor scores (clusters with at least two top holders).
| Parameter | In | Description |
|---|---|---|
chain | path | One of eth, bsc, base, sol, robinhood. |
address | path | Token contract address. EVM: 0x + 40 hex characters (case does not matter; responses lowercase it). Solana: base58 mint, 32–44 characters, case-sensitive. |
Response fields
| Field | Type | Description |
|---|---|---|
data.token | object | Token summary — see token object. |
data.risk.score | number | 0–100. ≥ 80 Low Risk · 50–79 Medium · 25–49 High · < 25 Very High. |
data.risk.label | string | The band name for score. |
data.risk.factors[] | array | name, score (0–100), weight (percent of the total), detail (human explanation). Weights sum to 100. |
data.risk.top10Pct | number | Share of supply held by the ten largest mapped holders, exchanges included. |
data.risk.clusterCount | number | Number of clusters in the snapshot. |
data.risk.clusters[] | array | id, wallets (all members), holders (members in the top 150), combined pct, members (addresses). |
meta | object | chain, address, computedAt, snapshotAgeSeconds. |
Example
curl "https://www.bananamaps.io/v1/risk/eth/0x6982508145454ce325ddbe47a25d4ec3d2311933" \
-H "Authorization: Bearer bm_live_YOUR_KEY"{
"data": {
"token": {
"chain": "eth",
"address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
"name": "Pepe",
"symbol": "PEPE",
"logo": "https://dd.dexscreener.com/ds-data/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933.png",
"price": 0.0000112,
"priceChange24h": -3.4,
"marketCap": 4712000000,
"volume24h": 812000000,
"totalHolders": 418223,
"holdersMapped": 150,
"computedAt": "2026-10-27T09:14:03.000Z"
},
"risk": {
"score": 64,
"label": "Medium Risk",
"factors": [
{
"name": "Top Holder",
"score": 85,
"weight": 25,
"detail": "2.4% held by #1 non-exchange wallet"
},
{
"name": "Top 10 Concentration",
"score": 75,
"weight": 25,
"detail": "18.6% held by top 10 (excl. 4 exchanges)"
},
{
"name": "Cluster Activity",
"score": 30,
"weight": 25,
"detail": "4 clusters detected"
},
{
"name": "Holder Count",
"score": 75,
"weight": 15,
"detail": "146 holders mapped"
},
{
"name": "Distribution",
"score": 50,
"weight": 10,
"detail": "Top 5 hold 13.9%"
}
],
"top10Pct": 25.42,
"clusterCount": 4,
"clusters": [
{
"id": 3,
"wallets": 3,
"holders": 2,
"pct": 4.38,
"members": [
"0x3a4d2a7e6c1f0b9d8e5f4a3b2c1d0e9f8a7b6c5d",
"0x9e2c7b1a0f3d4e5c6b7a8d9e0f1a2b3c4d5e6f7a",
"0x7c1e5b9a2d4f6e8c0b3a5d7f9e1c3b5a7d9f1e3c"
]
}
]
}
},
"meta": {
"chain": "eth",
"address": "0x6982508145454ce325ddbe47a25d4ec3d2311933",
"computedAt": "2026-10-27T09:14:03.000Z",
"snapshotAgeSeconds": 412
}
}Errors
Errors use the HTTP status and a stable code. The message is written for humans and may change; branch on code.
| Code | Status | Meaning |
|---|---|---|
bad_request | 400 | Unknown chain, or the address is not a valid address for that chain. Not metered. |
unauthorized | 401 | No Authorization header, or it is not "Bearer <key>". |
invalid_key | 401 | The key is malformed or does not exist. |
revoked_key | 401 | The key was revoked in the dashboard. Create a new one. |
account_canceled | 403 | The account is closed. |
not_found | 404 | Unknown route. |
not_indexed | 404 | No snapshot exists for this token yet. The message carries the app link that builds one. |
quota_exceeded | 429 | The monthly quota of your plan is used up. Retry-After points at the reset; upgrade for more. |
rate_limited | 429 | Too many calls this minute. Retry-After says how many seconds to wait. |
server_error | 500 | Something failed on our side. Safe to retry. |
Example
{
"error": {
"code": "quota_exceeded",
"message": "Monthly quota of 1,000 calls reached on the Free plan. Upgrade at https://www.bananamaps.io/developers/dashboard for a higher quota, or wait for the reset on Sun, 01 Nov 2026 00:00:00 GMT.",
"status": 429,
"docs": "https://www.bananamaps.io/developers/docs"
}
}{
"error": {
"code": "not_indexed",
"message": "No snapshot for this token yet. Open https://www.bananamaps.io/app?token=0x6982508145454ce325ddbe47a25d4ec3d2311933&chain=eth once to build it, then call again.",
"status": 404,
"docs": "https://www.bananamaps.io/developers/docs"
}
}Rate limits & quotas
Each plan has a per-minute rate limit and a monthly quota. Both are hard limits: at the quota you receive 429 quota_exceeded until the period resets or you upgrade — there is no overage billing. Every authorized call counts as one, whichever endpoint it hits, including a 404 not_indexed answer. Requests rejected before authentication (400 for a bad chain or address, 401) are not counted.
| Plan | Calls / month | Calls / minute | Price |
|---|---|---|---|
| Free | 1,000 | 30 | Free |
| Starter | 50,000 | 120 | $49/mo · $490/yr |
| Pro | 500,000 | 600 | $199/mo · $1,990/yr |
Headers
Sent on every authorized response, success or error:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Calls allowed per minute on your plan. |
X-RateLimit-Remaining | Calls left in the current minute. |
X-RateLimit-Reset | Unix time (seconds) when the minute window resets. |
X-Quota-Limit | Calls allowed this month on your plan. |
X-Quota-Used | Calls counted this month, including this one. |
X-Quota-Reset | ISO timestamp of the start of the next monthly period. |
Retry-After | Only on 429: seconds until you may call again. |
Monthly periods are calendar months in UTC. Usage per key and the reset date are on your dashboard.
Snapshots
BananaMaps builds a holder map by pulling a token's top 150 holders and replaying the transfers between them. That build takes a minute or two, so results are stored as a snapshot and refreshed in the background. The API serves snapshots; it never starts a build.
meta.computedAtis when the snapshot was computed andmeta.snapshotAgeSecondshow old it is at response time. Popular tokens refresh often; quiet ones less so.- If no snapshot exists you get
404 not_indexed. Open the token in the app once (the link is in the message) — the build runs, and the API answers from then on. - The
labelsendpoint does not need a snapshot; it answers for any address. - Responses are not cached by the API (
Cache-Control: no-store), so you always see the newest snapshot; cache on your side as you see fit.
Embed quickstart
BananaMaps Embed puts the live holder map on your own pages. Register your site in the dashboard to get a partner id, then paste this snippet where the map should appear:
<iframe
src="https://www.bananamaps.io/embed?token=TOKEN_ADDRESS&chain=eth&pid=YOUR_PID"
width="100%"
height="600"
style="border:0;border-radius:12px"
loading="lazy"
title="BananaMaps Embed"
></iframe>pid. Any other domain must pass a valid pid whose domain list matches the page, or visitors see a “get a free Embed key” message in place of the map. The frame is responsive: set width and height to fit your layout; 600 px tall works well.Embed parameters
| Parameter | Required | Description |
|---|---|---|
token | required | Token contract address (EVM 0x…, or a Solana mint). |
chain | required | One of eth, bsc, base, sol, robinhood. |
pid | required* | Your partner id from the dashboard (bme_…). *Not needed on first-party domains. |
badge | Partner plan | badge=0 hides the "Powered by BananaMaps" badge. Ignored on the Free plan. |
accent | Partner plan | Six-digit hex without #, e.g. accent=00C805. Replaces the gold accent in the map. Ignored on the Free plan. |
- Free plan: the “Powered by BananaMaps” badge and link are required and always shown;
badgeandaccentare ignored. - Partner plan:
badge=0andaccenttake effect. You can also save a default badge setting and accent per site in the dashboard, so the snippet stays short. - Domains: the id only works on the hostnames you listed (wildcards like
*.example.comallowed). Leave the list empty while testing and lock it down before launch. - Refresh: maps come from the same snapshots as the API. Partner sites get priority refresh for the tokens their visitors open.
- Metering: maps shown through BananaMaps Embed never count against your API quota.
Plans and pricing: BananaMaps Embed. Manage sites, badge and accent in the dashboard.