A field guide to Solana wallets
Dawnmoth reads any Solana address the way a naturalist reads a moth pinned to a board. It looks at what the chain already shows to everyone, measures it and names the species. No wallet connect, no keys, no signatures.
Every wallet ends up as one of nine species, from Larva nova, a wallet a few days old, to Balaena aurata, the whale. The reading takes a few seconds and runs entirely in your browser.
Quick start
The same module the site uses is served from the site itself. Import it and call identify.
import { identify } from "SITE/lib/dawnmoth.js";
const plate = await identify("A8A9GoxfmcjY2QAQ8tYS98SqDBF6hUFDmxAKBsLw29sL");
console.log(plate.species.latin); // "Robotica insomnis"
console.log(plate.specimen); // 9875
console.log(plate.notes); // plain-language field notes# The two public questions the module asks. Nothing else leaves the browser.
# 1. Last 1000 flights (signatures) and whether each one failed
curl -s https://solana-rpc.publicnode.com \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getSignaturesForAddress","params":["ADDRESS",{"limit":1000}]}'
# 2. SOL and every token jar the wallet owns
curl -s https://lite-api.jup.ag/ultra/v1/holdings/ADDRESS
# 3. Prices for up to 50 mints at a time
curl -s "https://lite-api.jup.ag/price/v3?ids=So11111111111111111111111111111111111111112"<div id="moth">Reading…</div>
<script type="module">
import { identify } from "SITE/lib/dawnmoth.js";
const p = await identify("ADDRESS");
document.getElementById("moth").innerHTML =
"<img src='SITE/assets/" + p.species.img + "' width='240'>" +
"<h3>" + p.species.latin + "</h3><p>" + p.species.lead + "</p>";
</script>Try it
This box runs identify() from lib/dawnmoth.js live, right here.

