@orangecheck/airdrop-gate
Turn a candidate address list into a sybil-resistant allowlist backed by real Bitcoin stake. Ships both a library and an oc-airdrop CLI.
yarn add @orangecheck/airdrop-gate
Library API
import { filterAllowlist } from '@orangecheck/airdrop-gate';
const { ok, rejected } = await filterAllowlist(candidates, {
minSats: 100_000,
minDays: 30,
concurrency: 8,
onProgress: (done, total) => console.log(`${done}/${total}`),
});
Addresses are deduplicated before checking — duplicates would just waste API calls.
Options
| Option | Default | Notes |
|---|---|---|
minSats | 0 | Minimum sats bonded. |
minDays | 0 | Minimum days unspent. |
concurrency | 4 | Parallel check() calls. Free tier allows 60/min/IP. |
onProgress | — | (done, total, last?) => void — fires after every candidate. |
relays | SDK defaults | Override Nostr discovery relays. |
rejectOnError | true | Treat SDK failures as rejection. Set false to surface errors. |
Returns
interface FilterAllowlistResult {
ok: string[]; // passing addresses, in input order
rejected: AirdropDecision[]; // rejected with reasons
all: AirdropDecision[]; // every decision in input order
}
CLI — oc-airdrop
Ships as a binary. Reads candidates on stdin (one per line; blanks and # comments ignored).
oc-airdrop filter --min-sats 100000 --min-days 30 \
< candidates.txt \
> allowlist.txt \
2> rejections.log
Flags
| Flag | Default | Notes |
|---|---|---|
--min-sats <n> | 0 | |
--min-days <n> | 0 | |
--concurrency <n> | 4 | |
--allow-lookup-errors | off | Surface SDK errors instead of rejecting. |
--json | off | Emit full JSON report instead of a plain allowlist. |
-h, --help | — |
JSON report shape
{
"total": 100,
"passed": 72,
"rejected": 28,
"allowlist": ["bc1q...", "bc1q...", ...],
"rejections": [
{
"address": "bc1q...",
"ok": false,
"reasons": ["below_min_sats"],
"check": { ... }
}
]
}
Shell one-liners
# Filter 10k candidates into an allowlist
oc-airdrop filter --min-sats 100000 --min-days 30 \
< candidates.txt > allowlist.txt
# Full audit trail with reasons
oc-airdrop filter --min-sats 1000000 --json \
< candidates.txt > report.json
# Count survivors
oc-airdrop filter --min-sats 100000 < candidates.txt | wc -l
Try it in-browser
/airdrop is a live demo: paste candidates, set thresholds, watch them filter in real time. Same logic as this CLI.
Threat model
What it raises the cost of
- Mass sybil attacks. 10 000 sybil wallets each needing
min_satsof locked Bitcoin becomes ruinously expensive. - Throwaway accounts. Candidates need real on-chain history.
What it doesn't solve
- A single determined attacker with real capital and time.
- Collusion between real users pooling capital.
- Front-running — candidates locking sats right before snapshot. Mitigate with
min_days+ past snapshots.
Pair with:
- Time-stamped snapshots (use last month's block height, not tip).
min_daysrequirements that predate the announcement.- Additional policy (e.g., cap per-address allocation).
Library example — progress UI
const progress = document.getElementById('progress')!;
const { ok, rejected } = await filterAllowlist(addresses, {
minSats: 100_000,
minDays: 30,
concurrency: 8,
onProgress: (done, total, last) => {
progress.textContent =
`${done}/${total} — ${last?.ok ? 'pass' : 'fail'} ${last?.address.slice(0, 12)}…`;
},
});
console.log(`${ok.length} qualify; ${rejected.length} rejected`);
Rate limits
Free /api/check tier: 60 req/min/IP → 3,600 candidates/hour. For bigger drops, self-host the verifier (open-source) or contact us for a higher-tier hosted key.
License
MIT.