docs / @orangecheck/airdrop-gate

@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

OptionDefaultNotes
minSats0Minimum sats bonded.
minDays0Minimum days unspent.
concurrency4Parallel check() calls. Free tier allows 60/min/IP.
onProgress(done, total, last?) => void — fires after every candidate.
relaysSDK defaultsOverride Nostr discovery relays.
rejectOnErrortrueTreat 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

FlagDefaultNotes
--min-sats <n>0
--min-days <n>0
--concurrency <n>4
--allow-lookup-errorsoffSurface SDK errors instead of rejecting.
--jsonoffEmit 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_sats of 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_days requirements 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.