Skip to content

Support advertised chain options in the web wallet flow 228#250

Open
pisum-sativum wants to merge 5 commits into
AnkanMisra:mainfrom
pisum-sativum:Support-advertised-chain-options-in-the-web-wallet-flow-228
Open

Support advertised chain options in the web wallet flow 228#250
pisum-sativum wants to merge 5 commits into
AnkanMisra:mainfrom
pisum-sativum:Support-advertised-chain-options-in-the-web-wallet-flow-228

Conversation

@pisum-sativum

Copy link
Copy Markdown

Summary

Resolves #228

Updates the web client to dynamically read and interpret the supportedChains field from the gateway's 402 Payment Required context. The client now intelligently skips network switches if the connected wallet is already on a supported alternative network, while still safely prompting a switch to the default expected chain when on an unsupported one.

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.

cd web
bun run lint
# Output: No errors

bun run build
# Output: ✓ Compiled successfully

bun test src/hooks/use-x402.test.ts
# Output: 3 pass

Screenshots

N/A

Notes For Reviewers

The wallet-widget component intentionally remains tied to the EXPECTED_CHAIN_ID environment variable. This ensures the visible UI (e.g., the active chain button in the nav) stays consistent with the deployment's default chain setting, while the payment logic inside useX402 safely handles the multi-chain flexibility under the hood without fighting the visual widget.

pisum-sativum and others added 3 commits July 5, 2026 04:31
This prevents holding up the HTTP handler goroutine for the duration of inference.

Co-authored-by: codex <codex@users.noreply.github.com>
Fixes issue AnkanMisra#241 where the same payment transaction hash could be replayed
indefinitely. This adds an atomic check and set in Redis to record used
transaction hashes (EIP-712 signatures) immediately after first successful
verification. A TTL of 30 days is applied to the keys in Redis to match
the on-chain finality + dispute window. Additionally, it implements an
in-memory fallback to avoid breaking tests and adds a load test simulating
concurrent replay submissions.

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

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

@pisum-sativum is attempting to deploy a commit to the ankanmisra's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Hi @pisum-sativum, thanks for opening this PR.

Every contribution helps MicroAI-Paygate grow. If you find the project useful, consider starring the repository — it helps others discover it.

Star MicroAI-Paygate on GitHub

Looking forward to reviewing this PR.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@pisum-sativum, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a74a9ed-4c09-4055-88bf-3dff46a54142

📥 Commits

Reviewing files that changed from the base of the PR and between 33e7fc3 and c872f73.

⛔ Files ignored due to path filters (1)
  • web/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • gateway/cache.go
  • gateway/cache_integration_test.go
  • gateway/errors_test.go
  • gateway/internal/ai/ollama.go
  • gateway/internal/ai/openrouter.go
  • gateway/internal/ai/provider.go
  • gateway/main.go
  • gateway/receipt_store_integration_test.go
  • gateway/redis.go
  • gateway/timeout_test.go
  • tests/e2e.test.ts
  • web/README.md
  • web/package.json
  • web/src/hooks/use-x402.test.ts
  • web/src/hooks/use-x402.ts
  • web/src/lib/errors.ts
  • web/src/lib/types.ts
  • web/src/lib/x402-client.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update go code TypeScript TypeScript code type:docs Documentation, API docs, examples, or contributor docs. type:testing Tests, coverage, fixtures, or validation-only work. labels Jul 4, 2026
@pisum-sativum

Copy link
Copy Markdown
Author

@AnkanMisra I have done the necessary changes kindly check and merge with gssoc and gssoc:approved tags.

@vercel

vercel Bot commented Jul 5, 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 Jul 6, 2026 5:04am

@AnkanMisra AnkanMisra added enhancement New feature or request gssoc:approved Approved for GSSoC contribution level:intermediate Moderate scope requiring project familiarity or cross-file changes. triage Needs maintainer triage. type:feature New user-facing or API-facing capability. labels Jul 5, 2026 — with ChatGPT Codex Connector
@AnkanMisra

Copy link
Copy Markdown
Owner

@codex review

@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: 72bc77af0a

ℹ️ 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 web/src/hooks/use-x402.ts Outdated
});
}

const signContext = { ...context, chainId: finalChain ?? context.chainId };

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 Keep EIP-712 chain aligned with gateway verification

When the challenge advertises an alternate chain and the wallet is already on it, this overwrites the EIP-712 domain chainId with finalChain. The signed retry still only sends signature/nonce/timestamp, and the gateway reconstructs the verification context from its configured CHAIN_ID in verifyPayment, so the verifier checks the signature against the default chain rather than the alternate one. Users on an advertised non-default chain will be rejected instead of skipping the switch; either keep signing context.chainId or add the selected chain to the protocol and gateway/verifier checks.

Useful? React with 👍 / 👎.

Comment thread gateway/main.go Outdated
// 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.
c.Writer.Header().Set("Content-Type", "text/event-stream")

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 Preserve the paid success response contract

