ity oracle.
Architecture Decisions
- Middleware Chaining over Inline Logic: Payment and identity checks are isolated as Express middleware. This allows independent testing, easy swapping of oracle providers, and clear separation between transport validation and business logic.
- Single-Use Macaroons: The L402 implementation enforces one-time macaroon consumption. Replays are rejected with a
409 Conflict, preventing token theft from granting persistent access.
- Chaintip-Anchored Scores: The DoI oracle ties reputation to a Bitcoin block height. The server validates that the returned score is recent enough to prevent replay attacks using old, inflated scores.
- Configurable Reputation Thresholds: Instead of hardcoding access rules, the system reads a minimum score from environment configuration. This allows operators to adjust requirements based on actual compute costs.
Implementation
import express, { Request, Response, NextFunction } from 'express';
import { createL402Gate, L402Context } from '@powforge/mcp-l402-gate';
import axios from 'axios';
const app = express();
app.use(express.json());
// Environment validation
const REQUIRED_ENV = [
'LN_GATE_HMAC',
'WALLET_INVOICE_KEY',
'WALLET_BASE_URL',
'REPUTATION_ORACLE_ENDPOINT',
'MIN_REPUTATION_THRESHOLD'
] as const;
REQUIRED_ENV.forEach(key => {
if (!process.env[key]) {
throw new Error(`Missing required configuration: ${key}`);
}
});
const HMAC_SECRET = process.env.LN_GATE_HMAC!;
const WALLET_KEY = process.env.WALLET_INVOICE_KEY!;
const WALLET_URL = process.env.WALLET_BASE_URL!;
const ORACLE_URL = process.env.REPUTATION_ORACLE_ENDPOINT!;
const MIN_SCORE = parseInt(process.env.MIN_REPUTATION_THRESHOLD!, 10);
// Initialize L402 payment gate
const l402Middleware = createL402Gate({
hmacSecret: HMAC_SECRET,
lnbitsUrl: WALLET_URL,
invoiceKey: WALLET_KEY,
macaroonExpiryMs: 300_000 // 5 minutes
});
// DoI reputation verification middleware
async function verifyCallerReputation(req: Request, res: Response, next: NextFunction) {
const callerPubkey = req.headers['x-caller-pubkey'] as string;
if (!callerPubkey) {
return res.status(400).json({ error: 'Missing caller identity header' });
}
try {
const oracleResponse = await axios.get(`${ORACLE_URL}/v1/score/${callerPubkey}`);
const { composite_score, chaintip_block, signature } = oracleResponse.data;
// Validate chaintip freshness (reject scores older than 6 blocks)
const currentHeight = await getCurrentBitcoinHeight();
if (currentHeight - chaintip_block > 6) {
return res.status(410).json({ error: 'Stale reputation score. Request refresh.' });
}
if (composite_score < MIN_SCORE) {
return res.status(403).json({
error: 'Insufficient reputation',
required: MIN_SCORE,
provided: composite_score
});
}
// Attach verified identity to request context
(req as any).verifiedIdentity = {
pubkey: callerPubkey,
score: composite_score,
blockAnchor: chaintip_block
};
next();
} catch (err) {
console.error('Oracle lookup failed:', err);
return res.status(502).json({ error: 'Identity verification service unavailable' });
}
}
// Mock helper for production blockchain height lookup
async function getCurrentBitcoinHeight(): Promise<number> {
const { data } = await axios.get('https://mempool.space/api/blocks/tip/height');
return data;
}
// MCP Tool Handler
app.post('/tools/fetch_market_metrics',
l402Middleware,
verifyCallerReputation,
async (req: Request, res: Response) => {
try {
const marketData = await axios.get('https://mempool.space/api/v1/fees/recommended');
const priceData = await axios.get('https://api.coinbase.com/v2/prices/BTC-USD/spot');
res.json({
tool: 'fetch_market_metrics',
status: 'success',
payload: {
btc_usd: priceData.data.data.amount,
fee_estimates: marketData.data,
caller_reputation: (req as any).verifiedIdentity.score
}
});
} catch (err) {
res.status(500).json({ error: 'Upstream data fetch failed' });
}
}
);
const PORT = process.env.PORT || 3100;
app.listen(PORT, () => {
console.log(`MCP gateway active on port ${PORT}`);
console.log(`Reputation threshold: ${MIN_SCORE}`);
});
Why This Structure Works
- Separation of Concerns: Payment validation (
l402Middleware) and identity verification (verifyCallerReputation) operate independently. If the oracle goes down, you can temporarily bypass reputation checks without breaking payment flows.
- Explicit Header Contract: The
x-caller-pubkey header is required before any oracle lookup. This prevents unnecessary network calls and enforces client-side identity assertion.
- Chaintip Validation: By comparing the oracle's block anchor against the current chain tip, the server rejects stale scores. Attackers cannot reuse a high score from a previous fork or historical snapshot.
- Graceful Degradation: The oracle call is wrapped in a try/catch that returns
502 rather than crashing the server. Production systems should implement circuit breakers and fallback caching for this layer.
Pitfall Guide
1. Admin Key Exposure in LNBits Configuration
Explanation: Using the LNBits admin key grants full wallet control, including balance transfers and key deletion. If the server is compromised, attackers can drain funds or revoke invoices.
Fix: Always use the invoice/read-only key. Scope permissions to minting and checking payment status only. Rotate keys quarterly.
2. Macaroon Replay Attacks
Explanation: L402 macaroons are designed for single use. If your implementation caches or reuses them, attackers can capture a valid macaroon and replay it indefinitely.
Fix: Enforce strict single-use validation. Track consumed macaroon IDs in a short-lived Redis store or in-memory LRU cache. Return 409 Conflict on duplicate submissions.
3. Oracle Latency Blocking Request Threads
Explanation: Synchronous oracle lookups can stall the event loop during network congestion or oracle downtime, causing cascading timeouts for all clients.
Fix: Implement async oracle calls with a timeout ceiling (e.g., 2000ms). Cache recent scores locally with a TTL matching the chaintip anchor window. Fail open with a warning or fail closed with a clear error, depending on your risk tolerance.
4. Threshold Misalignment with Compute Costs
Explanation: Setting MIN_REPUTATION_THRESHOLD too low invites abuse; setting it too high blocks legitimate users. Hardcoding values ignores actual API costs.
Fix: Map thresholds to operational tiers: 0 for free trials, 10 for standard API calls, 40 for GPU/ML workloads, 100 for destructive or high-cost side effects. Review metrics monthly and adjust.
Explanation: HTTP headers are case-insensitive per RFC 7230, but some clients send X-Caller-Pubkey while others send x-caller-pubkey. Direct string matching fails inconsistently.
Fix: Normalize headers before lookup: req.headers['x-caller-pubkey']?.toLowerCase(). Use middleware that standardizes incoming headers early in the chain.
6. Missing HMAC Secret Rotation Strategy
Explanation: The HMAC key signs L402 macaroons. If leaked, attackers can forge valid payment tokens without paying. Static secrets become liabilities over time.
Fix: Implement key versioning. Store HMAC_SECRET_V1 and HMAC_SECRET_V2. Validate against both during rotation windows. Automate rotation via CI/CD secrets management.
7. Chaintip Anchor Validation Bypass
Explanation: Skipping block height validation allows attackers to submit old, high scores from previous network states. The oracle signature remains valid, but the reputation is stale.
Fix: Always fetch the current Bitcoin block height and reject scores anchored more than 6 blocks in the past. Log warnings for scores approaching the cutoff.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Low-cost API wrapper (e.g., weather data) | L402 only, threshold 0 | Reputation overhead outweighs abuse risk | Minimal |
| Standard paid API (e.g., mempool.space, coinbase) | L402 + DoI, threshold 10 | Prevents Sybil farming while allowing new users | Low |
| GPU/ML inference endpoint | L402 + DoI, threshold 40 | High compute cost requires proven reputation | Medium |
| Destructive/financial operations | L402 + DoI, threshold 100 + manual review | Zero tolerance for abuse or replay | High |
| Internal/enterprise MCP gateway | Static keys + mTLS | Identity already managed via corporate IAM | None |
Configuration Template
# Server Configuration
PORT=3100
NODE_ENV=production
# L402 Payment Gate
LN_GATE_HMAC=<32-byte-hex-secret>
WALLET_BASE_URL=https://your-lnbits-instance.example
WALLET_INVOICE_KEY=<invoice-read-key>
# Identity Oracle
REPUTATION_ORACLE_ENDPOINT=https://identity.powforge.dev
MIN_REPUTATION_THRESHOLD=10
# Security & Performance
MACAROON_TTL_MS=300000
ORACLE_TIMEOUT_MS=2000
CHINTIP_MAX_AGE_BLOCKS=6
Quick Start Guide
- Initialize Project: Create a new TypeScript Express project and install dependencies:
npm install express axios @powforge/mcp-l402-gate dotenv.
- Configure Secrets: Generate an HMAC key, provision an LNBits invoice key, and populate the
.env template with your values.
- Deploy Middleware: Copy the core server structure into
src/index.ts, ensure environment validation runs on startup, and attach the L402 + reputation middleware to your tool routes.
- Test Payment Flow: Send a POST request without auth to trigger the
402 invoice response. Pay the bolt11 invoice, then retry with the Authorization: L402 header.
- Validate Reputation Gate: Include
x-caller-pubkey in subsequent requests. Verify that scores below your threshold return 403, while valid identities receive tool responses with embedded reputation metadata.