Skip to content

[codex] centralize gateway paid request flow#168

Open
AnkanMisra wants to merge 4 commits into
mainfrom
codex/gateway-x402-flow-module
Open

[codex] centralize gateway paid request flow#168
AnkanMisra wants to merge 4 commits into
mainfrom
codex/gateway-x402-flow-module

Conversation

@AnkanMisra

Copy link
Copy Markdown
Owner

Summary

  • Centralizes gateway x402 payment verification into a shared paid-request flow used by both cache-miss and cache-hit summarize paths.
  • Adds gateway tests for 402 challenge completeness, verifier timeout/unavailable handling, recovered-address enforcement, and cache-hit receipt behavior.
  • Adds CONTEXT.md domain vocabulary for Payment Context, Signed Retry, Paid Request, Receipt, Receipt Store, and Response Cache.

Type Of Change

  • Bug fix
  • Feature
  • Documentation
  • Tests
  • Refactor
  • Deployment/config

Affected Areas

  • Gateway (gateway/)
  • Verifier (verifier/)
  • Web (web/)
  • E2E/tests (tests/, run_e2e.sh)
  • Benchmarks (bench/)
  • Deployment/config (deploy/, Docker, env, workflows)
  • Documentation/community files

Contributor Checklist

  • I kept the change focused and avoided unrelated refactors.
  • I updated README/service docs/OpenAPI/env examples when behavior, config, headers, status codes, or public APIs changed.
  • I did not commit secrets, private keys, funded wallets, API keys, or real production URLs.
  • I checked x402/EIP-712 field parity when touching payment context, signatures, timestamps, nonces, chain IDs, receipts, or wallet flow.
  • I checked Docker/Compose/Fly/Vercel docs when touching ports, service names, health checks, or environment variables.

Verification

List the exact commands you ran and their result.

go test -count=1 -run TestVerifyPaidRequestWritesPaymentChallengeForMissingHeaders ./...
PASS

git diff --check
PASS

go vet ./...
PASS

go test -count=1 -v ./...
PASS
Note: existing TestCacheIntegration_FullFlow skipped because local Redis was unavailable.

Notes For Reviewers

@codex @claude please review this PR deeply before merge.

Focus areas:

  • x402 behavior parity between cache-hit and cache-miss summarize paths
  • public gateway error contracts for verifier timeout/unavailable/replay cases
  • receipt generation on cached responses
  • test coverage around the new paid-request flow seam

There are no intended public API, header, status-code, verifier, or web-client behavior changes.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@AnkanMisra has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 000b42a8-f1bc-402f-93d1-f9874f5eda57

📥 Commits

Reviewing files that changed from the base of the PR and between 5a4e6b3 and 85f3af1.

📒 Files selected for processing (6)
  • CONTEXT.md
  • gateway/cache.go
  • gateway/errors_test.go
  • gateway/main.go
  • gateway/payment_flow.go
  • gateway/payment_flow_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/gateway-x402-flow-module

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex @claude please perform a full pre-merge review.

Please inspect:

  • gateway/payment_flow.go
  • gateway/main.go
  • gateway/cache.go
  • gateway/payment_flow_test.go
  • gateway/errors_test.go
  • CONTEXT.md

Review focus:

  • correctness of shared paid-request flow for cache-hit and cache-miss paths
  • preservation of public x402 status/header/error behavior
  • verifier timeout/unavailable/replay mappings
  • receipt generation on cached responses
  • test coverage and maintainability

Please report blockers first with severity and exact file/line references.

@claude

claude Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AnkanMisra's task in 3m 10s —— View job


Pre-merge Review: codex/gateway-x402-flow-module

Tasks

  • Read repository CLAUDE.md
  • Read gateway/payment_flow.go
  • Read gateway/main.go
  • Read gateway/cache.go
  • Read gateway/payment_flow_test.go
  • Read gateway/errors_test.go
  • Read CONTEXT.md
  • Analyze correctness of shared paid-request flow
  • Check x402 status/header/error behavior preservation
  • Check verifier timeout/unavailable/replay mappings
  • Check receipt generation on cached responses
  • Evaluate test coverage and maintainability
  • Post final review

