npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@huma-protocol/scanner

v0.7.0

Published

HUMA wallet-graph scanner — signed sybil-risk scoring for any wallet list. Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche and Solana via free-tier Helius + Alchemy RPCs. Ed25519-signed JSON + Markdown + HTML reports.

Readme

HUMA Scanner

Sybil risk scoring for token distributions — measured, signed, and verifiable offline.

Score an airdrop eligibility list by wallet-graph structure, get a ranked report with per-wallet evidence and an Ed25519 signature anyone can verify without calling us.

npm node tests mutation score ROC-AUC license

Results · Install · Networks · How it works · Verify a report


Why this exists

An airdrop eligibility list is not a list of people. It is a list of addresses, and a meaningful share of them are one operator wearing many wallets. The question a token foundation actually needs answered is not "is this a human?" — it is "which of these addresses move together?"

HUMA answers that from public on-chain data alone. No KYC, no identity provider, no account linking, nothing to leak.

$ huma scan ./eligibility.csv

  ▸ Probing 2,000 wallets
      estimated cost: ~900K Alchemy CU (3.0% of a free month)
      progress: out/eligibility.scan-state.ndjson

  ⠹  [██████████████████░░░░░░]  62%  1247/2000  ·  21m elapsed  ·  ~28m left
     ethereum  0x8f2ac1…41d  farm-alt-3        22.4 wallets/min
     corpus hits 37  ·  contracts 8  ·  failed 3  ·  state ✓

🎯 What you get

| | | |---|---| | 🕸️ Graph clustering | Shared funder, synchronized first activity, common exit recipient, overlap with a 25,836-address labeled corpus | | 🧾 Evidence per wallet | Every score carries the specific reason it was flagged — never a bare number | | 🔏 Signed reports | Ed25519 over canonical JSON. Verify offline with @huma-protocol/sdk; no API call, no trust in us | | 📊 Published methodology | ROC-AUC, FPR, FNR, calibration curve and the corpus builder are all in this repo — including the run we retired | | 🌐 7 networks | Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche, Solana | | 💸 Runs on free tiers | Your own Alchemy + Helius keys. ~66,000 wallets/month at zero cost | | 🔌 Zero infrastructure | A local CLI. No server, no account, no data leaves your machine | | ⏸️ Resumable | Progress is on disk. Ctrl-C, quota exhaustion, or a dead connection loses nothing |


⚡ Quick start

npm install -g @huma-protocol/scanner

huma init                      # prompts for RPC keys, generates a signing keypair
huma doctor                    # checks keys, networks, corpus, disk — before you spend RPC
huma scan ./eligibility.csv    # → out/eligibility.{json,md,html,csv}
huma verify ./out/eligibility.json

Your CSV needs two columns:

address,chain,label
0xd8da6bf26964af9d7eed9e03e53415d37aa96045,ethereum,alice
0x8f2ac1c9e4f3b7a1d6e5c4b3a2918f7e6d5c4b3a,base,bob

🔏 Verifying a report

The point of signing is that the recipient does not have to trust the sender — including us. A signed report verifies with a public key and nothing else.

import { verifyReport } from '@huma-protocol/sdk';

const ok = await verifyReport(report, publicKeyB64);
// ok === false if a single byte of the payload changed
huma verify ./out/eligibility.json      # same check, from the CLI
huma diff old.json new.json             # what moved between two scans

📐 Quality bar

This is a pre-launch project by a solo founder. What stands behind it is the evidence, not the headcount:

| | | |---|---| | 549 | tests, covering happy path, failure path and edge cases | | 76.9% | mutation score — mutants Stryker kills, not lines merely executed. 85.7% on score-and-sign.ts, the module that produces every signed verdict. Reproduce with pnpm exec stryker run | | 0.7663 | ROC-AUC on the full 2,000-wallet corpus, measured against airdrop-eligible wallets, not random mainnet users | | 25,836 | labeled corpus addresses across two independent sources | | 0 | runtime dependencies beyond @noble crypto primitives |

