Potential x402 settlement gap: settlement failure is treated as success before sweep execution
Hi, I noticed a possible payment-flow issue in the current repository state. This is a conservative report based on the current code path, and I may be missing deployment-specific guards outside this repository.
Reviewed HEAD: a09e550e
What I observed
The x402 middleware verifies fields and then settlement errors from the facilitator are converted to { success: true }; the paid sweep route proceeds after the middleware.
Source scan notes:
- scan_uncovered1000_20260604_batch31: True x402 paid sweep API. Verification fails open when strict verification is unset, EIP-3009 signature verification is effectively TODO/returns valid, and settlement is launched in the background/non-blocking before the paid sweep job is queued and returned.
Relevant code excerpts:
src/api/middleware/x402.ts:275-342
275 async function verifyPayment(
276 payment: PaymentPayload,
277 config: X402Config
278 ): Promise<PaymentVerificationResult> {
279 try {
280 const { authorization } = payment.payload;
281
282 // Verify basic fields
283 if (authorization.to.toLowerCase() !== config.receiverAddress.toLowerCase()) {
284 return { valid: false, error: "Invalid receiver address" };
285 }
286
287 const requiredAmount = BigInt(centsToUsdcAmount(config.amountCents));
288 const providedAmount = BigInt(authorization.value);
289
290 if (providedAmount < requiredAmount) {
291 return { valid: false, error: "Insufficient payment amount" };
292 }
293
294 // Check validity window
295 const now = Math.floor(Date.now() / 1000);
296 const validAfter = parseInt(authorization.validAfter);
297 const validBefore = parseInt(authorization.validBefore);
298
299 if (now < validAfter) {
300 return { valid: false, error: "Payment not yet valid" };
301 }
302
303 if (now > validBefore) {
304 return { valid: false, error: "Payment expired" };
305 }
306
307 // Check if nonce was already used (replay protection)
308 const redis = getRedis();
309 const nonceKey = `${NONCE_PREFIX}${authorization.from}:${authorization.nonce}`;
310 const used = await redis.get(nonceKey);
311
312 if (used) {
313 return { valid: false, error: "Nonce already used" };
314 }
315
316 // Check for cached payment proof (already verified)
317 const paymentHash = generatePaymentHash(payment);
318 const cachedReceipt = await getCachedPaymentProof(paymentHash);
319 if (cachedReceipt) {
320 return { valid: true, txHash: cachedReceipt.txHash };
321 }
322
323 // Perform on-chain verification
324 const onChainResult = await verifyPaymentOnChain({
325 payer: authorization.from as `0x${string}`,
326 payee: authorization.to as `0x${string}`,
327 amount: authorization.value,
328 signature: payment.payload.signature,
329 nonce: authorization.nonce,
330 validAfter: authorization.validAfter,
331 validBefore: authorization.validBefore,
332 });
333
334 if (!onChainResult.valid) {
335 return { valid: false, error: onChainResult.error || "On-chain verification failed" };
336 }
337
338 // Mark nonce as used (expires after validBefore + buffer)
339 const ttl = validBefore - now + 3600; // 1 hour buffer
340 await redis.setex(nonceKey, ttl, "1");
341
342 return { valid: true, txHash: onChainResult.txHash };
src/api/middleware/x402.ts:352-390
352 async function settlePayment(
353 payment: PaymentPayload,
354 receipt: PaymentReceipt
355 ): Promise<{
356 success: boolean;
357 txHash?: string;
358 error?: string;
359 }> {
360 try {
361 // Use the x402 facilitator's /settle endpoint
362 const facilitatorUrl = process.env.X402_FACILITATOR_URL || "https://x402.org";
363
364 const settleResponse = await fetch(`${facilitatorUrl}/settle`, {
365 method: "POST",
366 headers: { "Content-Type": "application/json" },
367 body: JSON.stringify({
368 x402Version: X402_VERSION,
369 payment,
370 receipt,
371 }),
372 });
373
374 if (!settleResponse.ok) {
375 const errorData = await settleResponse.json().catch(() => ({}));
376 console.error("[x402] Settlement API error:", errorData);
377 // Settlement failure is non-blocking - payment was verified
378 return { success: true, txHash: receipt.txHash };
379 }
380
381 const result = await settleResponse.json() as { txHash?: string };
382 return {
383 success: true,
384 txHash: result.txHash || receipt.txHash,
385 };
386 } catch (error) {
387 console.error("[x402] Payment settlement error:", error);
388 // Settlement failure is non-blocking - we don't fail the request
389 return { success: true, txHash: receipt.txHash };
390 }
src/api/routes/sweep.ts:36-52
36 sweep.post(
37 "/",
38 authMiddleware,
39 // Apply x402 payment middleware if configured ($0.10 per sweep)
40 async (c, next) => {
41 if (isX402Configured()) {
42 const middleware = x402Middleware({
43 amountCents: 10,
44 receiverAddress: getX402ReceiverAddress(),
45 description: "Sweep sweep execution fee ($0.10)",
46 });
47 return middleware(c, next);
48 }
49 return next();
50 },
51 zValidator("json", sweepRequestSchema),
52 async (c) => {
Why this may matter
For paid API, agent, MCP, x402, AP2/UCP, or subscription-gated flows, the payment proof needs to dominate the protected release path. If verification is only structural, settlement is best-effort after release, payment fields are not rebound to server requirements, or the protected resource is reachable outside the paid route, a caller may receive the paid result without a completed payment for the intended resource, amount, asset, or recipient.
Suggested check
Consider making the paid-resource release depend on a payment state that is both verified and settled, or otherwise cryptographically bound to the current server-side requirements (resource, amount, asset, payTo, payer, nonce/idempotency key, and route/session). For intentionally asynchronous settlement, it may be worth failing closed on settlement errors or recording a durable pending state with explicit recovery/reconciliation semantics before returning the protected result.
Conservative caveat
This report is based on the repository code at the reviewed HEAD. If production uses an external gateway, webhook, facilitator, deployment setting, or middleware not present here that enforces the missing binding/settlement step before release, the practical impact may be lower.
Potential x402 settlement gap: settlement failure is treated as success before sweep execution
Hi, I noticed a possible payment-flow issue in the current repository state. This is a conservative report based on the current code path, and I may be missing deployment-specific guards outside this repository.
Reviewed HEAD:
a09e550eWhat I observed
The x402 middleware verifies fields and then settlement errors from the facilitator are converted to
{ success: true }; the paid sweep route proceeds after the middleware.Source scan notes:
Relevant code excerpts:
src/api/middleware/x402.ts:275-342src/api/middleware/x402.ts:352-390src/api/routes/sweep.ts:36-52Why this may matter
For paid API, agent, MCP, x402, AP2/UCP, or subscription-gated flows, the payment proof needs to dominate the protected release path. If verification is only structural, settlement is best-effort after release, payment fields are not rebound to server requirements, or the protected resource is reachable outside the paid route, a caller may receive the paid result without a completed payment for the intended resource, amount, asset, or recipient.
Suggested check
Consider making the paid-resource release depend on a payment state that is both verified and settled, or otherwise cryptographically bound to the current server-side requirements (
resource,amount,asset,payTo, payer, nonce/idempotency key, and route/session). For intentionally asynchronous settlement, it may be worth failing closed on settlement errors or recording a durable pending state with explicit recovery/reconciliation semantics before returning the protected result.Conservative caveat
This report is based on the repository code at the reviewed HEAD. If production uses an external gateway, webhook, facilitator, deployment setting, or middleware not present here that enforces the missing binding/settlement step before release, the practical impact may be lower.