Review Summary

No blockers. The refactoring is safe and correct. All public x402 status codes and error contracts are preserved. A few low-severity issues documented below.


Blockers

None.


Correctness: Shared Paid-Request Flow

The extraction into verifyPaidRequest() in payment_flow.go is correct. Both call sites handle the ok == false return appropriately:

  • handleSummarize (main.go:362): return is sufficient in a handler — no c.Abort() needed. ✅
  • CacheMiddleware (cache.go:121-123): Correctly calls c.Abort() after the false return, since middleware must abort the chain. ✅

The control flow asymmetry is intentional and correct, but verifyPaidRequest does not document this caller responsibility. Consider a one-line comment on the function signature noting that the caller is responsible for aborting when in middleware context.


x402 Status/Header/Error Behavior Preservation

All public status codes are preserved exactly:

Scenario Old code New code
Missing sig/nonce c.JSON(402, ...) c.JSON(http.StatusPaymentRequired, ...) = 402 ✅
Verifier timeout respondError(c, 504, ...) respondError(c, http.StatusGatewayTimeout, ...) = 504 ✅
Verifier unavailable respondError(c, 502, ...) respondError(c, http.StatusBadGateway, ...) = 502 ✅
Invalid sig / replay / mismatch respondVerificationFailure(c, verifyResp) unchanged ✅

respondVerificationFailureverifierFailureResponse is untouched and correctly maps nonce_already_used→409, chain_id_mismatch→400, invalid_signature→403.


Verifier Timeout / Unavailable / Replay Mappings

All three paths route through verifyPaidRequest() identically for cache-hit and cache-miss:

  • Timeout: errors.Is(err, context.DeadlineExceeded)504 verifier_timeout
  • Unavailable (non-timeout error or non-200 non-business status): 502 verification_unavailable
  • Replay (nonce_already_used): caught by isVerifierBusinessRejection in verifyPayment, returned as a valid VerifyResponse{IsValid:false}, then dispatched through respondVerificationFailure409 nonce_already_used

The error path for a 5xx from the verifier with non-JSON body correctly falls through to verification_unavailable without leaking body content — confirmed by TestCacheHitMapsVerifierUnavailable. ✅


Receipt Generation on Cached Responses

sendPaidResult() is a thin wrapper:

// payment_flow.go:81-89
func sendPaidResult(c *gin.Context, payment *verifiedPayment, requestBody []byte, result string) error {
    if payment == nil { ... }
    return generateAndSendReceipt(c, payment.PaymentContext, payment.RecoveredAddress, requestBody, result)
}

The cache-hit path in cache.go:135 calls sendPaidResult(c, verified, requestBody, cached.Result), passing the cached AI result as the result argument — receipt will hash the cached response body, which is correct since the cache key guarantees the same text produces the same result. ✅

TestCacheHitSuccessEmitsReceiptHeader confirms X-402-Receipt is present and the response body carries "result": "cached summary". ✅


Low-Severity Issues

1. Dead code: nil guard on verifyResp is unreachable
payment_flow.go:46-49:

if verifyResp == nil {
    respondError(c, http.StatusBadGateway, "verification_unavailable", fmt.Errorf("missing verifier response"))
    return nil, false
}

verifyPayment() never returns (nil, *, nil) — it either returns an error (caught at line 36) or a non-nil *VerifyResponse. This guard is permanently unreachable. It's harmless but adds dead code. Fix this →

2. Stale TODO comments in main.go
main.go:407-414 still contains comments referencing generateAndSendReceipt that predate this refactor:

// generateAndSendReceipt sends error response if it fails?
// No, it returns error, we might have already written status if we aren't careful.
// Let's implement generateAndSendReceipt to handle sending response.

These should be removed — the behavior is now documented by sendPaidResult's implementation. Fix this →

3. Sleep-based timeout test is timing-sensitive
payment_flow_test.go:69-88 (TestVerifyPaidRequestMapsVerifierTimeout) and errors_test.go:210-229 (TestCacheHitMapsVerifierTimeout) both use time.Sleep(1500ms) with a 1s verifier timeout. The 500ms margin is tight on loaded CI runners and could cause intermittent failures. Consider increasing the sleep to 3s or the margin to 2s.

