The Radar engine,as a read only API.
The same mainnet reader and the same scoring model Radar uses, over plain GET requests. No key, no account, open CORS. A bot gets the answer a person gets on Radar.
One base, six endpoints.
Field names are a contract: fields get added, never renamed. Times are unix seconds, SOL amounts are numbers, and unknown values are null, never guessed. Everything is read from Solana mainnet.
Per IP, per minute.
Published limits. They refresh from GET /api/v1 when it answers.
Limits count per IP in a fixed one minute window. Score has its own budget because each call reads several wallets. Mainnet reads are cached for 5 minutes, so a repeat request inside that window returns fast and costs no RPC calls. Wallet and score can take 5 to 40 s uncached on the free RPC; set a client timeout of at least 60 s.
Send a real request from here.
Each card calls the live endpoint from your browser and shows the status, the time and the JSON it got back.
Name, version, the mainnet source in use, the limits and every endpoint. Cheap, so call it to check the API is up.
curl -s "https://useshader.xyz/api/v1"
One wallet: how much history was read, its first funder, its top counterparties, its visibility index and its findings.
Uncached: 5 to 40 s on the free RPCcurl -s "https://useshader.xyz/api/v1/wallet/86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY?limit=300"
Score up to 5 wallets together: the visibility index, clusters, every pair link with its evidence, and the findings. The two default wallets are linked, so the response holds a cluster.
Uncached: 5 to 40 s on the free RPCcurl -s "https://useshader.xyz/api/v1/score?addresses=86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY,FuGFeXc42djG26x8CmtgYqP7cCtbdAKnmFjiGMav3Kmk&limit=300"
Profile any address: its label, whether it is a program, a busy service or a plain wallet, and whether it forwards to an exchange like a deposit address. The default is a labelled Binance hot wallet.
curl -s "https://useshader.xyz/api/v1/address/5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9"
Every exchange hot wallet and shared infrastructure address Radar knows. Cached for an hour.
curl -s "https://useshader.xyz/api/v1/labels"
Token facts for $SHADER: venue, status and the contract address, which stays null until launch.
curl -s "https://useshader.xyz/api/v1/token"
Every field of /score.
| Field | Type | Meaning |
|---|---|---|
| addresses | string[] | The addresses you sent, without duplicates. |
| source | string | Which mainnet path answered: helius, rpc or publicnode. |
| history[address] | object | signaturesListed, transactionsRead, transactionsUnavailable, truncated (more history exists than limit), complete (false when the list came from a pruned source). |
| firstFunders[address] | object | status (found, none, too-deep, unavailable), address, label, signature, time in unix seconds, sol. Null when unknown. |
| visibilityIndex | number | 0 to 100. The highest wallet index in the request. |
| verdict | string | IN THE SHADE, PARTLY VISIBLE, VISIBLE or EXPOSED. |
| partialHistory | boolean | True when part of the history could not be read, so links may be missing. |
| wallets[] | object[] | address, visibilityIndex, cluster (position in clusters, or -1), exchange (name or null), reasons. |
| clusters | string[][] | Groups of wallets whose pair links are above 0.50. |
| pairs[] | object[] | a, b, link from 0 to 1, and evidence[]: kind, weight, via, detail, signatures. |
| findings[] | object[] | strength (STRONG, MEDIUM, WEAK), title, detail, fix, wallets, via, signatures. Strongest first. |
Evidence kinds on pairs are direct, gas, deposit, funder, counterparty, amount and sync. Findings can also be about one wallet: an exchange touch or round amounts. /wallet returns the same history, funder and findings for one address, plus firstSeen, lastSeen and its top 10 counterparties. The weights are explained in How scoring works.
Plain messages, real status codes.
Every error is JSON with an error field written for people. Retry 429 and 502; fix the request on 400 and 405.
| Status | When | Body |
|---|---|---|
| 400 | A parameter is missing or invalid. | { "error": "limit must be a number from 10 to 1000." } |
| 405 | Any method other than GET or OPTIONS. The API is read only. | { "error": "Read only API. Use GET." } |
| 429 | Over the per minute limit for your IP. | { "error": "Too many requests. Try again in 41 s.", "retryAfter": 41 } |
| 502 | Mainnet could not be read right now. | { "error": "Could not read mainnet right now. Try again in a minute." } |
Both requests are real. The first sends an address that is not base58, the second sends a POST.
Two things worth building.
Before a wallet goes anywhere you want kept apart from your others, read its verdict and its findings. Exits early on a rate limit with the wait in seconds.
const BASE = "https://useshader.xyz/api/v1";
// Check a wallet before you use it for something you want kept apart.
async function checkWallet(address) {
const res = await fetch(`${BASE}/wallet/${address}?limit=300`);
const body = await res.json();
if (res.status === 429) throw new Error(`Rate limited. Retry in ${body.retryAfter} s.`);
if (!res.ok) throw new Error(body.error);
const h = body.history;
const partial = !h.complete || h.transactionsUnavailable / Math.max(1, h.transactionsRead) > 0.1;
console.log(`${body.verdict}, index ${body.visibilityIndex}${partial ? ", partial history" : ""}`);
if (body.exchange) console.log(`Tied to ${body.exchange}. Assume it is linked to your ID.`);
for (const f of body.findings) console.log(`${f.strength} ${f.title}. FIX: ${f.fix}`);
return body.verdict === "IN THE SHADE";
}
const clean = await checkWallet("86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY");
console.log(clean ? "Looks clean" : "Read the findings first");Send up to 5 entries together. Any cluster in the response is a group the evidence ties to one person, with the link and the reasons. Waits out a 429 using retryAfter.
const BASE = "https://useshader.xyz/api/v1";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Flag entries that are linked to each other, for example in a giveaway bot.
// Clusters only form inside one request, so send the wallets you suspect together.
async function flagCluster(addresses) {
const list = addresses.slice(0, 5).join(",");
for (let tries = 0; tries < 3; tries++) {
const res = await fetch(`${BASE}/score?addresses=${list}&limit=300`);
const body = await res.json();
if (res.status === 429) { await sleep(body.retryAfter * 1000); continue; }
if (!res.ok) throw new Error(body.error);
return body.clusters.map((group) => ({
wallets: group,
link: Math.max(...body.pairs.filter((p) => group.includes(p.a) && group.includes(p.b)).map((p) => p.link)),
why: body.findings.filter((f) => f.wallets.length === 2 && group.includes(f.wallets[0])).map((f) => f.title),
}));
}
throw new Error("Still rate limited after 3 tries.");
}
const flagged = await flagCluster([
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
"FuGFeXc42djG26x8CmtgYqP7cCtbdAKnmFjiGMav3Kmk",
]);
for (const c of flagged) console.log(`Linked at ${c.link}: ${c.wallets.join(" and ")} (${c.why.join("; ")})`);