Honesty is enforced in the output, not just the docs. A scan that drops wallets to rate limits marks itself partial in the signed report. A scan on a chain with no labeled corpus records unlabeled_chains, because those wallets score on 3 of 4 features and come out looking cleaner than they are. A result you cannot trust never arrives silently.

We retired our own headline number. An earlier evaluation scored ROC-AUC 0.9309 — measured against random mainnet users rather than airdrop-eligible wallets. Re-run against the population the tool is actually deployed against, the same scorer scored 0.4002, below chance. Two real defects surfaced, both fixed. The full account is below, including the 62% miss rate at the default threshold.


Methodology + Results (eval v2, 2026-09-01)

Question. Does the wallet-graph score separate known sybils from wallets a customer would actually hand us? This is the project's primary evaluation gate: ship only at ROC-AUC ≥ 0.75 with a false-positive rate under 10% at the default threshold, or don't ship.

Eval v1 (AUC 0.9309) is superseded and should not be quoted. Its negatives were unique EOA senders from recent mainnet blocks — people transacting on a Tuesday, most of whom never touch an airdrop. The deployment population is an eligibility list, where every wallet is airdrop-active by construction. v1 therefore measured an easier question than the product answers, and the number was inflated by a wallet-age artifact (median age 1,242 days for the 2024-era sybil labels vs 162 days for current-day negatives). Re-running the identical positives against airdrop-eligible negatives scored 0.4002 — below chance. The corpus builder, both runs, and the fixes are in the repo.

Corpus. n = 2,000 EVM wallets, 50/50:

  • 1,000 known_sybil — random holdout from the LayerZero community mirror, removed from the scan-time corpus so no wallet can flag itself. Identical wallets and seed as v1, so the two runs are directly comparable.
  • 1,000 airdrop_active_unflagged — distinct EOA senders to airdrop-eligibility venues (bridge and L2 deposit contracts) over a 400k-block window, minus the full sybil corpus and the CEX allowlist. "Unflagged," not verified clean: the LayerZero list is not exhaustive, so some are undetected sybils. That noise depresses measured performance, which is the safe direction for a kill-criterion gate.

Procedure. One interleaved scan over the mixed set (algorithm_version: wallet-graph-v0.3.1, free-tier Alchemy). 2,000 of 2,000 scored — no wallets dropped, the report is not partial. Wallets that hit rate limits on the first pass were recovered by re-running against the retained probe cache, which costs no RPC.

The corpus has been scanned more than once, and we publish the spread. This run scored 0.7663 on the full 2,000. The previous run of the same corpus scored 0.7669 on the 1,991 it managed to probe. Nothing in the scorer changed between them that affects this population, so the ~0.0006 gap is run-to-run variation from re-probing live chain data. A classifier metric quoted to four decimals with no stability band invites more confidence than one run earns.

Results

| Metric | Value | Gate | |---|---|---| | ROC-AUC | 0.7663 | S1 ≥ 0.75 ✅ · K1 kill < 0.70 ✅ | | FPR @ default threshold (score < 60) | 9.50% | K2 kill > 10% ✅ | | FNR @ default threshold | 62.10% | ⚠ see below |

Threshold sweep:

| Exclude if score < T | FPR | FNR | |---|---|---| | 30 | 5.5% | 96.5% | | 40 | 5.7% | 85.5% | | 50 | 7.4% | 72.7% | | 60 (default) | 9.5% | 62.1% | | 70 | 11.4% | 51.3% | | 80 | 15.0% | 37.3% | | 90 | 27.1% | 24.9% |

T=60 is the most sensitive cut the gates permit: T=70 reaches FPR 11.4% and trips K2.

Calibration by score decile (sybil share per bin):

score   10-19  20-29  30-39  40-49  50-59  60-69  70-79  80-89  90-99
n            7     83    112    145    127    127    176    245    978
sybil%   100%    34%    98%    88%    84%    85%    79%    51%    26%

What changed between v1 and v2 — two real defects, both invisible under v1's easier corpus.

  1. common_exit was inverted. It fired on 100% of clean wallets vs 78% of sybils, and was the most-fired feature. ~85% of the airdrop-active cohort shared the same popular router and token contracts; the 72-entry CEX allowlist could not suppress those. Fixed by skipping exits shared by ≥20% of a cohort — a destination most of the cohort uses carries no clustering information. The feature's discriminative gap flipped from −21.9 to +51.9.
  2. The age/activity bonus was anti-predictive. Decomposing the score: wallet_cluster (the four graph features) scored AUC 0.7661 on its own, while onchain_history + time_bonus scored 0.2634 — worth 35 of ~85 points and actively fighting the signal, because it rewards aged wallets carrying transaction volume, which is exactly what aged-wallet farming produces. v0.3 removes both from the score, rescales the cluster contribution to 0–100, and recalibrates thresholds to 60/80.

A third iteration was attempted and found nothing. Reported because a negative result is evidence too.

common_exit fires on 24% of clean wallets — six times the rate of shared_funder and a hundred times cluster_overlap — and its peer graph collapses into a single 876-wallet component covering 44% of the cohort. That looks like a defect, so 15 tuning candidates were swept against the same corpus: continuous IDF weighting in place of the binary cliff, minimum peers per destination, minimum distinct shared destinations, and tighter cliff values.

Every one of the 15 scored significantly worse (95% CI on ΔAUC excluding zero, downward, paired bootstrap, 500 replicates). Two results explain why:

  • Dropping common_exit entirely costs −0.0322 AUC. The feature carries real signal despite its false-positive rate.
  • The 20% "ubiquitous destination" cliff suppresses zero destinations in this cohort, yet tightening it to 10%, 5% or 2% degrades AUC monotonically (−0.0222, −0.0188, −0.0154). So the 24% clean firing does not come from popular routers being mistaken for farms. It comes from destinations shared by fewer than 2% of the cohort — genuinely rare addresses, which is exactly what the feature is supposed to fire on. It looked like a defect and is not one.

The decision rule — adopt only if the 95% CI on the AUC delta excludes zero, FPR stays under 10%, and AUC clears 0.75 — was written down before the numbers were known, and it earned its keep: on a partial 1,587-wallet cohort one candidate led at +0.0040 with a CI excluding zero, and at the full 2,000 it flipped to −0.0057. Picking the best of 16 rows without an interval would have shipped a regression as an improvement.

Known limitations.

  • FNR 62% at the default cut. The tool misses roughly 3 in 5 sybils. It is a ranking-and-evidence layer, not an auto-ban: the operator's call is final.
  • Temporal confound. Sybil labels are 2024-era; negatives are current. "Age" therefore partly separates label vintage rather than sybil-ness, so the 0.2634 above is likely inflated. Dropping the component is justified because it measurably helps on the realistic population and aged-wallet farming is cheap — not because age is proven worthless in general. A corpus with contemporaneous labels could change this.
  • Negatives are unflagged, not verified clean (see above).
  • Cohort features are cohort-composition-dependent by design — representative of a customer scanning one eligibility list.
  • Corpus is 100% EVM. Solana has no labels, so cluster_overlap cannot fire there; such scans are flagged unlabeled_chains in the report.
  • Earlier runs were partial (44 and 9 wallets dropped to rate limits) and said so in the signed payload. The run published here is complete: 2,000 of 2,000. A report that drops wallets always flags itself partial in-band.

Reproduce. From scanner/: pnpm tsx tools/build-hard-eval-corpus.ts, scan eval-all.csv, then pnpm tsx tools/compute-eval-metrics.ts --report=<scan.json> --labels=<eval-all.csv> --threshold=60.


Network coverage

Put the network in the CSV's chain column. evm is still accepted and means Ethereum mainnet, so existing files keep working.

