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

@goonerlabs/claim-guard

v0.1.1

Published

Drain-resistant claim limiter for airdrops, faucets, and reward pools — cooldowns, per-window caps, and a global pool budget. Zero dependencies, pluggable async store.

Readme

@goonerlabs/claim-guard

Drain-resistant claim limiting for airdrops, faucets, and reward pools — the guardrail that stops a popular drop from being emptied by rapid or scripted claims.

It enforces three independent rules per identity (a wallet, phone number, or user id):

  • cooldown — a minimum gap between consecutive claims
  • window cap — a maximum number of claims within a rolling window
  • pool budget — a global ceiling on how much the whole pool can ever pay out

Zero dependencies. State lives in a pluggable async store (in-memory by default), and the clock is injectable, so the logic is fully testable.

npm install @goonerlabs/claim-guard

Quick start

const { createClaimGuard } = require('@goonerlabs/claim-guard');

const guard = createClaimGuard({
  cooldownMs: 24 * 60 * 60 * 1000, // one claim per 24h per wallet
  maxPerWindow: 3,                 // ...and no more than 3
  windowMs: 24 * 60 * 60 * 1000,   // ...in any 24h
  pool: { total: 1_000_000 },      // and never pay out more than the pool holds
});

async function handleClaim(wallet, amount) {
  const decision = await guard.check(wallet, { amount });
  if (!decision.allowed) {
    return { ok: false, reason: decision.reason }; // 'cooldown' | 'window_cap' | 'pool_exhausted'
  }
  await payout(wallet, amount);          // do the real on-chain transfer
  await guard.record(wallet, { amount }); // only record once the payout succeeded
  return { ok: true };
}

Use check() + record() when the payout can fail (record only after success). Use claim() when you want the check-and-record done in one step.

API

createClaimGuard(options)

| Option | Meaning | |---|---| | cooldownMs | Minimum gap between claims (ms) | | windowMs + maxPerWindow | At most maxPerWindow claims per rolling windowMs | | pool.total | Global payout ceiling (use integer base units for tokens) | | store | Async { get(key), set(key, value) } (default: in-memory) | | now | Clock function (default: Date.now) — injectable for tests |

guard.check(identity, { amount? }) → Decision

Non-mutating. Returns { allowed: true } or { allowed: false, reason, retryAfterMs? , remaining? }.

guard.record(identity, { amount? })

Persists a claim. Call after the payout actually succeeds.

guard.claim(identity, { amount? }) → Decision

check() then record() in one step, serialized in-process so concurrent claims can't over-draw the pool or exceed a cap. For a pool enforced across multiple processes, back it with a store that has atomic counters or an external lock.

guard.release(identity, { amount? })

Refunds pool budget reserved by a claim() whose payout later failed. No-op when no pool is configured.

guard.poolRemaining() → number

Budget left (Infinity if no pool configured).

Using Redis (or any store)

Implement the two-method store interface:

const guard = createClaimGuard({
  cooldownMs: 86_400_000,
  store: {
    async get(key) { const v = await redis.get(key); return v ? JSON.parse(v) : null; },
    async set(key, value) { await redis.set(key, JSON.stringify(value)); },
  },
});

Testing

npm test   # node --test

License

MIT © Owolabi Adeyemi (goonerlabs)