This changes successful summarize responses to text/event-stream, with the receipt emitted in the stream instead of the documented X-402-Receipt header. Existing consumers in sdk/typescript/src/client.ts still parse the 200 body as JSON and protocol/microai.ts still reads the receipt header, while README/OpenAPI also advertise application/json, so paid SDK/smoke-test callers will fail or lose receipt verification. Either keep the JSON+header response for those clients or update/version the protocol and all consumers together.

Useful? React with 👍 / 👎.

Comment thread web/src/lib/x402-client.ts Outdated
Comment on lines +95 to +99
if (parsed.text) {
summary += parsed.text;
if (onChunk) onChunk(summary);
} else if (parsed.receipt) {
receipt = safeDecodeReceiptHeader(parsed.receipt);

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 Reject SSE error events instead of completing

When the gateway emits data: {"error": ...} after streaming has started (for upstream stream errors or receipt generation/storage failures), this parser ignores the error object and resolves with whatever partial summary/receipt state it has; useX402 then treats the still-200 retry as SummaryCompleted. In those failure scenarios a paid request can appear successful without a valid receipt, so the client should throw on error events and only resolve after the expected receipt/[DONE] sequence.

Useful? React with 👍 / 👎.

Comment thread gateway/cache.go
Comment on lines +230 to +232
// Instead of parsing the SSE stream from bodyBytes, we retrieve the full summary
// which handleSummarize saves in the gin context.
if fullSummaryVal, exists := c.Get("full_summary"); exists {

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 Remove the unused cached body read

This new cache-write path no longer consumes the bodyBytes assigned just above, leaving an unused local in gateway/cache.go. Go rejects unused locals at compile time, so the gateway package will not build until the assignment is removed or the buffered body is used again.

Useful? React with 👍 / 👎.

Comment thread gateway/main.go Outdated
Comment on lines +515 to +518
c.JSON(http.StatusPaymentRequired, gin.H{
"error": "Payment Required",
"message": "Transaction already used",
"paymentContext": createPaymentContext(),

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.

P2 Badge Return 409 for gateway replay hits

When this new gateway-level replay check is the component that detects reuse (for example after verifier nonce state is reset, or with any stateless verifier path), it returns a fresh 402 challenge instead of the existing 409 nonce_already_used contract used by respondVerificationFailure and the web error mapper. The client will classify the replay as an expired/new payment challenge and prompt the user incorrectly; return the same public conflict error here and in the duplicate cache-hit block.

Useful? React with 👍 / 👎.

Comment thread gateway/main.go Outdated
Comment on lines +580 to +583
responseMap := map[string]interface{}{
"result": fullSummary,
}
responseBody, _ := json.Marshal(responseMap)

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 Hash the streamed body in receipts

After switching the response to SSE, the client receives data: frames, but the receipt is still generated over a synthetic JSON body {"result": ...} that is no longer the actual response payload. Any consumer that verifies service.response_hash against the bytes it received will reject the receipt, and the receipt no longer attests to the displayed streamed response; either hash the actual streamed payload or keep the JSON response body.

Useful? React with 👍 / 👎.

Comment on lines +202 to +206
lines := strings.Split(string(body), "\n")
var result string
for _, line := range lines {
if strings.HasPrefix(line, "data: ") {
data := strings.TrimSpace(strings.TrimPrefix(line, "data: "))

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 Add the missing strings imports

This new SSE parsing helper calls strings.Split, HasPrefix, TrimSpace, and TrimPrefix, but the file's imports were not updated to include strings (the same pattern was added in receipt_store_integration_test.go). Once the gateway tests compile past the production error, these touched tests fail with undefined: strings; add the missing import in each file that uses it.

Useful? React with 👍 / 👎.

@AnkanMisra AnkanMisra left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

  1. Fix the go lint and go test
  2. Fix the bugs codex found

pisum-sativum and others added 2 commits July 5, 2026 22:41
Fixes eslint warnings/errors in the frontend web client and tests.
Removes unused variables in gateway cache and test files.
Adds missing strings import for gateway cache tests.

Co-authored-by: codex <codex@users.noreply.github.com>
- Keep EIP-712 chain aligned with gateway verification by avoiding finalChain override
- Preserve JSON response and header contract for standard clients using Accept header
- Hash actual streamed bytes in receipts for SSE mode
- Reject SSE error events in the web client instead of completing silently
- Return 409 Conflict instead of 402 for gateway-level replay cache hits

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

Copy link
Copy Markdown
Author

@AnkanMisra I have done the necessary changes kindly check and merge.

@AnkanMisra

Copy link
Copy Markdown
Owner

@codex review

@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: c872f73311

ℹ️ 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".

const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {

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 Restore JSON success handling for non-SSE responses

When postSummarize sends the signed retry it does not add Accept: text/event-stream, and gateway/main.go only emits SSE when that header is present; otherwise it returns JSON plus the X-402-Receipt header. This new parser only consumes data: lines, so normal successful payments in the web UI resolve to an empty summary and null receipt instead of saving/displaying the paid result. Either request SSE from the client or keep the previous JSON/header fallback here.

Useful? React with 👍 / 👎.

Comment thread tests/e2e.test.ts
Comment on lines +90 to +91
expect(textStr).toContain("data: {");
expect(textStr).toContain("[DONE]");

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.

P2 Badge Request SSE before asserting SSE frames

This signed E2E fetch still sends only JSON and x402 headers, but the gateway's SSE path is gated on Accept: text/event-stream; without that header a successful 200 response is the JSON body, so these new assertions fail even when the gateway behaves correctly. Add the SSE Accept header to this request or keep asserting the JSON response shape.

Useful? React with 👍 / 👎.

Comment on lines +281 to +285
} else if resp.Code == http.StatusPaymentRequired {
var body map[string]interface{}
_ = json.Unmarshal(resp.Body.Bytes(), &body)
if body["message"] == "Transaction already used" {
conflictCount++

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.

P2 Badge Count nonce replays as 409 conflicts

In the gateway path, respondVerificationFailure maps nonce_already_used to HTTP 409 and respondError emits an error field, not a 402 with message. This new replay test therefore records correct replay rejections as neither success nor conflict, leaving conflictCount at 0 and failing Go CI once replay protection works. Check http.StatusConflict and body["error"] == "nonce_already_used" instead.

Useful? React with 👍 / 👎.

Comment thread web/src/hooks/use-x402.ts
});
}

const signContext = { ...context };

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 Bind signatures to the supported chain actually selected

When currentChain is a supported alternative (for example the wallet is on 8453 while the challenge default chainId is 84532), finalChain is set but then discarded by creating signContext from context unchanged. The EIP-712 domain is still bound to the default chain, so the advertised alternative is never actually used and wallets that enforce domain/current-chain parity can still fail. Build the signed payment context from the selected supported chain, with matching gateway/verifier handling, before signing.

Useful? React with 👍 / 👎.

Comment thread gateway/main.go
Comment on lines +589 to +590
fmt.Fprint(c.Writer, chunk)
flusher.Flush()

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.

P2 Badge Allow SSE chunks to flush through the timeout wrapper

On the real /api/ai/summarize route, registerAPIRoutes wraps this handler in RequestTimeoutMiddleware, whose responseWriterShim buffers body writes and whose Flush() only flushes the underlying writer without copying the buffered chunk. In the Accept: text/event-stream path, these per-token flushes therefore do not reach the client until the handler completes, so the new streaming UI cannot receive incremental chunks. Either bypass buffering for SSE or make the timeout writer flush buffered data safely.

Useful? React with 👍 / 👎.

Comment thread gateway/redis.go
if memoryUsedTx[txHash] {
return true, nil
}
memoryUsedTx[txHash] = true

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.

P2 Badge Expire in-memory replay entries

When Redis is not required (for example RECEIPT_STORE=memory with caching disabled), every accepted signature is stored in memoryUsedTx and never removed, unlike the Redis path's 30-day TTL. A long-running gateway in memory mode will grow this map without bound and test runs can leak replay state between cases that reuse signatures. Store expiry timestamps with cleanup, or require Redis for replay tracking.

Useful? React with 👍 / 👎.

Comment thread gateway/main.go
}
if err != nil {
if useSSE {
fmt.Fprintf(c.Writer, "data: {\"error\": %q}\n\n", err.Error())

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 Sanitize streaming upstream errors

When an SSE client is connected and the upstream stream fails after it has been opened, this writes err.Error() directly into the response body instead of using the sanitized public error codes used by respondError. In that streaming scenario provider response details, URLs, or other internal diagnostics can be exposed to browsers; send a stable public code such as upstream_unavailable/upstream_timeout and keep the detailed error in server logs.

Useful? React with 👍 / 👎.

Comment on lines +112 to +114
line, err := s.reader.ReadBytes('\n')
if err != nil {
return "", err

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.

P2 Badge Treat unexpected stream EOF as a failure

For OpenRouter-compatible streaming, only the [DONE] event marks a complete response, but this returns io.EOF for any socket EOF before checking whether [DONE] was received. If the upstream or a proxy drops the stream early, handleSummarize treats it as a successful completion, generates a receipt, and can cache a partial or empty summary. Track clean completion separately and return an upstream error for EOF before [DONE].

Useful? React with 👍 / 👎.

@AnkanMisra AnkanMisra left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Fix the ci and the bugs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request go Pull requests that update go code gssoc:approved Approved for GSSoC contribution level:intermediate Moderate scope requiring project familiarity or cross-file changes. triage Needs maintainer triage. type:docs Documentation, API docs, examples, or contributor docs. type:feature New user-facing or API-facing capability. type:testing Tests, coverage, fixtures, or validation-only work. TypeScript TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support advertised chain options in the web wallet flow

2 participants