| Network | chain value | Labeled corpus | Notes | |---|---|---|---| | Ethereum mainnet | ethereum (or evm) | ✅ 25,836 entries | L1 | | Base | base | ✅ same corpus | Ethereum L2 | | Arbitrum One | arbitrum | ✅ same corpus | Ethereum L2 | | OP Mainnet | optimism | ✅ same corpus | Ethereum L2 | | Polygon PoS | polygon | ✅ same corpus | Ethereum sidechain | | Avalanche C-Chain | avalanche | ✅ same corpus | EVM L1 | | Solana | solana | ❌ none | see the warning below |

Every EVM network shares one address space, so the labeled corpus applies to all of them without a per-network label set: an address flagged from Ethereum activity is the same address on Base or Arbitrum. That is why adding an EVM network is close to free and adding Solana is not.

Tokens are covered by default. Scoring is address-based, not token-based, so any token on a supported network — ERC-20s such as LINK or UNI, or an L2-native token — is covered by scanning its holder addresses. There is nothing to configure per token.

Two limits, stated plainly:

  • Solana scores are optimistic. The corpus has zero Solana labels, so cluster_overlap — 1 of the 4 features — cannot fire there. Solana wallets are scored on 3 of 4 signals and come out cleaner than they are. A scan reports this on stderr and records unlabeled_chains in the signed report. Do not present a Solana score as equivalent to an EVM one.
  • Alchemy gates networks per app. A key with only Ethereum enabled returns 403 <NET>_MAINNET is not enabled for this app for the rest. Enable them at dashboard.alchemy.com → your app → Networks, then confirm with huma doctor, which lists each network's status before you spend RPC budget.

Not supported, and not planned: Bitcoin and its forks (Litecoin, Dogecoin, Bitcoin Cash), Lightning, Cardano, XRP, Tron, Polkadot. The four features assume an account model with a fundable, reusable address; UTXO chains and payment-channel networks do not provide one, and none of them host the airdrop distributions this tool is built for.

What it does

  1. Reads a CSV of wallet addresses (EVM and/or Solana)
  2. For each wallet, pulls minimal on-chain history via free-tier RPCs:
    • Helius (Solana, 1M credits/month free)
    • Alchemy (all EVM networks below, 30M compute units/month free)
  3. Computes four features:
    • Shared funder — multiple wallets in the scan funded from the same source
    • Sync timing — wallets with first activity in the same 4-hour window
    • Common exit — wallets that send outputs to the same recipient address
    • Cluster overlap — wallet (or its funder/exit) appears in a labeled-sybil corpus (LayerZero / Hop / Arbitrum / manual entries)
  4. Scores each wallet 0–100 using Variant A from ../docs/SCORING_ALGORITHM.md
  5. Signs the report (Ed25519) and writes:
    • out/<name>.json — full signed payload
    • out/<name>.md — human-readable report with per-wallet evidence

Install as a global CLI

# From the scanner directory (one time)
cd scanner
pnpm install
pnpm build       # compiles to dist/
npm link         # makes the `huma` and `huma-fetch-corpus` commands globally available

# Now run from ANY directory
cd /any/project
huma --help
huma init                       # interactive first-run: API keys + signing keypair → .env
huma doctor                     # env health check (paste into support issues)
huma scan ./my-wallets.csv      # outputs to ./out/my-wallets.{json,md,html}
huma verify ./out/my-wallets.json
huma diff old.json new.json     # band changes + added/removed wallets
huma feedback 0xabc=sybil --source=pilot-1 --confidence=0.95
huma-fetch-corpus               # populates ./data/sybil-corpus.csv
huma update --check             # check if a newer version is available
huma update                     # install the newer version (skipped in dev mode)
huma completions zsh > "${fpath[1]}/_huma"   # shell completion

Exit codes

All huma subcommands follow the same convention. Useful for shell pipelines:

huma scan list.csv && huma verify out/list.json || alert "scan failed"