// waiting for an addressThe reading
One address in, one page of the field guide out. Five steps, all of them read-only.
- Check the address. It must be 32 to 44 base58 characters. Anything else stops here and nothing is sent.
- Read the flights. The last 1,000 signatures from a public Solana RPC: when each one landed and whether it failed.
- Open the jars. Jupiter returns the SOL balance and every token account, including the empty ones a wallet leaves behind after selling.
- Price the jars. Up to 150 held tokens are priced through Jupiter, 50 per request. A token with no price counts as dust.
- Measure and name. The browser turns all of that into the numbers in Measurements, scores each species and picks the highest.
Data sources
Two public services, both open to browsers. No keys are bundled with the site.
| Source | Endpoint | What we take |
|---|---|---|
| Solana RPC | getSignaturesForAddress on solana-rpc.publicnode.com | Block time and error flag of the last 1,000 transactions |
| Jupiter | /ultra/v1/holdings/<address> | SOL balance, every token account and its amount |
| Jupiter | /price/v3?ids=… | USD price of SOL and held tokens |
| Fallback | getBalance | Used only when the holdings call fails. Then the plate shows SOL only |
Using your own RPC
Pass any endpoint that allows browser requests. The same goes for Jupiter if you run a proxy.
const plate = await identify(address, {
rpc: "https://your-rpc.example.com", // any Solana JSON-RPC endpoint
signatures: 500, // read fewer flights, faster
dustUsd: 5 // stricter idea of dust
});Measurements
Everything the species key looks at. All of it comes from measure(raw).
| Field | Meaning | How |
|---|---|---|
flights | Transactions read | Up to 1,000 most recent signatures |
capped | Wallet is older than what we read | true when all 1,000 came back |
ageDays | Days since the oldest flight read | Real age only when capped is false |
idleDays | Days since the last flight | Now minus newest block time |
perDay | Flights a day | Flights over the span between oldest and newest |
failRate | Share of crashed flights | Signatures with a non-null error |
activeHours | Hours of the day it flies | Distinct UTC hours with at least one flight, 0 to 24 |
peakHour | Busiest hour | UTC hour with the most flights |
medianGap | Typical seconds between flights | Median of gaps between neighbours |
netUsd | Value in plain sight | SOL × price + priced tokens |
tokens · dust | Full jars · jars under $1 | Token accounts with a balance above zero |
emptyJars · emptyShare | Token accounts left at zero | Usually what is left after selling everything |
Species key
Nine species. The order in this table is also the tie-break order.
| Species | Recognise it by | Rarity |
|---|
Scoring
Each species gets a score from 0 to 100. The highest one wins. Ties go to the species that comes first in the key.
larva: flights == 0 ? 100 : (young ? 80 : 0) + (flights < 15 ? 25 : 0)
robotica: (perDay > 2000 ? 85 : perDay > 400 ? 50 : perDay > 150 ? 25 : 0)
+ (activeHours >= 22 ? 30 : 0) + (flights > 50 && medianGap < 15 ? 15 : 0)
balaena: netUsd >= 250000 ? 95 : netUsd >= 50000 ? 70 + (netUsd - 50000) / 10000 : 0
snipera: failRate >= 0.1 ? 50 + failRate * 120 + (perDay > 40 ? 5 : 0) : 0
panica: emptyJars >= 8 && emptyShare >= 0.55 ? 45 + emptyShare * 40 + min(emptyJars, 60) / 4 : 0
pulvis: dust >= 12 && dustShare >= 0.65 ? 45 + dustShare * 30 + min(dust, 80) / 4 : 0
airdropia: tokens >= 8 && netUsd < 500 && 2 <= perDay <= 80 && failRate < 0.1 ? 52 + min(tokens, 40) / 2 : 0
holdia: (capped || ageDays >= 60) && perDay < 3 && tokensUsd >= 50 ? 55 + min(idleDays, 60) / 2 : 0
aurora: 40
// every score is clamped to 0…100; young = not capped and ageDays < 10Runner-up species
The plate shows the six best scores, not only the winner. A wallet that is 100 robot and 99 panic seller is worth knowing about.
Field notes
The bullet list on every plate. Each line is something any explorer already shows.
| Note | Shown when |
|---|---|
| First seen N days ago | Always. Says “older than its last 1,000 flights” when capped |
| Flies about N times a day | Always |
| N% of flights crashed | Crash rate is 2% or more |
| Most active around HH:00 UTC | Always. Adds “and at every other hour too” at 22+ active hours |
| Carries $N in plain sight | Always. SOL only when the token list was unavailable |
| N tokens worth less than a dollar | At least one dust token |
| Left N empty token jars | At least one empty jar |
| Has not moved in N days | Idle for 7 days or more |
The plate
A 1200 × 675 PNG drawn in the browser, sized for X.
It shows the species render, the Latin and common name, rarity, the specimen number and the first and last four characters of the address. The full address and balances are never drawn on it. The specimen number is a hash of the address, so the same wallet always gets the same number.
import { specimenNumber } from "SITE/lib/dawnmoth.js";
specimenNumber("A8A9GoxfmcjY2QAQ8tYS98SqDBF6hUFDmxAKBsLw29sL"); // 9875, foreverdawnmoth.js API
Everything is a named export. The default export bundles them too.
| Export | Signature | Returns |
|---|---|---|
identify | (address, opts?) | Promise of a full plate, see Output format |
readWallet | (address, opts?) | Raw data: signatures, SOL, jars, prices |
measure | (raw, opts?) | The fields in Measurements |
score | (features) | Object of nine scores, 0 to 100 |
classify | (features) | { species, scores } |
fieldNotes | (features) | Array of plain-language strings |
isAddress | (string) | true for a base58 string of 32 to 44 characters |
specimenNumber | (address) | Stable number from 1000 to 9999 |
SPECIES · ORDER | constants | Species records and the tie-break order |
DEFAULTS | constant | Endpoints and limits, see below |
Options
| Option | Default | Meaning |
|---|---|---|
rpc | solana-rpc.publicnode.com | Solana JSON-RPC endpoint |
jupiter | lite-api.jup.ag | Base for holdings and prices |
signatures | 1000 | Flights to read, 1000 is the RPC maximum |
maxPricedMints | 150 | Most tokens priced per reading |
dustUsd | 1 | A jar worth less than this is dust |
timeout | 15000 | Milliseconds per request |
Output format
What identify() resolves to. Shortened.
{
"address": "A8A9GoxfmcjY2QAQ8tYS98SqDBF6hUFDmxAKBsLw29sL",
"specimen": 9875,
"species": { "key": "robotica", "latin": "Robotica insomnis", "common": "The Sleepless Automaton", "rarity": "Rare", "img": "robotica.webp" },
"scores": { "larva": 0, "robotica": 100, "balaena": 0, "snipera": 100, "panica": 99, "pulvis": 90, "airdropia": 0, "holdia": 0, "aurora": 40 },
"features": { "flights": 1000, "capped": true, "perDay": 24000, "failRate": 0.85, "activeHours": 1, "netUsd": 4800, "tokens": 95, "dust": 80, "emptyJars": 4024 },
"notes": ["Older than its last 1,000 flights. We stopped counting there.", "Flies about 24,000 times a day.", "…"],
"readAt": "2026-11-04T18:20:00.000Z"
}Errors
| Message | Cause | What to do |
|---|---|---|
| That does not look like a Solana address. | Failed isAddress | Paste the full address, no spaces |
| HTTP 429 from … | Public endpoint rate limit | Wait a few seconds, or pass your own rpc |
| HTTP 5xx from lite-api.jup.ag | Wallet too large for the holdings call | Handled. The plate falls back to SOL only |
| The operation was aborted | A request took longer than timeout | Try again, or raise timeout |
Limits
What a field guide can and cannot tell from a moth on a board.
- It sees the last 1,000 flights. A very old, very busy wallet is judged by its recent behaviour only.
- It sees one wallet. People who split funds across many addresses look like many smaller moths.
- Prices are today's. A token that was worth a lot last month and nothing today counts as dust.
- Species are a joke with rules. The rules are exact and public, the names are not a verdict on anyone.
$DAWN
The coin of the field guide, on Solana.
The reader is free and stays free. It never asks for a wallet connection, holding $DAWN or anything else.
FAQ
Is it safe to paste my address?
An address is public by design. Dawnmoth only reads it. It cannot move funds, it never asks you to sign and it stores nothing.
Can I read someone else's wallet?
Yes, the same way any explorer can. That is exactly what the site wants you to notice.
I got the wrong species.
Look at the runner-up bars on your plate. Scores are close more often than you think, and the rules in Scoring show what tipped it.
Can my species change?
Yes. It reads your latest behaviour, so a caterpillar today can be a sniper next week. Most moths prefer not to discuss their past forms.
Why moths?
Moths are drawn to light. So are wallets, right up until someone turns it on.