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

@powforge/mcp-l402-gate

v0.3.0

Published

L402 Lightning paywall + Depth-of-Identity score gate for MCP. Two integration shapes: (a) stdio MCP server exposing l402_gate_challenge / l402_gate_verify / l402_gate_score for agent clients; (b) drop-in Express middleware + MCP tool factory that combine

Readme

@powforge/mcp-l402-gate

npm license node

Identity-scored Lightning paywall for MCP server operators.

L402 alone proves the caller paid 10 sats. It does not prove the caller has a reputation, has been around for more than 10 minutes, or that pricing one tool call shifts their economics at all. A fresh wallet pays the same 10 sats as a real user.

This package adds a Depth-of-Identity check on top of the L402 invoice. Drop it in front of an MCP tool and a caller has to (a) settle a Lightning invoice and (b) carry a DoI score above your threshold before the tool body runs. Cheap sybils still pay the toll, but the toll plus the per-pubkey reputation requirement is harder to grind than either piece on its own.

See it in action

A clone-and-run example server lives at github.com/zekebuilds-lab/mcp-l402-gate-example. It exposes one tool, bitcoin_data, that fetches the BTC/USD price plus mempool fees from mempool.space, gated by L402 + DoI. Clone it, fill in your LNBits creds, npm start, and you have a Lightning-gated MCP server running locally.

The Gap

Sats4AI's own documentation states the limitation plainly:

"autonomous agents cannot build reputation or receive preferential treatment across sessions."

@powforge/mcp-l402-gate closes that gap by composing L402 payment gating with the DoI oracle's composite identity score. A paying caller is also a known caller, with a per-pubkey reputation that survives across sessions and that costs irreversible work to fake.

Why not just L402

L402 is great wire format, weak abuse control. Recent MCP billing tools (sats4ai-mcp, invinoveritas, l402-kit, 402-mcp, coinopai-mcp) all ship the same 402 -> macaroon -> paid -> tool body flow, and an attacker can replay the flow from a fresh node every minute. coinopai-mcp's own author put it: "x402 is payment transport only. It doesn't handle agent identity, rate negotiation, multi-agent splits, or reputation."

PowForge has been shipping the missing piece. The DoI oracle at https://identity.powforge.dev returns a Schnorr-signed score for any Nostr pubkey, computed from observable irreversible work across four dimensions (social, access, vouch, economic). This package wires that score into the L402 path so a paying caller is also a costly-to-fake caller.

