Your MCP tools should not answer for free to agents you have never heard of

Charge a Lightning invoice per tool call. Reject callers below a reputation threshold. Same middleware does both.

L402 alone proves the caller paid 10 sats. It does not prove the caller has been around for more than 10 minutes. A fresh wallet pays the same as a trusted one until you stop letting them.

Live on npm Express middleware + MCP tool factory Schnorr-signed reputation 5-line integration
Install: two packages, copy-pasteable
npm install @powforge/mcp-l402-gate
npm install @powforge/mcp-identity

Or clone a working server: github.com/zekebuilds-lab/mcp-l402-gate-example — one tool (BTC price + mempool fees), L402-gated, runs in 5 commands.

Five-line gate (Express)

Drop in front of any tool route. Caller hits the route, gets a 402 with a Lightning invoice. Caller pays, replays with the macaroon and preimage. Gate checks the caller's Depth-of-Identity score against your threshold. Score below threshold means 403, score above means the tool body runs.

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 });
});

Caller passes their pubkey via the X-Caller-Pubkey header. v0.1.0 treats this as caller-asserted; v0.2.0 will bind it cryptographically via NIP-98.

Why not just L402

L402 is good 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.

The author of coinopai-mcp put it plain:

"x402 is payment transport only. It does not handle agent identity, rate negotiation, multi-agent splits, or reputation."

PowForge has been shipping the missing piece. The DoI oracle at identity.powforge.dev returns a Schnorr-signed depth-of-identity score for any Nostr pubkey, computed from observable irreversible work across four dimensions. @powforge/mcp-l402-gate wires that score into the L402 path so a paying caller is also a costly-to-fake caller.

How it works

1. Caller hits tool
First call to your gated route. No Authorization header yet.
2. L402 invoice minted
Gate returns 402 with macaroon + bolt11 invoice from your LNBits wallet.
3. Caller pays + replays
Settle the invoice. Replay with Authorization: L402 macaroon:preimage and X-Caller-Pubkey.
4. Gate scores caller
Gate calls oracle.powforge.dev for the caller's Schnorr-signed DoI score. Cached 5 min per pubkey.
5. Threshold check
Score below minScore returns 403. Score above runs the tool body with req.doiScore attached.
6. Macaroon burned
One-time-use. Replay returns 409. Macaroons expire on a configurable TTL (default 10 min).

MCP tool wrapping (no Express)

If you are not running Express, the package also ships a tool factory that returns a plain MCP tool definition with the gate baked in. The MCP client calls the tool, gets a payment challenge back as the tool result, pays the invoice, and re-calls the tool with the auth fields populated.

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 the same way you register any other tool. The gate is invisible to the rest of your server.

Reputation thresholds

The minScore field maps to the same buckets the oracle reports as rank. Pick what matches the cost of the tool you are gating.

minScoreRankUse it when
0unknownYou only want pay-to-call. Skip this package and use L402 directly.
10emergingFirst-call abuse hurts. Default for most public MCP tools.
40activeThe tool burns real GPU or has expensive side effects.
100establishedCompliance-sensitive or single-tenant SaaS-style endpoints.
200trustedHigh-trust admin tooling.

Set failClosed: true (default) to reject the call if the oracle is unreachable. Set failClosed: false to fall through with req.doiScoreError populated, and decide for yourself.

Who this is for

  • MCP server operators with expensive tools. Image rendering, web scraping, code execution, anything that costs you GPU minutes. L402 covers the cost; the DoI gate stops fresh sybils from grinding the toll.
  • Public MCP tool authors. Your tool is on the open registry, anyone can hit it. You want the casual user to pay 10 sats once and continue, but you do not want a swarm of throwaway pubkeys all paying the same toll forever.
  • Operators wiring agent-to-agent workflows. The other agent has its own pubkey with its own history. Trusted agents get a free pass; cold agents pay.

Works with any MCP server

The Express middleware shape works in front of any HTTP-fronted MCP server, including the official MCP SDK's StreamableHttp transport, or any custom server you stand up behind a reverse proxy. The MCP-tool factory shape works for stdio-only servers and any SDK that accepts a tool definition.

Three external IO points (createInvoiceFn, checkPaidFn, lookupScoreFn) are injectable for tests so you can run your CI suite without hitting a real LNBits wallet or the oracle.

Compare to other MCP gates

Side-by-side breakdowns against the other primitives shipping in this lane:

Get the packages

v0.1.0 caller-asserts the pubkey via header. v0.2.0 will bind it cryptographically via NIP-98 so an attacker cannot impersonate a high-reputation pubkey on the wire.

price the call. score the caller. let the gate do the rest.