tool calls, verifies L402 macaroons, queries an identity oracle, and evaluates composite thresholds before routing to the handler. The architecture must be fail-closed, enforce strict AND logic across identity axes, and return structured rejection payloads that agent runtimes can parse and act upon.
Step 1: Define the Threshold Configuration
The configuration object maps identity axes to minimum acceptable scores. Each axis represents a different dimension of caller legitimacy. The middleware evaluates all axes simultaneously; failure on any single axis triggers rejection.
interface ReputationThresholds {
composite: number;
depthSocial: number;
depthEconomic: number;
depthAccess: number;
depthVouch: number;
}
interface GateConfig {
hmacSecret: string;
lnbitsEndpoint: string;
lnbitsInvoiceKey: string;
baseSatAmount: number;
thresholds: ReputationThresholds;
failClosed: boolean;
oracleTimeoutMs: number;
}
Step 2: Implement the Middleware Router
The middleware intercepts incoming requests, validates the L402 macaroon against LNBits, fetches the caller's reputation envelope, and compares it against the configured thresholds. The evaluation uses strict AND logic across all axes.
import { Request, Response, NextFunction } from 'express';
async function reputationL402Middleware(config: GateConfig) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const macaroon = req.headers['x-l402-macaroon'] as string;
if (!macaroon) {
return res.status(402).json({ error: 'payment_required', scheme: 'bip122' });
}
// Verify Lightning settlement via LNBits
const paymentValid = await verifyL402Macaroon(config, macaroon);
if (!paymentValid) {
return res.status(402).json({ error: 'invalid_payment', details: 'macaroon_preimage_mismatch' });
}
const callerPubkey = extractPubkeyFromMacaroon(macaroon);
// Query identity oracle with timeout protection
const reputationEnvelope = await fetchReputationEnvelope(
callerPubkey,
config.oracleTimeoutMs
);
if (!reputationEnvelope && config.failClosed) {
return res.status(503).json({ error: 'service_unavailable', mode: 'fail_closed' });
}
// Evaluate thresholds using strict AND logic
const evaluationResult = evaluateThresholds(reputationEnvelope, config.thresholds);
if (!evaluationResult.passed) {
return res.status(403).json({
error: 'score_too_low',
rank: reputationEnvelope?.tier || 'unverified',
failed: evaluationResult.failures
});
}
// Attach verified caller context for downstream handlers
req.locals = {
callerPubkey,
reputation: reputationEnvelope,
paymentVerified: true
};
next();
} catch (err) {
console.error('[ReputationGate] Middleware error:', err);
return res.status(500).json({ error: 'internal_middleware_failure' });
}
};
}
Step 3: Threshold Evaluation Engine
The evaluation engine compares the fetched reputation envelope against the configured minimums. It returns a structured failure array that identifies exactly which axis triggered the rejection.
interface ReputationEnvelope {
composite: number;
depthSocial: number;
depthEconomic: number;
depthAccess: number;
depthVouch: number;
tier: string;
}
interface EvaluationResult {
passed: boolean;
failures: Array<{ field: string; value: number; minimum: number }>;
}
function evaluateThresholds(
envelope: ReputationEnvelope | null,
thresholds: ReputationThresholds
): EvaluationResult {
if (!envelope) return { passed: false, failures: [] };
const axes = [
{ field: 'composite', value: envelope.composite, min: thresholds.composite },
{ field: 'depthSocial', value: envelope.depthSocial, min: thresholds.depthSocial },
{ field: 'depthEconomic', value: envelope.depthEconomic, min: thresholds.depthEconomic },
{ field: 'depthAccess', value: envelope.depthAccess, min: thresholds.depthAccess },
{ field: 'depthVouch', value: envelope.depthVouch, min: thresholds.depthVouch },
];
const failures = axes
.filter(axis => axis.value < axis.min)
.map(axis => ({ field: axis.field, value: axis.value, minimum: axis.min }));
return {
passed: failures.length === 0,
failures
};
}
Architecture Decisions and Rationale
- Fail-Closed Default: When the identity oracle is unreachable, the middleware denies access rather than allowing unscored traffic. This prevents sybil networks from exploiting downtime. Operators can override this in development environments, but production deployments should maintain strict closure.
- Strict AND Logic Across Axes: Using OR logic would allow callers to bypass weak axes by overperforming on others. AND logic ensures baseline legitimacy across all dimensions, which is critical for preventing coordinated abuse.
- Structured Rejection Payloads: Returning a
failed array with field-level details enables agent runtimes to implement self-improvement loops. Instead of treating a 403 as a terminal error, agents can adjust behavior, accumulate reputation, and retry.
- Oracle Timeout Isolation: Identity scoring introduces network latency. Wrapping the oracle call in a timeout prevents slow reputation lookups from blocking the payment verification path or causing request queue saturation.
Pitfall Guide
1. Treating Identity Axes as OR Logic
Explanation: Allowing callers to pass if they meet any single threshold creates exploitable gaps. A sybil network can artificially inflate one axis while remaining weak across others.
Fix: Enforce strict AND evaluation across all configured axes. Document the threshold matrix clearly so operators understand that every axis acts as a hard gate.
2. Ignoring Fail-Closed Defaults During Oracle Outages
Explanation: If the identity oracle returns a timeout or 5xx error and the middleware defaults to fail-open, attackers can flood the endpoint during degradation windows.
Fix: Implement explicit fail-closed behavior with circuit breakers. Cache last-known good scores with a short TTL (e.g., 60 seconds) to maintain continuity during brief oracle blips without compromising security.
3. Hardcoding Thresholds Without Drift Monitoring
Explanation: Reputation scores decay over time. A threshold that blocks low-signal bots today may inadvertently block legitimate agents whose economic activity has naturally slowed.
Fix: Implement threshold drift monitoring. Log rejection rates per axis and trigger alerts when false-positive rates exceed 5%. Adjust thresholds dynamically based on rolling 7-day rejection analytics.
4. Returning Opaque Rejection Payloads
Explanation: Sending a generic 403 Forbidden without axis-level details breaks agent interoperability. Runtimes cannot distinguish between payment failure, identity failure, or rate limiting.
Fix: Always return structured JSON with error, rank, and failed arrays. Include the exact field, current value, and minimum required. This enables automated retry logic and reduces manual debugging.
5. Confusing Economic vs Social Depth
Explanation: Economic depth measures on-chain capital commitment and transaction history. Social depth measures vouches, network participation, and community signals. Treating them as interchangeable misaligns threat models.
Fix: Map axes to specific threat vectors. Use depthEconomic to filter automated scrapers and sybil farms. Use depthSocial to filter low-effort spam. Adjust thresholds independently based on the tool's sensitivity.
6. Neglecting Rate Limiting Post-Verification
Explanation: Passing identity and payment checks does not grant infinite access. High-reputation agents can still be compromised or misused, leading to resource exhaustion.
Fix: Layer traditional rate limiting on top of reputation gating. Use reputation scores to adjust rate limits dynamically: higher composite scores receive elevated ceilings, while emerging tiers face stricter caps.
7. Overlooking Macaroon Preimage Validation
Explanation: L402 relies on cryptographic preimages to prove payment. Skipping preimage verification or trusting client-provided metadata allows replay attacks.
Fix: Always validate macaroons against the LNBits invoice API. Verify that the preimage matches the settled invoice and that the macaroon has not been revoked. Implement macaroon expiration checks to prevent long-term token reuse.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-volume public API | Identity-Aware L402 with strict AND logic | Prevents sybil flooding while allowing legitimate traffic to pass after reputation accumulation | Moderate (oracle queries + LNBits fees) |
| Enterprise agent fleet | Reputation-Aware with cached scores & elevated thresholds | Reduces latency for known agents while maintaining baseline security | Low (cache hit rate >85%) |
| Open research network | Identity-Aware with relaxed social thresholds | Encourages participation while filtering automated scrapers via economic depth | Low-Moderate |
| Pay-per-call premium tool | Standard L402 + strict economic gating | Maximizes revenue per call without complex reputation logic | Minimal |
Configuration Template
import { reputationL402Middleware } from './middleware/reputation-gate';
const gateConfig = {
hmacSecret: process.env.GATE_HMAC_SECRET!,
lnbitsEndpoint: process.env.LNBITS_API_URL!,
lnbitsInvoiceKey: process.env.LNBITS_INVOICE_KEY!,
baseSatAmount: 10,
thresholds: {
composite: 10,
depthSocial: 5,
depthEconomic: 3,
depthAccess: 2,
depthVouch: 1
},
failClosed: true,
oracleTimeoutMs: 2000
};
app.use('/mcp', reputationL402Middleware(gateConfig));
Quick Start Guide
- Install dependencies: Add
express, @lnbits/sdk, and your preferred HTTP client to your MCP server project.
- Configure LNBits: Create an invoice key with macaroon verification permissions. Set
LNBITS_API_URL and LNBITS_INVOICE_KEY environment variables.
- Deploy the middleware: Attach the reputation gate to your MCP route. Set
failClosed: true for production and configure thresholds based on your threat model.
- Test rejection paths: Use a low-reputation pubkey to trigger a 403. Verify that the response includes the
failed array with axis-level details.
- Monitor and tune: Track rejection rates per axis over 7 days. Adjust thresholds if false positives exceed 5% or if legitimate agents report access friction.