Requirements

  • Node >= 18
  • An LNBits wallet (URL + invoice/read API key) for the Lightning side
  • An accessible PowForge oracle URL (default: https://identity.powforge.dev)

5-line integration (Express)

const express = require('express');
const { mcpL402Middleware } = require('@powforge/mcp-l402-gate');

const app = express();
app.use('/tools/expensive', mcpL402Middleware({
  secret: process.env.GATE_HMAC_SECRET,
  lnbitsUrl: process.env.LNBITS_URL,
  lnbitsApiKey: process.env.LNBITS_INVOICE_KEY,
  satsAmount: 10,
  minScore: 10, // composite >= 10 means "emerging" tier on the oracle
}));

app.post('/tools/expensive', (req, res) => {
  // Reached only when L402 paid AND req.doiScore >= 10
  res.json({ ok: true, doiScore: req.doiScore, l402: req.l402Token });
});

The caller passes their pubkey via the X-Caller-Pubkey header or ?pubkey= query string. v0.3.0 still treats this as caller-asserted; a future release will bind it cryptographically via NIP-98.

MCP tool wrapping

const { mcpL402Tool } = require('@powforge/mcp-l402-gate');

const expensiveTool = mcpL402Tool({
  secret: process.env.GATE_HMAC_SECRET,
  lnbitsUrl: process.env.LNBITS_URL,
  lnbitsApiKey: process.env.LNBITS_INVOICE_KEY,
  satsAmount: 10,
  minScore: 10,
}, {
  name: 'image_render',
  description: 'Render an image. 10 sats. Requires DoI score >= 10.',
  inputSchema: {
    type: 'object',
    properties: {
      prompt: { type: 'string' },
      pubkey: { type: 'string' },
      auth: { type: 'object', properties: { macaroon: { type: 'string' }, preimage: { type: 'string' } } },
    },
    required: ['prompt', 'pubkey'],
  },
}, async (args, ctx) => {
  // Runs only when paid AND ctx.doiScore >= 10
  return { image_url: `https://example/r/${args.prompt}`, billed_to: ctx.doiScore };
});

// Register expensiveTool with your MCP server. On first call without args.auth,
// the tool returns { paid: false, challenge: { macaroon, invoice, ... } }.
// The MCP client pays the invoice, then re-calls with args.auth set.

Demo agent

A runnable five-step walkthrough ships with the package as mcp-l402-gate-demo. It exercises a real L402+DoI endpoint end to end so an operator (or a curious agent author) can see the full challenge -> pay -> score -> retry loop in under 90 seconds.

$ npx -y @powforge/mcp-l402-gate-demo \
    --pubkey b4b12dfbc3dfdfa803bb72e344e761dc78db4ec2058c8db3f1c3ac63f9e42b44 \
    --target https://image.powforge.dev/mcp \
    --tool image_render \
    --lnbits-url $LNBITS_URL \
    --lnbits-key $LNBITS_INVOICE_KEY

[1/5] Calling tool without auth — expecting 402 challenge
      ok challenge: payment_hash=ab12cd34 price=10 sats scope=mcp-l402-gate:call (124ms)
[2/5] Paying invoice via LNBits
      ok paid in 412ms preimage=e3f7a8b9
[3/5] Looking up DoI score for caller pubkey
      ok composite=42 rank=active depth={social:8,access:12,vouch:6,economic:16} (88ms)
[4/5] Retrying tool call with auth={macaroon, preimage}
      ok 200 in 89ms
[5/5] Response:
      {"paid":true,"doiScore":42,"rank":"active","result":{"image_url":"..."}}

Pass --simulate-low-score to short-circuit step 3 with composite=0 and watch the gate emit score_too_low against the live endpoint — useful for confirming your minScores config blocks the right callers before you point real traffic at it.

| Flag | Default | Notes | |------|---------|-------| | --pubkey <hex64> | required | Caller pubkey the oracle scores. | | --target <url> | https://image.powforge.dev/mcp | MCP endpoint base. | | --tool <name> | image_render | Tool to call on the endpoint. | | --lnbits-url <url> | $LNBITS_URL | LNBits base URL for the pay step. | | --lnbits-key <key> | $LNBITS_INVOICE_KEY | LNBits invoice/read key. | | --oracle <url> | https://identity.powforge.dev | DoI oracle base. | | --simulate-low-score | off | Force composite=0 in step 3 to exercise the deny path. |

Exit codes distinguish which step failed so the demo composes with shell scripts and CI smoke checks:

| Code | Meaning | |------|---------| | 0 | Full walkthrough completed and the tool returned a 200. | | 1 | Step 1 failed — challenge endpoint unreachable or wrong shape. | | 2 | Step 2 failed — LNBits pay errored or returned no preimage. | | 3 | Step 3 failed — oracle lookup failed. | | 4 | Step 4 failed — retry call denied (most commonly score_too_low). |

Config reference

| Field | Default | Notes | |-------|---------|-------| | secret | required | HMAC key for macaroon signing. Rotate periodically. | | lnbitsUrl | required | LNBits base URL. | | lnbitsApiKey | required | LNBits invoice/read key. NEVER pass admin key. | | satsAmount | 10 | Invoice amount per call. | | minScores | optional | Map of dotted-path -> required minimum. See Multi-dimensional gating below. Wins over minScore when both set. | | minScore | 10 | Reject paid callers below this composite score. Object form {composite: 10, social: 5} is accepted as sugar for minScores. Scalar form is the legacy single-axis path. | | failClosed | true | If oracle errors, reject the call. Set false to fall through with req.doiScoreError. | | oracleUrl | https://identity.powforge.dev | Override for self-hosted oracles. | | scope | mcp-l402-gate:call | L402 macaroon scope. | | ttlSeconds | 600 | Macaroon validity. | | scoreField | composite | Which envelope field to compare to scalar minScore. Ignored when minScores is set. | | callerPubkeyHeader | x-caller-pubkey | HTTP header carrying the caller's asserted pubkey. | | oracleAuth | optional | {macaroon, preimage} if your oracle is itself L402-paywalled. | | createInvoiceFn | optional | Test seam. Async (memo) => {payment_hash, bolt11}. | | checkPaidFn | optional | Test seam. Async (payment_hash) => boolean. | | lookupScoreFn | optional | Test seam. Async (pubkey) => {composite, rank, depth}. |

Multi-dimensional gating

The DoI oracle returns a composite score plus four depth.* axes — depth.social, depth.access, depth.vouch, depth.economic. Collapsing all of that into a single composite threshold throws away the only signal that meaningfully separates a fresh sybil from a real participant: the shape of where their identity came from.

@powforge/mcp-l402-gate v0.3.0 adds minScores so you can gate on multiple axes at once. The gate evaluates an AND across every entry; one trip is enough to deny.

mcpL402Middleware({
  secret: process.env.GATE_HMAC_SECRET,
  lnbitsUrl: process.env.LNBITS_URL,
  lnbitsApiKey: process.env.LNBITS_INVOICE_KEY,
  satsAmount: 10,
  minScores: {
    composite: 10,        // emerging tier overall
    'depth.social': 5,    // at least some social weight
    'depth.economic': 3,  // at least some economic skin in the game
  },
});

When a caller trips one or more axes, the 403 body carries a failed array so the client knows exactly which dimension(s) blocked the call:

{
  "error": "score_too_low",
  "score": 2,
  "min": 5,
  "field": "depth.social",
  "rank": "emerging",
  "failed": [{ "field": "depth.social", "value": 2, "min": 5 }]
}

If you only want a single composite threshold, keep using minScore: 10 — that path is unchanged. The scalar shape is treated as sugar for {[scoreField]: N} and the deprecation is soft: existing v0.2 callers do not need to migrate.

Precedence

| Config shape | Behavior | |--------------|----------| | minScores: { ... } | Wins. minScore ignored if also set. | | minScore: { ... } | Object form accepted as minScores sugar. | | minScore: 10 (number) | Legacy single-axis path. Pairs with scoreField. | | neither set | Defaults to minScore: 10 on scoreField: 'composite'. |

Score thresholds (composite)

Same buckets the oracle reports as rank:

| Threshold | Rank | Use it when | |-----------|------|-------------| | 0 | unknown | You only want pay-to-call. Skip this package and use L402 directly. | | 10 | emerging | First-call abuse hurts. Default for most public MCP tools. | | 40 | active | The tool burns real GPU or has expensive side effects. | | 100 | established | Compliance-sensitive or single-tenant SaaS-style endpoints. | | 200 | trusted | High-trust admin tooling. |

Failure modes

| Status | Body | Meaning | |--------|------|---------| | 402 | {error: "payment required", macaroon, invoice, payment_hash} | First call. Pay the invoice, retry with Authorization: L402 <macaroon>:<preimage>. | | 401 | {error: "invalid macaroon", reason} | Macaroon malformed, expired, wrong scope, or wrong signature. | | 401 | {error: "preimage does not match payment hash"} | Preimage failed sha256 check against the macaroon's payment hash. | | 409 | {error: "macaroon already redeemed"} | Replay guard fired. Mint a fresh macaroon. | | 400 | {error: "caller_pubkey_required"} | No X-Caller-Pubkey header or ?pubkey= query. | | 403 | {error: "score_too_low", score, min, rank, field, failed: [...]} | Caller paid but one or more DoI thresholds tripped. The failed array lists every axis that was below its minScores entry. | | 503 | {error: "oracle_unavailable", mode: "fail_closed"} | Oracle error and failClosed is on (the default). | | 502 | {error: "invoice provider unavailable"} | LNBits unreachable on first-call mint. |

Why this is a separate package

The L402 macaroon mint and verify code, the LNBits client, and the oracle client are all already shipping inside other PowForge packages. The point of @powforge/mcp-l402-gate is to make the composition trivial: one factory, one config object, one middleware OR one tool wrapper. Operators do not have to assemble three packages by hand to get a defended endpoint.

Tests

npm test

61 unit tests, no real network. The macaroon HMAC is real; LNBits and oracle are stubbed.

License

MIT.

Links