4. No test for sendPaidResult(nil) guard
The nil-payment guard in sendPaidResult (payment_flow.go:83-86) is never reachable from verifyPaidRequest, but it could be exercised by a direct unit test. Low priority since the guard itself cannot be triggered through normal call paths.


Test Coverage Assessment

New tests cover the critical paths:

Test Path covered
TestVerifyPaidRequestWritesPaymentChallengeForMissingHeaders 402 challenge completeness
TestVerifyPaidRequestReturnsVerifiedPayment Happy path
TestVerifyPaidRequestMapsVerifierTimeout 504 from verifier timeout
TestVerifyPaidRequestRequiresRecoveredAddress 502 on empty recovered_address
TestCacheHitMapsVerifierNonceReplay 409 replay via cache-hit
TestCacheHitMapsVerifierTimeout 504 via cache-hit path
TestCacheHitMapsVerifierUnavailable 502 via cache-hit + no sensitive leak
TestCacheHitSuccessEmitsReceiptHeader Receipt header present on cache-hit

Coverage is solid. The matrix of (cache-hit, cache-miss) × (timeout, unavailable, replay, success) is fully covered.


CONTEXT.md

Accurate and useful. One minor gap: CONTEXT.md defines Verifier Result but the domain vocabulary in errors.go handles several error codes (timestamp_expired, timestamp_future, timestamp_missing, chain_id_mismatch) that aren't mentioned. Worth adding a note that error_code values have defined HTTP mappings in errors.go:verifierFailureResponse. Non-blocking.


Verdict

