Skip to content

Potential settlement-ordering issue: paid resource can be released without completed settlement #33

Description

@chenshj73

Potential settlement-ordering issue: paid resource can be released without completed settlement

Hi, I noticed a possible payment-flow issue while reviewing the current repository state. This is a conservative report based on the visible code path, and I may be missing deployment-specific guards outside this repository.

Reviewed HEAD: bf1f63de9018

What I observed

In the visible payment path, I observed:

x402 gateway advertises paid MCP tools and per-tool prices; middleware verifies X-Payment then calls next() without invoking settle(), while PaymentVerifier has an unused settle() method. The verifier also returns cached positive results before rechecking expectedAmount/payTo/network, and its cache key excludes tool/resource/amount, allowing a previously verified lower-priced payment to be reused within TTL against a different paid tool.

Relevant code locations:

  • README.md
  • src/gateway/x402-server.ts
  • src/gateway/payment-verifier.ts
  • src/gateway/pricing.ts

Relevant code excerpts

README.md:200-224

 200 
 201 - 🔄 **Swap/DEX** - Token swaps via 1inch, 0x, ParaSwap
 202 - 🌉 **Bridge** - Cross-chain transfers via LayerZero, Stargate, Wormhole
 203 - ⛽ **Gas** - Gas prices across chains, EIP-1559 suggestions
 204 - 📦 **Multicall** - Batch read/write operations
 205 - 📊 **Events/Logs** - Query historical events, decode logs
 206 - 🔒 **Security** - Rug pull detection, honeypot check, GoPlus token/address security, dApp phishing detection
 207 - 💰 **Staking** - Liquid staking (Lido), LP farming
 208 - ✍️ **Signatures** - Sign messages, verify signatures, EIP-712
 209 - 🏦 **Lending** - Aave/Compound positions, borrow rates
 210 - 📈 **Price Feeds** - Historical prices, TWAP, oracle aggregation
 211 - 📁 **Portfolio** - Track holdings across chains
 212 - 🏛️ **Governance** - Snapshot votes, on-chain proposals
 213 - 🚀 **Deployment** - Deploy contracts, CREATE2, upgradeable proxies, verification
 214 - 🛡️ **MEV Protection** - Flashbots Protect, private transactions, bundle simulation
 215 - 🆔 **ENS/Domains** - Register, transfer, renew, set records, subdomains
 216 - 📊 **Market Data** - CoinGecko & CoinStats prices, OHLCV, trending, categories, exchanges
 217 - 🌐 **DeFi Analytics** - DefiLlama TVL, yields, fees, bridges, stablecoins, protocol data
 218 - 💬 **Social Sentiment** - LunarCrush social metrics, influencers, trending topics
 219 - 📈 **DEX Analytics** - DexPaprika & GeckoTerminal pools, trades, OHLCV, trending tokens
 220 - 🔮 **Predictions** - Polymarket prediction markets, crypto forecasts
 221 - 📉 **Technical Indicators** - 50+ indicators (RSI, MACD, Bollinger Bands, etc.)
 222 - 🔔 **Alerts** - Price alerts, whale movement alerts, gas alerts (NEW)
 223 - 📡 **WebSockets** - Real-time price streams, trade feeds, mempool monitoring (NEW)
 224 - 🐋 **Wallet Analytics** - Whale tracking, wallet scoring, behavior analysis (NEW)

src/gateway/x402-server.ts:98-122

  98       network: NETWORK,
  99       payTo: PAY_TO_ADDRESS,
 100     })
 101   }
 102 
 103   // Verify payment
 104   try {
 105     const paymentData = JSON.parse(Buffer.from(paymentSignature, "base64").toString())
 106     const verification = await paymentVerifier.verify(paymentData, {
 107       expectedAmount: pricing.priceUSDC,
 108       expectedPayTo: PAY_TO_ADDRESS,
 109       expectedNetwork: NETWORK,
 110     })
 111 
 112     if (!verification.valid) {
 113       return res.status(402).json({
 114         error: "Payment Invalid",
 115         message: verification.reason || "Payment verification failed",
 116       })
 117     }
 118 
 119     // Rate limiting check (even with valid payment)
 120     const clientId = paymentData.from || req.ip || "anonymous"
 121     const rateLimitResult = await rateLimiter.check(clientId, toolName)
 122     

src/gateway/payment-verifier.ts:241-265

 241         headers: {
 242           "Content-Type": "application/json",
 243         },
 244         body: JSON.stringify({ payment }),
 245       })
 246 
 247       if (!response.ok) {
 248         const error = await response.text()
 249         return { valid: false, reason: `Settlement failed: ${error}` }
 250       }
 251 
 252       const result = await response.json()
 253       return {
 254         valid: result.success === true,
 255         transactionHash: result.transactionHash,
 256         settledAt: result.settledAt,
 257       }
 258     } catch (error) {
 259       console.error("Settlement request failed:", error)
 260       return { valid: false, reason: "Settlement request failed" }
 261     }
 262   }
 263 
 264   /**
 265    * Generate cache key for a payment

src/gateway/pricing.ts:59-83

  59     category: "free",
  60   },
  61 
  62   // ═══════════════════════════════════════════════════════════════
  63   // MICRO TIER ($0.0001) - High-volume reads
  64   // ═══════════════════════════════════════════════════════════════
  65   "get_balance": {
  66     priceUSDC: "100",
  67     description: "Get wallet balance",
  68     rateLimit: 60,
  69     category: "read",
  70   },
  71   "get_token_balance": {
  72     priceUSDC: "100",
  73     description: "Get ERC-20 token balance",
  74     rateLimit: 60,
  75     category: "read",
  76   },
  77   "get_transaction": {
  78     priceUSDC: "100",
  79     description: "Get transaction details",
  80     rateLimit: 60,
  81     category: "read",
  82   },
  83   "get_transaction_receipt": {

Why this may matter

If the paid resource is returned after verification but before settlement is completed or enforced, a client may receive the protected result without a confirmed transfer.

Suggested check

Consider making the paid-resource path depend on server-trusted payment requirements and a completed payment state. In particular, re-check recipient/payTo, amount, asset, network, nonce/idempotency, resource binding, and settlement result at the exact point where the protected API/tool/content is released.

Conservative caveat

I only reviewed the code visible in this repository at the HEAD above. If deployment-specific middleware or an upstream service enforces the missing binding/settlement invariant, this may already be mitigated there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions