API v1
One GET. Every wallet.
The same engine behind scora.fun, as a free public JSON API. No key, no auth, CORS open. Build leaderboards, bots, Discord flexes, whatever.
Endpoint
GET/api/v1/wallet/{address}
address is any base58 Solana wallet address (a system account on the ed25519 curve). Program accounts, vaults and token mints are rejected with a 400.
Responses are cached ~5 minutes at the edge and ~10 minutes in the engine; X-Scora-Cache: HIT|MISS tells you which you got.
curl https://scora.fun/api/v1/wallet/9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWMFetch it
Plain fetch works from browsers and servers alike. The API sends Access-Control-Allow-Origin: * on every GET.
scora is null only for empty wallets (which also return 404), so a 200 always carries a real score.
const res = await fetch(
"https://scora.fun/api/v1/wallet/9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"
);
if (!res.ok) throw new Error((await res.json()).message);
const wallet = await res.json();
console.log(wallet.scora, wallet.tier); // 78 "Titanium"TypeScript
The response shape is stable and versioned (version: "1"). Fields are only ever added within v1, never removed or renamed.
Hold times can be floors: when holdDaysIsFloor is true, the wallet has held the token at least holdDays days. Render it as "90+d", like we do.
interface ScoraWallet {
version: "1";
address: string;
analyzedAt: string; // ISO 8601
coverage: number; // 0–1 data confidence
scora: number | null; // 0–100
tier: string | null; // Paper | Steel | Titanium | Diamond
kind: "scored" | "empty" | "fresh";
isEstimate: boolean;
subScores: {
conviction: number; vintage: number; portfolioHealth: number;
discipline: number; degenIndex: number; // each 0–10
} | null;
degen: { memePct: number; label: string };
wallet: {
ageDays: number | null; ageIsFloor: boolean; txSampled: number;
solBalance: number; activeWeekRatio: number | null;
swapsPerWeek: number | null;
};
tokens: Array<{
mint: string; symbol: string; name: string | null;
logoUri: string | null; amount: number; price: number | null;
usdValue: number; pctOfPortfolio: number;
holdDays: number | null; holdDaysIsFloor: boolean;
tokenAgeDays: number | null; alive: boolean;
isMajor: boolean; isStable: boolean;
}>;
totalUsd: number;
notes: string[];
}
const wallet: ScoraWallet = await fetch(
`https://scora.fun/api/v1/wallet/${address}`
).then((r) => r.json());Response schema
Generated at build time from the exact zod schema the API validates against, so the docs cannot drift from the implementation.
coverage (0–1) says how much of the wallet's history fit our per-request data budget. Anything the budget cut short is listed in notes as plain English.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"version": {
"type": "string",
"const": "1"
},
"address": {
"type": "string"
},
"analyzedAt": {
"type": "string"
},
"coverage": {
"type": "number"
},
"scora": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"kind": {
"type": "string",
"enum": [
"scored",
"empty",
"fresh"
]
},
"isEstimate": {
"type": "boolean"
},
"subScores": {
"anyOf": [
{
"type": "object",
"properties": {
"conviction": {
"type": "number"
},
"vintage": {
"type": "number"
},
"portfolioHealth": {
"type": "number"
},
"discipline": {
"type": "number"
},
"degenIndex": {
"type": "number"
}
},
"required": [
"conviction",
"vintage",
"portfolioHealth",
"discipline",
"degenIndex"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"degen": {
"type": "object",
"properties": {
"memePct": {
"type": "number"
},
"label": {
"type": "string"
}
},
"required": [
"memePct",
"label"
],
"additionalProperties": false
},
"wallet": {
"type": "object",
"properties": {
"ageDays": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"ageIsFloor": {
"type": "boolean"
},
"txSampled": {
"type": "number"
},
"solBalance": {
"type": "number"
},
"activeWeekRatio": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"swapsPerWeek": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
}
},
"required": [
"ageDays",
"ageIsFloor",
"txSampled",
"solBalance",
"activeWeekRatio",
"swapsPerWeek"
],
"additionalProperties": false
},
"tokens": {
"type": "array",
"items": {
"type": "object",
"properties": {
"mint": {
"type": "string"
},
"symbol": {
"type": "string"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"logoUri": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"amount": {
"type": "number"
},
"price": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"usdValue": {
"type": "number"
},
"pctOfPortfolio": {
"type": "number"
},
"holdDays": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"holdDaysIsFloor": {
"type": "boolean"
},
"tokenAgeDays": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"alive": {
"type": "boolean"
},
"isMajor": {
"type": "boolean"
},
"isStable": {
"type": "boolean"
}
},
"required": [
"mint",
"symbol",
"name",
"logoUri",
"amount",
"price",
"usdValue",
"pctOfPortfolio",
"holdDays",
"holdDaysIsFloor",
"tokenAgeDays",
"alive",
"isMajor",
"isStable"
],
"additionalProperties": false
}
},
"totalUsd": {
"type": "number"
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"version",
"address",
"analyzedAt",
"coverage",
"scora",
"tier",
"kind",
"isEstimate",
"subScores",
"degen",
"wallet",
"tokens",
"totalUsd",
"notes"
],
"additionalProperties": false
}Rate limits
10 fresh analyses per 5 minutes per IP, shared with the web UI. Cache hits don't count, so hammering a hot wallet is free.
On 429, honor Retry-After (seconds).
HTTP/1.1 429 Too Many Requests
Retry-After: 117
{
"error": "rate_limited",
"message": "Too many fresh analyses. Cached wallets still return instantly."
}Errors
Every error body is JSON with error (machine-readable) and message (human-readable).
Partial-data policy: when an upstream source hiccups mid-analysis we return a 200 with lower coverage and honest notes. A 503 only means we got nothing at all.
| Status | error | When |
|---|---|---|
| 400 | invalid_address | Not base58, off-curve, or a token mint / program account. |
| 404 | empty_wallet | No tokens, no SOL, no history. Nothing to score. |
| 429 | rate_limited | Too many fresh analyses from one IP. Honors Retry-After. |
| 503 | upstream_unavailable | Solana data sources are down. Retry shortly. |
Changelog
v1 · 2026-07 · initial release: wallet analysis endpoint.
Questions or ideas? Ping @scoradotfun or read the methodology.