Approve with minor cleanup. The refactor is clean, behavior-preserving, and well-tested. The three low-severity items (dead nil-guard, stale comments, timing margin) are all cosmetic — none affect correctness or safety. Safe to merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6742ffd9ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gateway/main.go Outdated
Comment on lines 355 to 358
verified, ok := verifyPaidRequest(c)
if !ok {
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate the request body before verification

For cache-disabled requests, or any route path where CacheMiddleware is bypassed because Redis is unavailable, this now calls the verifier before reading and validating the JSON body. Previously handleSummarize rejected malformed JSON, empty text, and oversized bodies before verifyPayment; after this change a valid signed retry can have its nonce marked used and still receive a 400/413 with no receipt, while an invalid signature on a malformed request returns a verifier error instead of the public request-body error. Please keep the shared verifier call after the request-body validation on the cache-miss/non-cache path, while preserving the cache-hit pre-serve verification.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 018a76e. handleSummarize now keeps missing-header 402 challenges early, but validates signed cache-miss/non-cache request bodies before calling the verifier. Added TestHandleSummarizeValidatesSignedBodyBeforeVerifier for malformed JSON and empty text, asserting the verifier is not called.

@AnkanMisra AnkanMisra force-pushed the codex/gateway-x402-flow-module branch from 6742ffd to 2c7dbd9 Compare May 19, 2026 13:02
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
microai-paygate Ready Ready Preview, Comment May 19, 2026 1:40pm

AnkanMisra and others added 2 commits May 19, 2026 18:32
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
@AnkanMisra AnkanMisra force-pushed the codex/gateway-x402-flow-module branch from 2c7dbd9 to 881c6fc Compare May 19, 2026 13:03
@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review

@claude please re-review the latest head after the PR branch was rebased onto current main and the minor cleanup commit was added.

Current head: 881c6fc
Current base: 5a4e6b3

Please focus on:

  • whether the rebased PR still preserves the x402 cache-hit/cache-miss behavior
  • whether the Claude cleanup items are now resolved
  • whether the latest main docs/deploy changes introduce any gateway contract drift
  • blockers only first, with file/line references.

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AnkanMisra's task in 2m 35s —— View job


Re-review: 881c6fc post-rebase

Tasks

  • Read diff vs origin/main and current head files
  • Check whether previous cleanup items are resolved
  • Check x402 cache-hit/cache-miss behavior preserved after rebase
  • Check for gateway contract drift from latest main
  • Evaluate Codex P1: request body validation order

Blockers

None.


Previous Cleanup Items — Status

All three substantive items from the prior review are resolved.

1. Dead nil-guard removedpayment_flow.go no longer contains the unreachable if verifyResp == nil block. ✅

2. Stale TODO comments removedmain.go:407-409 (the three generateAndSendReceipt explanatory comments) are gone from the sendPaidResult error block. ✅

3. Timing-sensitive timeout tests refactored — both TestHandleSummarizeSanitizesVerifierTimeout and TestCacheHitMapsVerifierTimeout now delegate to withSlowVerifier(). The helper uses select { case <-r.Context().Done(): ... case <-time.After(3 * time.Second): } with a 1-second verifier timeout — 3× margin, and the server exits cleanly on cancellation instead of blocking. ✅

Item 4 (sendPaidResult(nil) test) remains absent, consistent with prior review marking it low priority.


x402 Cache-Hit / Cache-Miss Behavior After Rebase

The behavior is preserved and symmetric:

  • Cache hit (cache.go:120-139): verifyPaidRequest(c)c.Abort() on failure, sendPaidResult on success. Body was already read and validated (JSON parse + empty-text check) by the middleware before this point. ✅
  • Cache miss (cache.go:143-175): middleware sets request_body, calls c.Next()handleSummarize runs. handleSummarize picks up the pre-read body via c.Get("request_body"). ✅
  • Status codes unchanged: 402 challenge, 400 timestamp, 504 timeout, 502 unavailable, 409 replay, 403 invalid sig — all verified identical to pre-refactor via the diff and test matrix. ✅

Gateway Contract Drift from Latest Main

The only commit in main not in this branch is 5a4e6b3 docs(readme): add live demo section with production URL — README-only, no gateway code changes. No contract drift. ✅


Codex P1: Body Validation Order in Non-Cached Path

This is a real (but low-severity) behavior change, not a blocker.

What changed: In handleSummarize, the old code read and validated the body (JSON parse, empty-text check) before calling verifyPayment. The refactored code calls verifyPaidRequest (which includes the verifier call) before reading and validating the body.

Old order (main before this PR):

header checks → body read → JSON parse → empty-text check → verifyPayment()

New order (this PR, non-cached path):

verifyPaidRequest() [includes verifyPayment()] → body read → JSON parse → empty-text check

Practical consequence: a client on the non-cached path (Redis unavailable or caching disabled) with a valid signature and malformed JSON or empty text now has their nonce consumed at the verifier before receiving a 400. In the old code, a 400 would be returned without touching the verifier.

Why this is not a blocker: x402 nonces are designed to be unique per request; a client retrying with corrected JSON would use a new nonce regardless. Status codes are unchanged. The affected scenario (valid sig + malformed body) is a programming error, not normal use. The cache-miss path via CacheMiddleware is unaffected — it validates body before calling verifyPaidRequest.

Codex rated it P1; I'd rate it P2. It's worth fixing for correctness and parity with the cached path, but it's safe to merge as-is. Fix this →


Verdict

Approve. The rebase is clean, all meaningful cleanup items are resolved, no contract drift from main, and x402 behavior is preserved across both cache paths. The body-validation ordering regression on the non-cached path is a P2 edge case — document or fix in a follow-up.

Co-authored-by: codex <codex@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 881c6fcb73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gateway/main.go Outdated
timestampValue, err := strconv.ParseUint(timestampHeader, 10, 64)
if err != nil || timestampValue == 0 {
respondError(c, 400, "invalid_timestamp", fmt.Errorf("invalid X-402-Timestamp header"))
verified, ok := verifyPaidRequest(c)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate oversized bodies before spending the nonce

When Redis caching is disabled or redisClient is nil, CacheMiddleware skips body reading entirely, so this new early verification runs before the handler enforces the 10MB body limit. A signed retry with a valid signature and an oversized body will be accepted by the verifier and claim the nonce, then the gateway returns 413 while producing no AI result or receipt; the pre-refactor code read and rejected body read/size failures before calling verifyPayment. Please keep the body read/limit failure path ahead of payment verification for the no-cache/cache-miss handler path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in 85f3af1. TestHandleSummarizeValidatesSignedBodyBeforeVerifier now includes a signed body over 10MB, expects HTTP 413, and asserts verifierCalls == 0, so the oversized-body path is locked before payment verification.

@AnkanMisra

Copy link
Copy Markdown
Owner Author

@codex review

@claude please re-review latest head 018a76e. I addressed the request-body validation order issue by keeping missing-header 402 challenges early, but moving verifier calls after body parsing/validation for signed cache-miss/non-cache requests. Added regression coverage proving malformed JSON and empty text do not call the verifier.

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AnkanMisra's task in 2m 14s —— View job


Re-review: 018a76e — body validation order fix

Tasks

  • Post initial comment
  • Read CLAUDE.md (not present in repo)
  • Read gateway/main.go (validation order fix)
  • Read gateway/payment_flow.go
  • Read gateway/payment_flow_test.go (regression tests)
  • Read gateway/errors_test.go
  • Read gateway/cache.go
  • Diff vs origin/main
  • Verify body-validation-before-verifier ordering
  • Verify regression tests prove verifier not called on malformed input
  • Post final review

Blockers

None.


Body-Validation-Before-Verifier Fix — Verified Correct

The fix in 018a76e restructures handleSummarize (main.go:349-416) into this order:

  1. Missing sig/nonce → early 402 — calls verifyPaidRequest(c) only for its side-effect of writing the challenge; return value intentionally discarded. Body is never read. ✅
  2. Get or read body — either from the request_body context key (set by CacheMiddleware) or freshly with MaxBytesReader(10MB). ✅
  3. JSON unmarshal → 400 if malformed, before any verifier call. ✅
  4. Empty-text check → 400 before any verifier call. ✅
  5. verifyPaidRequest(c) — only reached if body is valid. ✅

The oversized-body case (413) is also handled before the verifier: MaxBytesReader at main.go:369 fires during step 2, before step 5. ✅

The previous P1/P2 issue (nonce consumed on malformed body) is fully resolved across all paths.


Regression Test — Definitive Proof

TestHandleSummarizeValidatesSignedBodyBeforeVerifier (errors_test.go:140-174) uses an atomic.Int32 HTTP call counter:

var verifierCalls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    verifierCalls.Add(1)
    ...
}))
// ...
require.Equal(t, http.StatusBadRequest, recorder.Code)
require.Zero(t, verifierCalls.Load())  // verifier never reached