| Code | Meaning | |------|---------| | 0 | Success. (huma verify exit 0 = signature valid. huma diff exit 0 regardless of whether changes were found — diffing isn't an assertion.) | | 1 | Fatal error: missing env var, no wallets probed successfully, file not found, RPC fully unreachable, malformed input report. | | 2 | Validation error: bad CLI arguments, malformed CSV, unknown subcommand argument, OR signature invalid (for verify / diff). |

huma doctor is the exception: exit 0 if all checks pass or only warnings; exit 1 if any check failed.

Updating

huma update --check     # checks GitHub for a newer version, doesn't install
huma update             # installs the newer version via `npm install -g`

The detector knows the difference between a real global install and a dev checkout / npm link — it won't blow away your source. Override the source URL with HUMA_UPDATE_SOURCE=https://... if you publish from a fork; override the install command with HUMA_UPDATE_INSTALL_CMD="..." for non-npm distributions.

Path resolution: .env and data/sybil-corpus.csv are read from your current directory first, then fall back to the scanner's install dir. Output files are always written to cwd. So you can keep one set of keys in the scanner repo for dev, and override per-project in any other directory by dropping a local .env.

When you're ready to publish to npm registry, flip "private": false in package.json and run npm publish — prepublishOnly enforces typecheck + build.


In-repo development

cd scanner

# Install deps (pnpm preferred; npm works too)
pnpm install

# Copy env template and fill in keys
cp .env.example .env

# Get free API keys:
#   Helius:  https://dashboard.helius.dev  (1M credits/month, 10 RPS)
#   Alchemy: https://dashboard.alchemy.com  (30M CU/month, multichain)

# Generate a POC signing keypair (run from inside scanner/)
node --input-type=module -e "import('@noble/ed25519').then(async e => { \
  const sk = e.utils.randomPrivateKey(); \
  const pk = await e.getPublicKeyAsync(sk); \
  console.log('HUMA_SIGNING_PRIVATE_KEY_B64=' + Buffer.from(sk).toString('base64')); \
  console.log('HUMA_PUBLIC_KEY_B64=' + Buffer.from(pk).toString('base64')); \
})"

# Paste the HUMA_SIGNING_PRIVATE_KEY_B64 line into .env. Keep HUMA_PUBLIC_KEY_B64
# somewhere you can verify against later (e.g. ~/.huma/pk.b64 — gitignored).

The signing key is local-only for POC. Production keys live in Cloudflare Workers Secrets per ../docs/SECURITY.md.


Running a scan

With the sample wallets

pnpm scan examples/sample-wallets.csv

You'll get out/sample-wallets.json and out/sample-wallets.md.

With your own wallets

Drop a CSV at examples/my-wallets.csv (gitignored — won't be committed). Columns:

address,chain,label
0x06ee4f2bAaFbF8dA9230e176318799928A1b7510,evm,my_main_eth
0x89980Ef183E54596113CF29f50E525A7744bB1A1,evm,my_secondary
3FxzajXw1w5FVWeL2HFv5fHSJzsL7FdVwY,solana,my_solana_main
E5eAnLzBVLBWthdTBV5jxD8Wmt5SEtdLYZViBfMHXfuR,solana,my_solana_b

Then:

pnpm scan examples/my-wallets.csv

Open out/my-wallets.md in any markdown viewer to see your scored cohort + per-wallet evidence.

Output to a specific path

pnpm scan examples/my-wallets.csv --out=out/2026-05-16-personal

What's in scope vs not

In scope (this POC):

  • Solana + Ethereum + Base
  • Funder / timing / exit cohort features
  • Ed25519-signed JSON + markdown reports
  • Sequential scanning with polite rate-limit pacing (~10 RPS)
  • Local signing key

Not in scope yet (deferred to actual Week 2–4 build):

  • ENS resolution (use 0x... for now; resolve .eth names manually first)
  • Other EVM chains beyond Ethereum + Base (easy to add — see providers.ts)
  • Identity signals (GitHub, X, Worldcoin) — Variant B in ../docs/SCORING_ALGORITHM.md
  • Behavioral signals (deferred to Phase 2)
  • Calibration weights from /v1/feedback (Month 3+)
  • Gas-pattern similarity and quest-completion timing features
  • Parallel batching, retry/backoff, exhaustive history (saved for production)
  • Public-key registry endpoint (replaced by env var for POC)

Seeding the labeled-sybil corpus

cluster_overlap only fires when a real corpus is loaded. The shipped data/sybil-corpus.example.csv is a schema-only placeholder.

Quick ingest (LayerZero — ~800K addresses)

# Full corpus (~10 MB download, parses ~800K addresses)
pnpm fetch-corpus

# Or: smoke-test with the first 500 rows only
pnpm fetch-corpus -- --limit=500

# Or: merge with existing entries instead of replacing
pnpm fetch-corpus -- --append

This pulls initialList.csv.zip from the LayerZero-Labs/sybil-report repo (joint analysis by LayerZero + Chaos Labs + Nansen, confidence 0.85 in our schema) and writes data/sybil-corpus.csv. That file is gitignored — corpus data is local, not source.

Adding more sources

Append rows to data/sybil-corpus.csv from other public sources (or your own pilot-customer feedback). Schema:

address,chain,source,confidence,notes
0xab12...,evm,hop,0.9,"hop-protocol/hop-airdrop#192 reviewed sybil cohort"
0xcd34...,evm,arbitrum,0.7,"ArbitrumFoundation/sybil-detection — pattern match"
3FxzAj...,solana,manual,0.95,"observed in pilot #1 post-claim outcomes"

| Public source | Method | Approx volume | |---|---|---| | LayerZero-Labs/sybil-report | Auto-ingestable via pnpm fetch-corpus | ~803K addresses | | hop-protocol/hop-airdrop | Manual — pull from GitHub issues | ~hundreds of curated reports | | ArbitrumFoundation/sybil-detection | Manual — methodology + spot lists | Varies | | Pilot customer feedback | Manual entry from post-claim audits | Grows with every pilot |

How cluster_overlap scores

Per ../docs/SCORING_ALGORITHM.md Variant A — penalty capped at 25 points (largest single penalty in the formula):

  • Direct hit (wallet is in the corpus) → up to -20 × confidence points
  • Funder hit (wallet's first funder is in the corpus) → up to -8 × confidence points
  • Exit hit (wallet's recent recipients overlap with corpus) → up to -5 × hit_count points

A wallet that scores zero penalties from the cohort but has its funder appear in the LayerZero corpus still drops ~7 points — that's the signal that crosses scans and grows over time.


Free-tier budget math

For a 1,000-wallet EVM scan:

  • ~3 Alchemy calls per wallet (2× alchemy_getAssetTransfers + 1× eth_getTransactionCount)
  • ~150 CU each → ~450K CU for the scan
  • 30M CU/month free tier covers ~60 full 1K-wallet scans/month

For a 1,000-wallet Solana scan:

  • ~31 Helius calls per wallet (1× getSignaturesForAddress + ~30× getTransaction)
  • 31K calls per 1K-wallet scan
  • 1M credits/month free tier comfortably covers tens of 1K-wallet Solana scans

POC works on free tier. Paid tiers (~$50/mo each) become necessary above ~10K wallets/scan or multiple scans per week.


Verifying a signed report

A signed JSON report can be verified by any party with the public key. The signature covers the canonical-JSON serialization of the report payload minus the signature field (sorted keys, no whitespace, UTF-8). See verifyReport() in src/score-and-sign.ts.


Files

scanner/
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
├── README.md          (this file)
├── examples/
│   └── sample-wallets.csv
├── out/               (generated; gitignored)
└── src/
    ├── scan.ts           (CLI entrypoint)
    ├── providers.ts      (Helius + Alchemy wrappers)
    ├── features.ts       (cohort feature computation)
    ├── score-and-sign.ts (Variant A scoring + Ed25519 signing)
    └── report.ts         (markdown rendering)