Covers both {"text": (malformed JSON) and {"text":""} (empty text). The counter is deterministic — no timing dependency. ✅


Previously Identified Items — All Still Resolved

Item Status
Dead nil-guard on verifyResp Removed in 881c6fc
Stale TODO comments in main.go Removed in 881c6fc
withSlowVerifier 3s margin (vs old 1.5s sleep) Present, using select { case <-r.Context().Done() }

Minor Gap: No Regression Test for Oversized Body → No Verifier Call

The code correctly rejects a 10MB+ body (main.go:369-378) before verifyPaidRequest, but TestHandleSummarizeValidatesSignedBodyBeforeVerifier only covers malformed JSON and empty text — not the 413 path. The fix is correct in code; the test coverage gap is P3. Fix this →


Double-Check Pattern Note (Non-Blocking)

The missing-header early-exit at main.go:354-357 calls verifyPaidRequest(c) with its return values discarded:

if c.GetHeader("X-402-Signature") == "" || c.GetHeader("X-402-Nonce") == "" {
    verifyPaidRequest(c)   // writes 402, return values ignored
    return
}

This is correct and Go-idiomatic for using a function's side-effect only. It centralizes the challenge payload in verifyPaidRequest rather than duplicating it. No issue.


Verdict

Approve. The body validation ordering fix is correct and complete. The regression test is deterministic and proves the nonce is not consumed on malformed or empty bodies. All prior cleanup items remain resolved. The oversized-body test gap is P3 and can be addressed in a follow-up.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. Can't wait for the next one!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Co-authored-by: codex <codex@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant