Skip to content

Conversation

@ezynda3
Copy link
Contributor

@ezynda3 ezynda3 commented Jan 13, 2026

Summary

  • Implement x402 v2 protocol core types, chains, and validation logic
  • Add EVM signer with EIP-3009 authorization support
  • Add SVM (Solana) signer with transaction building
  • Add HTTP transport layer with middleware, client, and facilitator
  • Add Gin framework middleware adapter
  • Add MCP integration with server and client transport
  • Add comprehensive test suite and example applications

Features

  • Multi-chain support: EVM (Base, Ethereum, Arbitrum, Optimism, Polygon) and SVM (Solana)
  • Payment requirements: Flexible payment options with network and asset selection
  • Signers: EVM (EIP-3009) and SVM transaction signing
  • HTTP middleware: Standard library and Gin framework support
  • MCP integration: Payment-gated MCP tools and resources
  • Examples: Complete examples for HTTP, Gin, and MCP integrations

Summary by CodeRabbit

  • New Features

    • x402 v2 payment protocol added: HTTP and MCP integrations, middleware, client/server examples, and end-to-end payment flows
    • Support for EVM and Solana signers, token configurations, payment selection, and event callbacks
    • Encoding/decoding, validation, and timeout/configuration utilities for v2 payments
  • Bug Fixes / API

    • Several public APIs and constructors stabilized (handler/server call sites now return errors)
  • Documentation

    • Removed legacy "Issue Tracking with bd (beads)" section from docs

✏️ Tip: You can customize this high-level summary in your review settings.

Implement Phase 4 of v2 protocol - EVM signer with EIP-3009 authorization:

- v2/internal/eip3009: EIP-712 typed data and signature creation utilities
- v2/signers/evm: Signer implementation for Ethereum-compatible chains
  - NewSigner/NewSignerFromKey constructors
  - CanSign/Sign methods for payment payload generation
  - Support for max amount limits via WithMaxAmount option
  - CAIP-2 network identifier support

Tests use Foundry/Anvil default test key for consistency and clarity.
Implement the v2 SVM (Solana) signer for the x402 protocol v2.

New files:
- v2/internal/solana/solana.go - Utility functions for Solana
  - BuildTransferCheckedInstruction: SPL Token TransferChecked builder
  - BuildSetComputeUnitLimitInstruction: Compute budget instruction
  - BuildSetComputeUnitPriceInstruction: Priority fee instruction
  - DeriveAssociatedTokenAddress: ATA derivation
  - GetRPCURL: CAIP-2 network to RPC URL mapping

- v2/signers/svm/signer.go - SVM Signer implementation
  - NewSigner: Create from base58 private key
  - NewSignerFromKey: Create from existing key
  - NewSignerFromKeygenFile: Create from Solana keygen JSON file
  - Full v2.Signer interface implementation
  - CAIP-2 network validation (rejects EVM networks)
  - Fee payer extraction from requirements.Extra

Key differences from v1:
- Uses CAIP-2 network identifiers (e.g., solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp)
- Returns v2.SVMPayload type directly
- Uses v2.PaymentRequirements with Amount field
- Sets X402Version to 2

This completes Phase 5 of the v2 implementation plan.
…port

Implements Phase 6 of the v2 protocol implementation plan.

New files:
- v2/http/internal/helpers/helpers.go - HTTP helper functions
  - ParsePaymentHeader, SendPaymentRequired, AddPaymentResponseHeader
  - ParsePaymentRequirements, ParseSettlement, BuildPaymentHeader
  - BuildResourceURL for constructing resource URLs from requests

- v2/http/middleware.go - HTTP payment middleware
  - Config struct with facilitator options, authorization, and hooks
  - NewX402Middleware factory with automatic requirement enrichment
  - settlementInterceptor for deferred settlement on success
  - Support for verify-only mode and fallback facilitators
  - GetPaymentFromContext for handler access to payment info

- v2/http/transport.go - HTTP transport with 402 handling
  - X402Transport implementing http.RoundTripper
  - Automatic 402 detection and payment retry
  - Payment callbacks for monitoring (attempt, success, failure)

- v2/http/client.go - HTTP client wrapper
  - NewClient with functional options pattern
  - WithSigner, WithSelector, WithHTTPClient options
  - WithPaymentCallback, WithPaymentCallbacks options
  - GetSettlement helper for extracting settlement from response

All tests pass with race detection enabled.
Implements Phase 7 of the v2 protocol plan.

- Add v2/http/gin/middleware.go with Gin-compatible middleware
- Support CAIP-2 network identifiers (e.g., eip155:84532)
- Add PaymentContextKey for storing verified payment in Gin context
- Add GetPaymentFromContext helper function
- Support verify-only mode
- Support primary and fallback facilitators
- Add comprehensive test coverage

The middleware follows the same pattern as v1 Gin middleware but uses
v2 types with x402Version: 2 and restructured PaymentRequired/PaymentPayload.
Implements Phase 8 of the v2 protocol plan - MCP (Model Context Protocol)
integration for payment-gated AI tools.

Server-side components:
- v2/mcp/types.go: MCP-specific payment requirements type (v2 format)
- v2/mcp/errors.go: MCP-specific error types and payment error wrapper
- v2/mcp/server/config.go: Server configuration with tool payment mapping
- v2/mcp/server/server.go: X402Server wrapping MCP server with payment support
- v2/mcp/server/handler.go: HTTP handler for payment verification/settlement
- v2/mcp/server/facilitator.go: HTTP facilitator client for MCP server
- v2/mcp/server/requirements.go: Requirement validation for MCP tools

Client-side components:
- v2/mcp/client/config.go: Client configuration with signers and callbacks
- v2/mcp/client/transport.go: Transport wrapper for automatic 402 handling

Key v2 protocol features:
- CAIP-2 network identifiers (e.g., eip155:84532)
- Resource info object in payment requirements
- Extensions passthrough support
- Payment in _meta['x402/payment'] for tool calls
- Settlement response in _meta['x402/payment-response'] in results

Tests cover:
- Free and paid tool handling
- Payment verification and settlement flow
- VerifyOnly mode
- Tool execution error handling (no settlement)
- Payment requirements extraction (v2 format)
- Configuration options
Add Phase 9 examples demonstrating v2 protocol usage:
- examples/v2/http/main.go: HTTP server/client with payment middleware
- examples/v2/gin/main.go: Gin framework integration example
- examples/v2/mcp/main.go: MCP server with paid tools and client

All examples demonstrate:
- CAIP-2 network identifiers (e.g., eip155:84532)
- v2 PaymentRequirements configuration
- EVM and SVM signer creation
- Automatic payment handling
- Payment event callbacks

This completes Phase 9 of the v2 implementation plan.
Add unit tests for the internal eip3009 package covering:
- GenerateNonce: randomness, uniqueness, non-zero values
- CreateAuthorization: field population, time bounds, nonce uniqueness
- SignAuthorization: signature format, determinism, chain separation

These tests ensure the cryptographic foundations of EVM payment
signing work correctly across different chains, tokens, and amounts.
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

This PR adds a complete x402 v2 implementation: protocol types, chain configs, signers (EVM/Solana), selector, encoding/validation, HTTP and MCP transports/middleware, facilitator client, Gin/PocketBase integrations, MCP server/client glue, examples, and extensive tests.

Changes

Cohort / File(s) Summary
Chain & Network Config
v2/chains.go, v2/chains_test.go
CAIP-2 constants, ChainConfig, GetChainConfig/ValidateNetwork/GetChainID/GetSolanaGenesisHash, NewUSDCTokenConfig and tests.
Core Types & Conversions
v2/types.go, v2/types_test.go
v2 protocol types (ResourceInfo, PaymentRequirements, PaymentPayload, EVMPayload, SVMPayload, VerifyResponse, SettleResponse, SupportedResponse, TokenConfig) and AmountToBigInt/BigIntToAmount with tests.
Errors & Events
v2/errors.go, v2/events.go
PaymentError/ErrorCode sentinels and PaymentEvent + PaymentCallback types.
Timeouts & Config
v2/config.go
TimeoutConfig with defaults, fluent setters, and validation.
Encoding Utilities
v2/encoding/encoding.go, v2/encoding/encoding_test.go
Base64-JSON encode/decode helpers for payments/settlement/requirements/verify responses and tests.
Validation Framework
v2/validation/validation.go, v2/validation/validation_test.go
Validators for amounts, networks, addresses, resources, requirements, payloads, and related tests.
Signer API & Selector
v2/signer.go, v2/selector.go, v2/selector_test.go
Signer interface, DefaultPaymentSelector, FindMatchingRequirement and selector tests.
EIP-3009 Utils & EVM Signer
v2/internal/eip3009/eip3009.go, v2/internal/eip3009/eip3009_test.go, v2/signers/evm/signer.go, v2/signers/evm/signer_test.go
EIP-3009 nonce/authorization/signing and EVM signer implementation + tests.
Solana Utilities & SVM Signer
v2/internal/solana/solana.go, v2/signers/svm/signer.go, v2/signers/svm/signer_test.go
Solana instruction builders, ATA derivation, RPC URL mapping, and Solana signer (keyfile, transaction building) with extensive tests.
HTTP Client, Transport & Helpers
v2/http/client.go, v2/http/client_test.go, v2/http/transport.go, v2/http/transport_test.go, v2/http/internal/helpers/*
X402Transport RoundTripper, Client wrapper with signer/selector/callback options, helpers for headers/requirements/settlement, and tests.
Facilitator HTTP Client
v2/http/facilitator.go, v2/http/facilitator_test.go
FacilitatorClient with Verify/Settle/Supported/EnrichRequirements, auth provider, hooks, retry/timeout and tests.
Middleware (stdlib, Gin, PocketBase)
v2/http/middleware.go, v2/http/gin/middleware.go, http/*/middleware.go, v2/http/*_test.go
HTTP and Gin middleware for 402 flows, context storage, settlement interceptor, request enrichment with timeouts, and tests.
MCP Client & Transport
v2/mcp/client/config.go, v2/mcp/client/transport.go, v2/mcp/client/transport_test.go
MCP transport wrapper that intercepts 402, extracts requirements, injects _meta.x402.payment, retries, and payment event callbacks.
MCP Server: Types, Config, Handler, Facilitator, Server
v2/mcp/types.go, v2/mcp/server/config.go, v2/mcp/server/facilitator.go, v2/mcp/server/handler.go, v2/mcp/server/server.go, v2/mcp/server/*_test.go, v2/mcp/server/requirements.go
MCP 402 payload type, server config for payable tools, HTTPFacilitator adapter, X402Handler that verifies/settles and injects settlement into JSON-RPC _meta, X402Server wrapper, validation helpers, error types, and tests.
MCP Error Handling
v2/mcp/errors.go
MCP-specific error types, PaymentError wrapper, IsPaymentError and sentinel errors.
Internal Transport & Middleware Edits
mcp/server/handler.go, mcp/server/server.go, http/*
Handler/factory signatures now propagate errors (NewX402Handler returns error), middleware enrichment uses context timeouts and EnrichRequirements(ctx,...); facilitator HTTP clients set request timeouts.
Examples & Scripts
examples/v2/*/main.go, ralph.nu, AGENTS.md
Three runnable examples (gin/http/mcp) demonstrating servers/clients and CLI flags; new script; removed Issue Tracking section from AGENTS.md (99 lines removed).

Sequence Diagram(s)

sequenceDiagram
    participant Client as HTTP Client
    participant Server as HTTP Server
    participant Facilitator as Facilitator
    participant Signer as Signer

    Client->>Server: GET /data
    Server-->>Client: 402 Payment Required (PaymentRequired)
    Client->>Signer: SelectAndSign(requirements)
    Signer-->>Client: PaymentPayload
    Client->>Server: GET /data + X-PAYMENT header
    Server->>Facilitator: Verify(PaymentPayload, requirements)
    Facilitator-->>Server: VerifyResponse
    alt Verify Success
        Server->>Facilitator: Settle(PaymentPayload, requirements)
        Facilitator-->>Server: SettleResponse
        Server-->>Client: 200 OK + X-PAYMENT-RESPONSE (settlement)
    else Verify Failed
        Server-->>Client: 402 Payment Required
    end
Loading
sequenceDiagram
    participant MCPClient as MCP Client
    participant MCPServer as MCP Server
    participant Facilitator as Facilitator
    participant Signer as Signer

    MCPClient->>MCPServer: JSON-RPC call (no _meta.x402)
    MCPServer-->>MCPClient: JSON-RPC error 402 (PaymentRequirements)
    MCPClient->>Signer: SelectAndSign(requirements)
    Signer-->>MCPClient: PaymentPayload
    MCPClient->>MCPServer: JSON-RPC call (_meta.x402.payment=PaymentPayload)
    MCPServer->>Facilitator: Verify(PaymentPayload, requirements)
    Facilitator-->>MCPServer: VerifyResponse
    alt Verify Success
        MCPServer->>Facilitator: Settle(PaymentPayload, requirements)
        Facilitator-->>MCPServer: SettleResponse
        MCPServer-->>MCPClient: JSON-RPC result (_meta.x402.payment-response=settlement)
    else Verify Failed
        MCPServer-->>MCPClient: JSON-RPC error 402
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hop with a ledger and tiny delight,
I sign tiny payments by day and by night,
From EVM to Solana I scurry and cheer,
402s vanish when signers appear —
A carrot for code, a hop and a cheer! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(v2): implement x402 v2 protocol with multi-chain support' clearly summarizes the main change—implementing the x402 v2 protocol with multi-chain capabilities—which aligns with the substantial additions across core types, EVM/SVM signers, HTTP transport, and MCP integration shown in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

- Fix errcheck: add explicit error handling for json.Encode and w.Write calls
- Fix staticcheck SA5011: use t.Fatal instead of t.Error for nil checks
- Fix staticcheck SA4006: properly use nonce variable in eip3009_test.go
- Remove unused mockSigner2 type and math/big import from client_test.go
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In @v2/http/facilitator.go:
- Around line 298-336: EnrichRequirements currently uses context.Background(),
preventing callers from controlling cancellation/timeouts; change the method
signature of FacilitatorClient.EnrichRequirements to accept a context.Context as
the first parameter (e.g., EnrichRequirements(ctx context.Context, requirements
[]v2.PaymentRequirements) ([]v2.PaymentRequirements, error)), replace the
internal call to c.Supported(context.Background()) with c.Supported(ctx), and
propagate this new ctx to all callers and unit tests (update any call sites and
mocks) so cancellation and deadlines flow through; keep the rest of the merging
logic unchanged.

In @v2/http/gin/middleware.go:
- Around line 60-88: The http.Client instances inside NewX402Middleware are
created without timeouts, so set each Client's Timeout to the request timeout
from the provided config (use config.Timeouts.RequestTimeout or equivalent) when
constructing the v2http.FacilitatorClient for both facilitator and
fallbackFacilitator; update the Client field assignment in the
v2http.FacilitatorClient initializers (in NewX402Middleware) to use
&http.Client{Timeout: config.Timeouts.RequestTimeout} so requests cannot hang
indefinitely.

In @v2/http/middleware.go:
- Around line 287-293: The Hijack implementation on settlementInterceptor
currently delegates to i.w without ensuring the response is settled, which can
allow a client-visible success before settlement; update
settlementInterceptor.Hijack to first ensure settlement (e.g., call the
interceptor's settle/commit method or a new settleIfNeeded helper that checks
the committed state) and return any settlement error, then only if settlement
succeeds proceed to assert i.w as http.Hijacker and call Hijack; if the writer
does not implement http.Hijacker, return an appropriate error after attempting
settlement.

In @v2/mcp/client/config.go:
- Around line 40-45: The WithSigner option currently appends a v2.Signer without
checking for nil, which can introduce nil-interface entries in Config.Signers;
update the WithSigner function to check if signer == nil and simply return
without modifying c.Signers when nil (i.e., only append when signer is non-nil)
so callers who accidentally pass nil won’t create a nil entry that breaks
signing later.

In @v2/mcp/client/transport.go:
- Around line 58-74: When handling a 402 response, don't marshal a nil
resp.Error.Data into json.RawMessage (which leads to an "unexpected end of JSON
input" unmarshal error); instead check if resp.Error.Data == nil and immediately
return ErrNoPaymentRequirements (or call t.extractPaymentRequirements with a
nil/empty indicator that yields ErrNoPaymentRequirements). Update the 402
handling blocks that build `data` (the one around the resp.Error.Data check and
the similar block at lines 114-120) to return the clearer
ErrNoPaymentRequirements when resp.Error.Data is nil, referencing the existing
function t.extractPaymentRequirements and the ErrNoPaymentRequirements sentinel.

In @v2/mcp/server/config.go:
- Around line 97-107: GetPaymentConfig currently takes toolName and returns
&config where config is the map value copy, so callers get a pointer to a local
copy (misleading). Fix by choosing one approach: (A) change the signature to
GetPaymentConfig(toolName string) (ToolPaymentConfig, bool) and return the value
and existence flag (preferred for read-only); or (B) change c.PaymentTools to
map[string]*ToolPaymentConfig and return the stored pointer from
GetPaymentConfig(toolName string) *ToolPaymentConfig (update map initialization
and all callers to expect pointer semantics). Update all call sites to match the
new return type/semantics.

In @v2/mcp/server/handler.go:
- Around line 252-277: After unmarshalling into v2.PaymentPayload in
extractPayment, validate the payment.X402Version field against the supported
version(s) before returning (or before any verify/settle logic uses it); if the
version is missing or unsupported, return nil (or an error) so downstream
verify/settle routines do not operate on an incompatible payload. Update
extractPayment (and any callers that immediately verify/settle) to reference
v2.PaymentPayload and payment.X402Version and perform this check against your
canonical supported constant (e.g., X402SupportedVersion) before proceeding.
- Around line 72-79: The initializeFacilitators function currently panics on
missing config; change it to return an error instead by updating its signature
to (Facilitator, Facilitator, error), replace the panic("x402: ...") with a
formatted error (e.g., fmt.Errorf("x402: at least one facilitator URL must be
provided")), and return nil, nil, err; update any callers (notably the
X402Handler constructor) to accept the error and change the constructor
signature to return (*X402Handler, error), propagating the error up so library
users can handle configuration failures instead of crashing.
- Around line 389-411: Change the verify-only branch (where settleResp == nil)
to reflect verification success instead of marking settlement-skipped as
failure: when verifyResp indicates verification succeeded, set
meta["x402/payment-response"] to a v2.SettleResponse with Success: true,
Network: payment.Accepted.Network, Payer from verifyResp (and omit or leave
Transaction empty); only set Success: false and include an errorReason when
verifyResp indicates an actual verification failure. Update the code around
jsonrpcResp.Result handling (the block using settleResp, verifyResp,
v2.SettleResponse, and payment.Accepted.Network) to implement this logic.

In @v2/signers/svm/signer.go:
- Around line 199-214: The RPC call to fetch a recent blockhash can hang because
it uses context.Background(); wrap the call in a context with timeout (e.g.,
context.WithTimeout) and pass that ctx to client.GetLatestBlockhash, then defer
cancel() to ensure cancellation; keep using solutil.GetRPCURL and rpc.New for
client creation but replace the plain Background context with the new timed
context around the GetLatestBlockhash call (reference the GetLatestBlockhash
method and the surrounding client variable and ctx usage).
- Around line 283-314: Insert an idempotent ATA-creation instruction for destATA
before the TransferChecked instruction in buildPartiallySignedTransfer: call the
appropriate helper (e.g., a CreateAssociatedTokenAccountIdempotent or
BuildCreateAssociatedTokenAccountInstruction) that sets feePayer as the payer
and recipient as the owner for mint, add that instruction to the instructions
slice immediately before the BuildTransferCheckedInstruction so the transaction
will create the recipient ATA (if needed) in the same transaction and let the
facilitator sponsor the rent-exempt balance.
🟡 Minor comments (10)
v2/http/facilitator_test.go-206-209 (1)

206-209: Unchecked error return from json.Encode.

Static analysis flagged that the error return from json.NewEncoder(w).Encode(response) is not checked. While unlikely to fail in tests, it's good practice to check.

🔧 Suggested fix
 		response := v2.VerifyResponse{IsValid: true}
 		w.Header().Set("Content-Type", "application/json")
-		json.NewEncoder(w).Encode(response)
+		if err := json.NewEncoder(w).Encode(response); err != nil {
+			t.Errorf("Failed to encode response: %v", err)
+		}
 	}))

This pattern appears in multiple mock handlers throughout the test file. Consider applying consistent error handling to all instances.

v2/http/transport.go-36-41 (1)

36-41: Potential race condition when setting t.Base.

Assigning t.Base = http.DefaultTransport mutates the transport struct. If RoundTrip is called concurrently on the same X402Transport instance, this could cause a data race. Consider initializing Base in a constructor or using a local variable.

🔒️ Suggested fix
 func (t *X402Transport) RoundTrip(req *http.Request) (*http.Response, error) {
 	// Ensure we have a base transport
-	if t.Base == nil {
-		t.Base = http.DefaultTransport
-	}
+	base := t.Base
+	if base == nil {
+		base = http.DefaultTransport
+	}

 	// Clone the request to avoid modifying the original
 	reqCopy := req.Clone(req.Context())

 	// Make the first attempt
-	resp, err := t.Base.RoundTrip(reqCopy)
+	resp, err := base.RoundTrip(reqCopy)

And update line 119 similarly:

-	respRetry, err := t.Base.RoundTrip(reqRetry)
+	respRetry, err := base.RoundTrip(reqRetry)
v2/mcp/client/config.go-37-38 (1)

37-38: Fix misleading docstring: Option configures Config, not Transport.
Line 37 says “configuring the Transport” but Option is func(*Config).

v2/mcp/client/transport.go-3-13 (1)

3-13: Import grouping doesn’t match repo guidelines (stdlib / external / internal).
Please separate github.com/mark3labs/mcp-go/... (external) from github.com/mark3labs/x402-go/... (internal) with a blank line.

v2/internal/solana/solana.go-43-56 (1)

43-56: Prefer encoding/binary for little-endian packing (safer + clearer).

Proposed refactor
 import (
+	"encoding/binary"
 	"fmt"

 	"github.com/gagliardetto/solana-go"
 	"github.com/gagliardetto/solana-go/programs/token"
 	"github.com/gagliardetto/solana-go/rpc"

 	v2 "github.com/mark3labs/x402-go/v2"
 )
@@
 func BuildSetComputeUnitLimitInstruction(units uint32) solana.Instruction {
 	data := make([]byte, 5)
 	data[0] = 2 // SetComputeUnitLimit discriminator
-	data[1] = byte(units)
-	data[2] = byte(units >> 8)
-	data[3] = byte(units >> 16)
-	data[4] = byte(units >> 24)
+	binary.LittleEndian.PutUint32(data[1:], units)
@@
 func BuildSetComputeUnitPriceInstruction(microlamports uint64) solana.Instruction {
 	data := make([]byte, 9)
 	data[0] = 3 // SetComputeUnitPrice discriminator
-	data[1] = byte(microlamports)
-	data[2] = byte(microlamports >> 8)
-	data[3] = byte(microlamports >> 16)
-	data[4] = byte(microlamports >> 24)
-	data[5] = byte(microlamports >> 32)
-	data[6] = byte(microlamports >> 40)
-	data[7] = byte(microlamports >> 48)
-	data[8] = byte(microlamports >> 56)
+	binary.LittleEndian.PutUint64(data[1:], microlamports)

Also applies to: 61-78

v2/http/facilitator.go-361-374 (1)

361-374: extractPayer should handle the concrete v2.EVMPayload type (not only decoded maps).

Proposed fix
 func extractPayer(payload v2.PaymentPayload) string {
-	// Try to extract from EVM payload
-	if evmPayload, ok := payload.Payload.(map[string]interface{}); ok {
-		if auth, ok := evmPayload["authorization"].(map[string]interface{}); ok {
-			if from, ok := auth["from"].(string); ok {
-				return from
-			}
-		}
-	}
+	switch p := payload.Payload.(type) {
+	case v2.EVMPayload:
+		return p.Authorization.From
+	case map[string]interface{}:
+		if auth, ok := p["authorization"].(map[string]interface{}); ok {
+			if from, ok := auth["from"].(string); ok {
+				return from
+			}
+		}
+	}
 	// For SVM payloads, the payer would need to be extracted from the transaction
 	// which requires decoding. The facilitator should provide this in the response.
 	return ""
 }
v2/mcp/server/facilitator.go-105-123 (1)

105-123: Nil-check payment before dereferencing in Verify/Settle.

Proposed fix
 func (f *HTTPFacilitator) Verify(ctx context.Context, payment *v2.PaymentPayload, requirement v2.PaymentRequirements) (*v2.VerifyResponse, error) {
+	if payment == nil {
+		return nil, fmt.Errorf("facilitator verify failed: %w", v2.ErrMalformedHeader)
+	}
 	resp, err := f.client.Verify(ctx, *payment, requirement)
v2/http/internal/helpers/helpers.go-42-45 (1)

42-45: Unchecked error return from JSON encoding.

The return value of json.NewEncoder(w).Encode(response) is not checked. While encoding to a ResponseWriter rarely fails, ignoring the error could mask issues in edge cases.

🔧 Proposed fix
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusPaymentRequired)
-	json.NewEncoder(w).Encode(response)
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		// Log the error; response headers are already sent so we can't change status
+		return
+	}
 }

Alternatively, if logging is preferred:

if err := json.NewEncoder(w).Encode(response); err != nil {
    // Consider logging: the client may have disconnected
    _ = err // suppress lint if logging not available
}
v2/http/client_test.go-286-319 (1)

286-319: Remove unused mockSigner2 type.

This type is defined but never used. Since both test files are in the same http package, mockSigner from transport_test.go is accessible here. Remove this duplicate to fix the linter warning.

🔧 Proposed fix
-// mockSigner2 is a duplicate for this file since we can't import from transport_test.go
-type mockSigner2 struct {
-	network   string
-	scheme    string
-	tokens    []v2.TokenConfig
-	maxAmount *big.Int
-	priority  int
-	signFunc  func(*v2.PaymentRequirements) (*v2.PaymentPayload, error)
-}
-
-func (m *mockSigner2) Network() string             { return m.network }
-func (m *mockSigner2) Scheme() string              { return m.scheme }
-func (m *mockSigner2) GetPriority() int            { return m.priority }
-func (m *mockSigner2) GetTokens() []v2.TokenConfig { return m.tokens }
-func (m *mockSigner2) GetMaxAmount() *big.Int      { return m.maxAmount }
-func (m *mockSigner2) CanSign(req *v2.PaymentRequirements) bool {
-	return req.Network == m.network && req.Scheme == m.scheme
-}
-func (m *mockSigner2) Sign(req *v2.PaymentRequirements) (*v2.PaymentPayload, error) {
-	if m.signFunc != nil {
-		return m.signFunc(req)
-	}
-	return &v2.PaymentPayload{
-		X402Version: 2,
-		Accepted: v2.PaymentRequirements{
-			Scheme:  req.Scheme,
-			Network: req.Network,
-			Amount:  req.Amount,
-		},
-		Payload: map[string]interface{}{
-			"signature": "0xmocksig",
-		},
-	}, nil
-}

Also remove the unused "math/big" import if no longer needed after this removal.

v2/http/client_test.go-181-214 (1)

181-214: Fix unchecked error returns in test handler (pipeline failure).

The pipeline is failing due to unchecked json.NewEncoder(w).Encode() at line 199. While in test code, fixing this ensures the linter passes and prevents silent test failures.

🔧 Proposed fix
 			w.Header().Set("Content-Type", "application/json")
 			w.WriteHeader(http.StatusPaymentRequired)
-			json.NewEncoder(w).Encode(paymentReq)
+			if err := json.NewEncoder(w).Encode(paymentReq); err != nil {
+				t.Errorf("Failed to encode payment request: %v", err)
+			}
 			return
 		}

 		settlement := v2.SettleResponse{
 			Success:     true,
 			Transaction: "0x1234567890abcdef",
 			Network:     "eip155:84532",
 			Payer:       "0xPayerAddress",
 		}
 		encoded, _ := encoding.EncodeSettlement(settlement)
 		w.Header().Set("X-PAYMENT-RESPONSE", encoded)
 		w.WriteHeader(http.StatusOK)
-		w.Write([]byte("Protected content"))
+		_, _ = w.Write([]byte("Protected content"))
 	}))
🧹 Nitpick comments (30)
examples/v2/mcp/main.go (1)

198-206: Consider validating type assertions for handler arguments.

The type assertion message, _ := args["message"].(string) silently ignores if the value is not a string. While this is example code, it could lead to empty strings being echoed without any indication of a problem.

💡 Suggested improvement
 func echoHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 	args := req.GetArguments()
-	message, _ := args["message"].(string)
+	message, ok := args["message"].(string)
+	if !ok || message == "" {
+		return &mcp.CallToolResult{
+			Content: []mcp.Content{
+				mcp.NewTextContent("Error: message argument is required"),
+			},
+			IsError: true,
+		}, nil
+	}
 	return &mcp.CallToolResult{
 		Content: []mcp.Content{
 			mcp.NewTextContent(fmt.Sprintf("Echo (x402 v2): %s", message)),
 		},
 	}, nil
 }
v2/mcp/client/transport_test.go (1)

122-126: Unused transport variable.

The Transport instance on line 123 is created but never used. The test manually implements the injection logic instead of calling the transport's method. Consider either removing the unused variable or testing the actual transport method if available.

💡 Suggested fix
 func TestInjectPaymentMeta(t *testing.T) {
-	_ = &Transport{
-		config: DefaultConfig("http://example.com"),
-	}
-
 	payment := &v2.PaymentPayload{
v2/internal/eip3009/eip3009_test.go (1)

416-430: Consider removing or enhancing the struct field test.

This test only verifies compile-time correctness (field types), which the compiler already guarantees. If kept, consider adding runtime assertions for expected zero-value behavior or remove this test as it provides no runtime validation value.

v2/config.go (1)

45-58: Consider validating RequestTimeout.

The Validate() method checks VerifyTimeout and SettleTimeout but doesn't validate RequestTimeout. Consider adding:

  1. RequestTimeout > 0 check for consistency
  2. Optionally, RequestTimeout >= SettleTimeout to ensure the overall request timeout accommodates settlement operations.
♻️ Proposed enhancement
 func (tc TimeoutConfig) Validate() error {
 	if tc.VerifyTimeout <= 0 {
 		return fmt.Errorf("verify timeout must be positive, got %v", tc.VerifyTimeout)
 	}
 	if tc.SettleTimeout <= 0 {
 		return fmt.Errorf("settle timeout must be positive, got %v", tc.SettleTimeout)
 	}
+	if tc.RequestTimeout <= 0 {
+		return fmt.Errorf("request timeout must be positive, got %v", tc.RequestTimeout)
+	}
 	if tc.SettleTimeout < tc.VerifyTimeout {
 		return fmt.Errorf("settle timeout (%v) should be >= verify timeout (%v)",
 			tc.SettleTimeout, tc.VerifyTimeout)
 	}
+	if tc.RequestTimeout < tc.SettleTimeout {
+		return fmt.Errorf("request timeout (%v) should be >= settle timeout (%v)",
+			tc.RequestTimeout, tc.SettleTimeout)
+	}
 	return nil
 }
v2/signers/evm/signer_test.go (1)

160-188: Good error path test for amount limits.

Validates that ErrAmountExceeded is returned when the amount exceeds the configured max. Consider using errors.Is() for error comparison for more robust error checking.

♻️ Proposed improvement
+import "errors"
+
 func TestSignAmountExceeded(t *testing.T) {
 	// ... existing setup ...
 
 	_, err = signer.Sign(requirements)
-	if err != v2.ErrAmountExceeded {
+	if !errors.Is(err, v2.ErrAmountExceeded) {
 		t.Errorf("Expected ErrAmountExceeded, got %v", err)
 	}
 }
v2/selector_test.go (1)

204-241: Good max amount limit test.

Tests both the rejection path (amount exceeds limit) and success path (amount within limit). Consider adding an explicit assertion on the error code for the exceeded case for consistency with other tests.

♻️ Proposed enhancement for consistency
 	_, err := selector.SelectAndSign([]Signer{signer}, requirements)
 	if err == nil {
 		t.Error("SelectAndSign() should fail when amount exceeds limit")
 	}
+	var paymentErr *PaymentError
+	if errors.As(err, &paymentErr) {
+		if paymentErr.Code != ErrCodeNoValidSigner {
+			t.Errorf("error code = %s, want %s", paymentErr.Code, ErrCodeNoValidSigner)
+		}
+	}
v2/http/gin/middleware.go (1)

90-98: Enrichment error handling could be improved.

When enrichment fails, the middleware logs a warning and continues with original requirements. This is reasonable, but consider whether this should be configurable behavior—some deployments may want to fail fast if the facilitator is unreachable at startup.

v2/chains_test.go (1)

362-374: Consider using strings.Contains from stdlib.

The custom containsString and containsSubstring helpers duplicate functionality provided by strings.Contains. Using the stdlib function would be simpler and more idiomatic.

♻️ Suggested simplification
+import (
+	"errors"
+	"strings"
+	"testing"
+)

-// Helper function
-func containsString(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
-}
-
-func containsSubstring(s, substr string) bool {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
-}
+// Helper function
+func containsString(s, substr string) bool {
+	return strings.Contains(s, substr)
+}
v2/http/middleware_test.go (1)

143-157: Check error return value of w.Write.

Static analysis indicates the error return value of w.Write is not checked. While this is in test code and unlikely to fail, addressing it would satisfy the linter.

♻️ Suggested fix
 		w.WriteHeader(http.StatusOK)
-		w.Write([]byte("OK"))
+		_, _ = w.Write([]byte("OK"))
 	}))
v2/signers/svm/signer_test.go (2)

96-111: Consider using errors.Is for sentinel error checking.

The test uses errTarget but doesn't actually verify the error wrapping with errors.Is. Currently errTarget is set but not used in error comparison.

♻️ Suggested improvement
 		t.Run(tt.name, func(t *testing.T) {
 			signer, err := NewSigner(tt.network, tt.key, tt.tokens, tt.opts...)
 			if tt.wantErr {
 				if err == nil {
 					t.Fatal("expected error, got nil")
 				}
+				if tt.errTarget != nil && !errors.Is(err, tt.errTarget) {
+					t.Errorf("expected error wrapping %v, got %v", tt.errTarget, err)
+				}
 				return
 			}

Don't forget to add "errors" to imports.


708-720: Consider using strings.Contains from stdlib.

Similar to the chains_test.go file, these helper functions duplicate stdlib functionality.

♻️ Suggested simplification
+import (
+	"strings"
+	// ... other imports
+)

 // Helper function to check if error message contains expected string
 func containsString(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(substr) == 0 || indexOfSubstring(s, substr) >= 0)
+	return strings.Contains(s, substr)
 }
-
-func indexOfSubstring(s, substr string) int {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return i
-		}
-	}
-	return -1
-}
v2/validation/validation.go (1)

94-107: url.Parse rarely returns errors - consider additional validation.

url.Parse is very permissive and will parse almost any string without error. Consider additional validation such as checking for a valid scheme (http/https/mcp) if stricter URL validation is needed.

♻️ Suggested enhancement
 func ValidateResourceInfo(resource v2.ResourceInfo) error {
 	if resource.URL == "" {
 		return fmt.Errorf("resource URL cannot be empty")
 	}

 	// Validate URL format
-	if _, err := url.Parse(resource.URL); err != nil {
+	parsed, err := url.Parse(resource.URL)
+	if err != nil {
 		return fmt.Errorf("invalid resource URL: %w", err)
 	}
+	
+	// Optionally validate scheme
+	if parsed.Scheme == "" {
+		return fmt.Errorf("resource URL must have a scheme")
+	}

 	return nil
 }
v2/http/transport.go (1)

138-160: Consider emitting failure callback when settlement indicates failure.

Currently, OnPaymentSuccess is only called when settlement.Success is true, but there's no corresponding failure callback when settlement exists but indicates failure. This could leave payment failures unobserved.

♻️ Suggested enhancement
 	// Parse settlement response
 	settlement := helpers.ParseSettlement(respRetry.Header.Get("X-PAYMENT-RESPONSE"))

 	// Trigger success callback if settlement indicates success
 	if settlement != nil && settlement.Success && t.OnPaymentSuccess != nil {
 		event := v2.PaymentEvent{
 			Type:        v2.PaymentEventSuccess,
 			Timestamp:   time.Now(),
 			Method:      "HTTP",
 			URL:         req.URL.String(),
 			Transaction: settlement.Transaction,
 			Payer:       settlement.Payer,
 			Duration:    duration,
 		}
 		if selectedRequirement != nil {
 			event.Network = selectedRequirement.Network
 			event.Scheme = selectedRequirement.Scheme
 			event.Amount = selectedRequirement.Amount
 			event.Asset = selectedRequirement.Asset
 			event.Recipient = selectedRequirement.PayTo
 		}
 		t.OnPaymentSuccess(event)
+	} else if settlement != nil && !settlement.Success && t.OnPaymentFailure != nil {
+		event := v2.PaymentEvent{
+			Type:      v2.PaymentEventFailure,
+			Timestamp: time.Now(),
+			Method:    "HTTP",
+			URL:       req.URL.String(),
+			Error:     fmt.Errorf("settlement failed: %s", settlement.ErrorReason),
+			Duration:  duration,
+		}
+		t.OnPaymentFailure(event)
 	}

Note: You'll need to add "fmt" to the imports.

v2/validation/validation_test.go (1)

83-89: Test naming nit: “EVM mainnet/testnet” labels don’t match the chain IDs used.
eip155:8453 / eip155:84532 are Base mainnet / Base Sepolia, not Ethereum mainnet/testnet.

v2/mcp/client/transport.go (2)

141-183: Use payment.Accepted for the attempt callback (avoid ambiguous “re-find” logic).
Matching only on Network + Scheme can pick the wrong requirement when multiple accepts share those fields. payment.Accepted is already the selected requirement.

Proposed simplification
 	// Use selector to choose signer and create payment
 	payment, err := t.config.Selector.SelectAndSign(t.config.Signers, requirements)
@@
-	// Find the requirement that was actually selected by matching the payment's network and scheme
-	// This ensures the payment attempt event reflects the actual requirement that was chosen
-	var selectedReq *v2.PaymentRequirements
-	for i := range requirements {
-		if requirements[i].Network == payment.Accepted.Network && requirements[i].Scheme == payment.Accepted.Scheme {
-			selectedReq = &requirements[i]
-			break
-		}
-	}
-
-	// Trigger payment attempt callback with the actually selected requirement
-	if t.config.OnPaymentAttempt != nil && selectedReq != nil {
+	// Trigger payment attempt callback with the selected requirement
+	if t.config.OnPaymentAttempt != nil {
 		t.config.OnPaymentAttempt(v2.PaymentEvent{
 			Type:      v2.PaymentEventAttempt,
 			Timestamp: startTime,
 			Method:    "MCP",
-			Amount:    selectedReq.Amount,
-			Asset:     selectedReq.Asset,
-			Network:   selectedReq.Network,
-			Recipient: selectedReq.PayTo,
-			Scheme:    selectedReq.Scheme,
+			Amount:    payment.Accepted.Amount,
+			Asset:     payment.Accepted.Asset,
+			Network:   payment.Accepted.Network,
+			Recipient: payment.Accepted.PayTo,
+			Scheme:    payment.Accepted.Scheme,
 		})
 	}

244-258: Consider firing OnPaymentFailure for non-402 JSON-RPC errors on retry too.
Right now only retry transport errors and “still 402” trigger the failure hook; other error codes return without emitting failure.

v2/encoding/encoding.go (1)

17-129: Optional: reduce duplication with shared helpers (or generics) to keep behavior consistent.
Not required, but it’ll make future additions less error-prone.

v2/mcp/server/server.go (1)

83-93: Consider adding graceful shutdown support.

http.ListenAndServe blocks indefinitely with no shutdown mechanism. For production use, consider accepting a context or returning *http.Server to enable graceful shutdown.

♻️ Suggested improvement
-// Start starts the MCP server on the given address.
-func (s *X402Server) Start(addr string) error {
+// Start starts the MCP server on the given address and returns the server for shutdown control.
+func (s *X402Server) Start(addr string) (*http.Server, error) {
 	handler := s.Handler()
 	if s.config.Verbose {
 		fmt.Printf("Starting x402 v2 MCP server on %s\n", addr)
 		fmt.Printf("Facilitator URL: %s\n", s.config.FacilitatorURL)
 		fmt.Printf("Verify-only mode: %v\n", s.config.VerifyOnly)
 		fmt.Printf("Protected tools: %d\n", len(s.config.PaymentTools))
 	}
-	return http.ListenAndServe(addr, handler)
+	server := &http.Server{Addr: addr, Handler: handler}
+	return server, server.ListenAndServe()
 }
examples/v2/gin/main.go (1)

245-314: Consider extracting duplicated signer creation logic.

The signer creation blocks for SVM (lines 246-275) and EVM (lines 276-314) share a similar pattern. For an example, this is acceptable, but extracting a helper could improve maintainability.

examples/v2/http/main.go (1)

136-167: Consider handling JSON encoding errors.

The json.NewEncoder(w).Encode(response) calls on lines 154 and 166 discard errors. While acceptable for examples, this could silently fail.

♻️ Suggested improvement
 	w.Header().Set("Content-Type", "application/json")
-	_ = json.NewEncoder(w).Encode(response)
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		http.Error(w, "failed to encode response", http.StatusInternalServerError)
+		return
+	}
v2/selector.go (1)

129-139: Consider retry with next candidate on signing failure.

Currently, if signing fails for the highest-priority candidate, the error is returned immediately. Depending on use case, it might be beneficial to try the next candidate.

v2/http/gin/middleware_test.go (1)

19-452: Consider consolidating tests using table-driven pattern.

Based on learnings, test files should use table-driven tests. The current tests are well-structured individually but could be consolidated for similar test cases (e.g., NoPayment, VerifyOnly, and InvalidHeader tests share setup patterns).

v2/http/client.go (1)

53-94: Consider using getOrCreateTransport helper consistently.

WithSigner and WithSelector duplicate the transport creation logic that's already extracted into getOrCreateTransport (lines 139-151). Using the helper would reduce duplication.

♻️ Suggested refactor for WithSigner
 func WithSigner(signer v2.Signer) ClientOption {
 	return func(c *Client) error {
-		// Get or create the X402Transport
-		transport, ok := c.Transport.(*X402Transport)
-		if !ok {
-			// Wrap the existing transport
-			transport = &X402Transport{
-				Base:     c.Transport,
-				Signers:  []v2.Signer{},
-				Selector: v2.NewDefaultPaymentSelector(),
-			}
-			c.Transport = transport
-		}
-
-		// Add the signer
+		transport := getOrCreateTransport(c)
 		transport.Signers = append(transport.Signers, signer)
 		return nil
 	}
 }
v2/types.go (3)

196-213: Consider adding JSON tags for consistency.

Unlike all other types in this file, TokenConfig lacks JSON struct tags. If this type is intended for internal configuration only (not serialized to/from JSON), this is fine. However, for consistency and future-proofing, consider adding JSON tags.

♻️ Optional: Add JSON tags
 // TokenConfig defines a token supported by a signer.
 type TokenConfig struct {
 	// Address is the token contract address (EVM) or mint address (Solana).
-	Address string
+	Address string `json:"address"`
 
 	// Symbol is the token symbol (e.g., "USDC").
-	Symbol string
+	Symbol string `json:"symbol"`
 
 	// Decimals is the number of decimal places for the token.
-	Decimals int
+	Decimals int `json:"decimals"`
 
 	// Priority is the token's priority level within the signer.
 	// Lower numbers indicate higher priority (1 > 2 > 3).
-	Priority int
+	Priority int `json:"priority,omitempty"`
 
 	// Name is an optional human-readable token name.
-	Name string
+	Name string `json:"name,omitempty"`
 }

215-234: Consider validating non-negative decimals.

The function works correctly for typical use cases. However, if decimals is negative, big.Int.Exp(10, negative, nil) returns 0, which would silently convert any amount to 0 with "Exact" accuracy. While callers should provide valid decimal values, a defensive check would prevent subtle bugs.

♻️ Add decimals validation
 func AmountToBigInt(amount string, decimals int) (*big.Int, error) {
+	if decimals < 0 {
+		return nil, ErrInvalidAmount
+	}
+
 	// Parse decimal string and convert to atomic units
 	value := new(big.Float)
 	if _, ok := value.SetString(amount); !ok {
 		return nil, ErrInvalidAmount
 	}

236-249: Same decimals validation concern applies here.

Similar to AmountToBigInt, negative decimals would create a zero divisor. big.Float.Quo with zero divisor produces infinity, and Text() would output "+Inf" instead of a valid amount string.

♻️ Add decimals validation
 func BigIntToAmount(value *big.Int, decimals int) string {
 	if value == nil {
 		return "0"
 	}
+	if decimals < 0 {
+		return "0"
+	}
 
 	// Convert to float and divide by 10^decimals
 	f := new(big.Float).SetInt(value)

Alternatively, return an error (which would require changing the function signature) or panic for invalid input if this is considered a programming error.

v2/http/internal/helpers/helpers.go (1)

92-99: Consider proxy-aware scheme detection.

r.TLS != nil check won't detect HTTPS when the server is behind a reverse proxy (nginx, load balancer). Consider checking X-Forwarded-Proto header for proxy deployments.

♻️ Proxy-aware implementation
 func BuildResourceURL(r *http.Request) string {
 	scheme := "http"
-	if r.TLS != nil {
+	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
+		scheme = proto
+	} else if r.TLS != nil {
 		scheme = "https"
 	}
 	return scheme + "://" + r.Host + r.RequestURI
 }
v2/mcp/server/requirements.go (1)

10-25: Potential redundant scheme validation.

The check at line 20 (req.Scheme != "exact") appears redundant since validation.ValidatePaymentRequirements (per v2/validation/validation.go lines 134-140) already validates the scheme and only allows "exact" or empty (which it rejects). The MCP-specific error message is more descriptive though, so keeping it provides better context.

If the intent is to provide MCP-specific error messages, consider adding a comment explaining why:

// MCP-specific validation: only 'exact' scheme is supported
// Note: This check is intentionally redundant with shared validation
// to provide a more descriptive MCP-specific error message.
if req.Scheme != "exact" {
v2/http/client_test.go (1)

14-27: Test logic is correct despite static analysis warning.

The staticcheck warning about possible nil pointer dereference is a false positive. Line 20 checks client == nil and returns early via t.Error, so line 24 is only reached when client is non-nil.

However, consider using t.Fatal instead of t.Error on line 21 to make the control flow clearer:

♻️ Clearer control flow
 	if client == nil {
-		t.Error("Expected non-nil client")
+		t.Fatal("Expected non-nil client")
 	}
v2/http/transport_test.go (1)

50-73: Test logic is correct; consider suppressing linter warning.

The unchecked w.Write return value at line 53 is flagged by the linter. In test code this is low-risk, but for consistency:

♻️ Suppress linter warning
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusOK)
-		w.Write([]byte("OK"))
+		_, _ = w.Write([]byte("OK"))
 	}))
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d563bb and c831a8b.

📒 Files selected for processing (49)
  • AGENTS.md
  • examples/v2/gin/main.go
  • examples/v2/http/main.go
  • examples/v2/mcp/main.go
  • v2/chains.go
  • v2/chains_test.go
  • v2/config.go
  • v2/encoding/encoding.go
  • v2/encoding/encoding_test.go
  • v2/errors.go
  • v2/events.go
  • v2/facilitator/interface.go
  • v2/http/client.go
  • v2/http/client_test.go
  • v2/http/facilitator.go
  • v2/http/facilitator_test.go
  • v2/http/gin/middleware.go
  • v2/http/gin/middleware_test.go
  • v2/http/internal/helpers/helpers.go
  • v2/http/internal/helpers/helpers_test.go
  • v2/http/middleware.go
  • v2/http/middleware_test.go
  • v2/http/transport.go
  • v2/http/transport_test.go
  • v2/internal/eip3009/eip3009.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/internal/solana/solana.go
  • v2/mcp/client/config.go
  • v2/mcp/client/transport.go
  • v2/mcp/client/transport_test.go
  • v2/mcp/errors.go
  • v2/mcp/server/config.go
  • v2/mcp/server/facilitator.go
  • v2/mcp/server/handler.go
  • v2/mcp/server/handler_test.go
  • v2/mcp/server/requirements.go
  • v2/mcp/server/server.go
  • v2/mcp/types.go
  • v2/selector.go
  • v2/selector_test.go
  • v2/signer.go
  • v2/signers/evm/signer.go
  • v2/signers/evm/signer_test.go
  • v2/signers/svm/signer.go
  • v2/signers/svm/signer_test.go
  • v2/types.go
  • v2/types_test.go
  • v2/validation/validation.go
  • v2/validation/validation_test.go
💤 Files with no reviewable changes (1)
  • AGENTS.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Group imports as: stdlib, external dependencies, then internal packages with blank lines between groups
Use camelCase for variables and functions, PascalCase for exported items, avoid abbreviations
Always check errors; wrap with context using fmt.Errorf("context: %w", err) for error handling
For exported functions, start comments with function name; use // for inline comments, /* */ for block comments
Prefer channels over mutexes for concurrency; always handle goroutine lifecycles properly

Files:

  • v2/signer.go
  • v2/encoding/encoding_test.go
  • v2/http/transport.go
  • v2/mcp/client/transport_test.go
  • v2/http/internal/helpers/helpers_test.go
  • v2/config.go
  • v2/selector_test.go
  • v2/validation/validation.go
  • v2/http/facilitator_test.go
  • v2/facilitator/interface.go
  • v2/mcp/server/server.go
  • v2/http/gin/middleware.go
  • v2/mcp/server/requirements.go
  • v2/signers/svm/signer_test.go
  • v2/encoding/encoding.go
  • v2/mcp/client/config.go
  • v2/validation/validation_test.go
  • v2/http/internal/helpers/helpers.go
  • v2/mcp/client/transport.go
  • v2/internal/solana/solana.go
  • v2/http/client.go
  • v2/selector.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/http/middleware_test.go
  • v2/chains_test.go
  • v2/errors.go
  • v2/signers/evm/signer_test.go
  • v2/types_test.go
  • v2/internal/eip3009/eip3009.go
  • v2/signers/evm/signer.go
  • v2/http/client_test.go
  • v2/http/transport_test.go
  • v2/events.go
  • v2/mcp/server/handler_test.go
  • v2/http/gin/middleware_test.go
  • v2/mcp/errors.go
  • v2/mcp/server/config.go
  • v2/http/facilitator.go
  • examples/v2/http/main.go
  • examples/v2/mcp/main.go
  • examples/v2/gin/main.go
  • v2/http/middleware.go
  • v2/mcp/types.go
  • v2/mcp/server/facilitator.go
  • v2/types.go
  • v2/chains.go
  • v2/mcp/server/handler.go
  • v2/signers/svm/signer.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Test files must end with _test.go; use table-driven tests; mock external dependencies

Files:

  • v2/encoding/encoding_test.go
  • v2/mcp/client/transport_test.go
  • v2/http/internal/helpers/helpers_test.go
  • v2/selector_test.go
  • v2/http/facilitator_test.go
  • v2/signers/svm/signer_test.go
  • v2/validation/validation_test.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/http/middleware_test.go
  • v2/chains_test.go
  • v2/signers/evm/signer_test.go
  • v2/types_test.go
  • v2/http/client_test.go
  • v2/http/transport_test.go
  • v2/mcp/server/handler_test.go
  • v2/http/gin/middleware_test.go
🧠 Learnings (1)
📚 Learning: 2025-12-04T10:19:11.236Z
Learnt from: CR
Repo: mark3labs/x402-go PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:19:11.236Z
Learning: Applies to **/*_test.go : Test files must end with `_test.go`; use table-driven tests; mock external dependencies

Applied to files:

  • v2/mcp/client/transport_test.go
  • v2/http/internal/helpers/helpers_test.go
  • v2/validation/validation_test.go
  • v2/types_test.go
  • v2/http/client_test.go
  • v2/mcp/server/handler_test.go
  • v2/http/gin/middleware_test.go
🧬 Code graph analysis (35)
v2/signer.go (1)
v2/types.go (3)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • TokenConfig (197-213)
v2/encoding/encoding_test.go (2)
v2/types.go (8)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • SettleResponse (152-167)
  • PaymentRequired (64-79)
  • Extension (55-61)
  • VerifyResponse (140-149)
v2/encoding/encoding.go (8)
  • EncodePayment (17-23)
  • DecodePayment (28-41)
  • EncodeSettlement (47-53)
  • DecodeSettlement (58-71)
  • EncodeRequirements (76-82)
  • DecodeRequirements (87-100)
  • EncodeVerifyResponse (105-111)
  • DecodeVerifyResponse (116-129)
v2/http/transport.go (1)
v2/http/internal/helpers/helpers.go (3)
  • ParsePaymentRequirements (58-70)
  • BuildPaymentHeader (88-90)
  • ParseSettlement (74-85)
v2/mcp/client/transport_test.go (5)
v2/mcp/client/transport.go (1)
  • Transport (16-19)
v2/mcp/client/config.go (6)
  • DefaultConfig (99-106)
  • WithPaymentAttemptCallback (64-68)
  • WithPaymentSuccessCallback (71-75)
  • WithPaymentFailureCallback (78-82)
  • WithPaymentCallback (55-61)
  • WithSelector (85-89)
v2/mcp/server/config.go (1)
  • DefaultConfig (67-75)
v2/mcp/errors.go (1)
  • ErrNoPaymentRequirements (27-27)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/http/internal/helpers/helpers_test.go (2)
v2/types.go (4)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
  • SettleResponse (152-167)
v2/http/internal/helpers/helpers.go (6)
  • ParsePaymentHeader (14-31)
  • SendPaymentRequired (34-45)
  • AddPaymentResponseHeader (48-55)
  • ParsePaymentRequirements (58-70)
  • ParseSettlement (74-85)
  • BuildResourceURL (93-99)
v2/selector_test.go (4)
v2/types.go (6)
  • TokenConfig (197-213)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • EVMPayload (102-108)
  • EVMAuthorization (111-129)
v2/selector.go (1)
  • FindMatchingRequirement (148-160)
v2/signer.go (1)
  • Signer (8-32)
v2/errors.go (5)
  • ErrorCode (63-63)
  • ErrCodeNoValidSigner (67-67)
  • ErrCodeInvalidRequirements (73-73)
  • PaymentError (86-98)
  • ErrCodeSigningFailed (76-76)
v2/validation/validation.go (2)
v2/chains.go (3)
  • ValidateNetwork (197-231)
  • NetworkTypeEVM (16-16)
  • NetworkTypeSVM (18-18)
v2/types.go (5)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • PaymentRequired (64-79)
v2/http/facilitator_test.go (4)
v2/types.go (7)
  • VerifyResponse (140-149)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • PaymentRequirements (31-52)
  • SettleResponse (152-167)
  • SupportedResponse (185-194)
  • SupportedKind (170-182)
v2/http/facilitator.go (2)
  • FacilitatorClient (41-81)
  • AuthorizationProvider (26-26)
v2/config.go (2)
  • DefaultTimeouts (21-25)
  • TimeoutConfig (9-18)
v2/errors.go (3)
  • ErrVerificationFailed (47-47)
  • ErrSettlementFailed (50-50)
  • ErrFacilitatorUnavailable (44-44)
v2/facilitator/interface.go (1)
v2/types.go (6)
  • PaymentPayload (82-99)
  • PaymentRequirements (31-52)
  • VerifyResponse (140-149)
  • SettleResponse (152-167)
  • SupportedResponse (185-194)
  • X402Version (15-15)
v2/http/gin/middleware.go (2)
v2/http/internal/helpers/helpers.go (2)
  • BuildResourceURL (93-99)
  • ParsePaymentHeader (14-31)
v2/types.go (3)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
v2/mcp/server/requirements.go (3)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/validation/validation.go (1)
  • ValidatePaymentRequirements (111-167)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/signers/svm/signer_test.go (4)
v2/signers/svm/signer.go (5)
  • Option (32-32)
  • WithPriority (107-112)
  • WithMaxAmount (99-104)
  • NewSigner (35-42)
  • NewSignerFromKeygenFile (78-96)
v2/chains.go (3)
  • NetworkSolanaMainnet (36-36)
  • NetworkSolanaDevnet (37-37)
  • NetworkBaseSepolia (30-30)
v2/types.go (1)
  • X402Version (15-15)
v2/internal/solana/solana.go (1)
  • ComputeBudgetProgramID (15-15)
v2/encoding/encoding.go (1)
v2/types.go (4)
  • PaymentPayload (82-99)
  • SettleResponse (152-167)
  • PaymentRequired (64-79)
  • VerifyResponse (140-149)
v2/mcp/client/config.go (1)
v2/mcp/server/config.go (2)
  • Config (21-64)
  • DefaultConfig (67-75)
v2/validation/validation_test.go (1)
v2/validation/validation.go (7)
  • ValidateAmount (28-45)
  • ValidateNetwork (49-61)
  • ValidateAddress (66-92)
  • ValidateResourceInfo (96-107)
  • ValidatePaymentRequirements (111-167)
  • ValidatePaymentPayload (171-201)
  • ValidatePaymentRequired (204-224)
v2/mcp/client/transport.go (4)
v2/mcp/client/config.go (3)
  • Config (11-35)
  • Option (38-38)
  • DefaultConfig (99-106)
v2/mcp/errors.go (2)
  • WrapX402Error (74-82)
  • ErrNoPaymentRequirements (27-27)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/types.go (2)
  • ResourceInfo (18-27)
  • X402Version (15-15)
v2/internal/solana/solana.go (1)
v2/chains.go (2)
  • NetworkSolanaMainnet (36-36)
  • NetworkSolanaDevnet (37-37)
v2/http/client.go (3)
v2/http/transport.go (1)
  • X402Transport (13-31)
v2/types.go (1)
  • SettleResponse (152-167)
v2/http/internal/helpers/helpers.go (1)
  • ParseSettlement (74-85)
v2/selector.go (3)
v2/signer.go (1)
  • Signer (8-32)
v2/types.go (2)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
v2/errors.go (8)
  • NewPaymentError (114-121)
  • ErrCodeNoValidSigner (67-67)
  • ErrNoValidSigner (8-8)
  • ErrCodeInvalidRequirements (73-73)
  • ErrInvalidRequirements (14-14)
  • ErrCodeSigningFailed (76-76)
  • ErrCodeUnsupportedScheme (82-82)
  • ErrUnsupportedScheme (59-59)
v2/http/middleware_test.go (2)
v2/types.go (4)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
  • SettleResponse (152-167)
v2/http/middleware.go (3)
  • Config (16-59)
  • NewX402Middleware (71-216)
  • GetPaymentFromContext (305-315)
v2/signers/evm/signer_test.go (3)
v2/signers/evm/signer.go (3)
  • NewSigner (28-57)
  • WithMaxAmount (91-96)
  • GetChainID (201-222)
v2/types.go (1)
  • X402Version (15-15)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/types_test.go (1)
v2/types.go (15)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • PaymentRequired (64-79)
  • PaymentPayload (82-99)
  • EVMPayload (102-108)
  • EVMAuthorization (111-129)
  • Extension (55-61)
  • VerifyResponse (140-149)
  • SettleResponse (152-167)
  • SupportedResponse (185-194)
  • SupportedKind (170-182)
  • AmountToBigInt (217-234)
  • BigIntToAmount (238-249)
  • TokenConfig (197-213)
v2/http/transport_test.go (2)
v2/types.go (4)
  • X402Version (15-15)
  • PaymentRequired (64-79)
  • ResourceInfo (18-27)
  • SettleResponse (152-167)
v2/http/transport.go (1)
  • X402Transport (13-31)
v2/http/gin/middleware_test.go (2)
v2/http/gin/middleware.go (4)
  • Config (18-18)
  • NewX402Middleware (60-208)
  • GetPaymentFromContext (225-235)
  • PaymentContextKey (21-21)
v2/types.go (3)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
  • X402Version (15-15)
v2/mcp/server/config.go (4)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/mcp/server/handler.go (4)
  • AuthorizationProvider (50-50)
  • OnBeforeFunc (53-53)
  • OnAfterVerifyFunc (56-56)
  • OnAfterSettleFunc (59-59)
v2/mcp/client/config.go (2)
  • DefaultConfig (99-106)
  • Config (11-35)
v2/http/facilitator.go (5)
v2/types.go (7)
  • PaymentPayload (82-99)
  • PaymentRequirements (31-52)
  • VerifyResponse (140-149)
  • SettleResponse (152-167)
  • X402Version (15-15)
  • SupportedResponse (185-194)
  • SupportedKind (170-182)
v2/config.go (1)
  • TimeoutConfig (9-18)
v2/facilitator/interface.go (2)
  • Interface (16-28)
  • VerifyRequest (31-40)
retry/retry.go (1)
  • WithRetry (32-81)
v2/errors.go (3)
  • ErrFacilitatorUnavailable (44-44)
  • ErrVerificationFailed (47-47)
  • ErrSettlementFailed (50-50)
examples/v2/http/main.go (3)
v2/chains.go (1)
  • GetChainConfig (186-192)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/signers/svm/signer.go (1)
  • NewSignerFromKeygenFile (78-96)
examples/v2/mcp/main.go (3)
v2/chains.go (1)
  • GetChainConfig (186-192)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/signers/svm/signer.go (1)
  • NewSignerFromKeygenFile (78-96)
v2/http/middleware.go (1)
v2/http/internal/helpers/helpers.go (4)
  • BuildResourceURL (93-99)
  • SendPaymentRequired (34-45)
  • ParsePaymentHeader (14-31)
  • AddPaymentResponseHeader (48-55)
v2/mcp/types.go (1)
v2/types.go (3)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • Extension (55-61)
v2/mcp/server/facilitator.go (4)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/types.go (1)
  • SettleResponse (152-167)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/mcp/server/handler.go (4)
  • AuthorizationProvider (50-50)
  • OnBeforeFunc (53-53)
  • OnAfterVerifyFunc (56-56)
  • OnAfterSettleFunc (59-59)
v2/types.go (2)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/errors.go (1)
  • ErrInvalidAmount (23-23)
v2/chains.go (3)
v2/errors.go (1)
  • ErrInvalidNetwork (29-29)
v2/validation/validation.go (1)
  • ValidateNetwork (49-61)
v2/types.go (1)
  • TokenConfig (197-213)
v2/mcp/server/handler.go (5)
v2/mcp/server/config.go (3)
  • Config (21-64)
  • DefaultConfig (67-75)
  • ToolPaymentConfig (12-18)
v2/mcp/server/facilitator.go (1)
  • Facilitator (15-21)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/types.go (2)
  • SettleResponse (152-167)
  • X402Version (15-15)
v2/mcp/server/requirements.go (1)
  • SetToolResource (29-33)
v2/signers/svm/signer.go (2)
v2/internal/solana/solana.go (7)
  • GetRPCURL (90-99)
  • DeriveAssociatedTokenAddress (81-87)
  • BuildSetComputeUnitLimitInstruction (43-56)
  • DefaultComputeUnits (18-18)
  • BuildSetComputeUnitPriceInstruction (61-78)
  • DefaultComputeUnitPrice (21-21)
  • BuildTransferCheckedInstruction (24-38)
v2/types.go (1)
  • X402Version (15-15)
🪛 GitHub Actions: golangci-lint
v2/http/client_test.go

[error] 199-199: golangci-lint: Error return value of (*encoding/json.Encoder).Encode is not checked (errcheck).

🪛 GitHub Check: lint
v2/http/facilitator_test.go

[failure] 208-208:
Error return value of (*encoding/json.Encoder).Encode is not checked (errcheck)

v2/http/internal/helpers/helpers.go

[failure] 44-44:
Error return value of (*encoding/json.Encoder).Encode is not checked (errcheck)

v2/internal/eip3009/eip3009_test.go

[failure] 24-24:
SA4006: this value of nonce is never used (staticcheck)

v2/http/middleware_test.go

[failure] 156-156:
Error return value of w.Write is not checked (errcheck)

v2/http/client_test.go

[failure] 287-287:
type mockSigner2 is unused (unused)


[failure] 24-24:
SA5011: possible nil pointer dereference (staticcheck)


[failure] 20-20:
SA5011(related information): this check suggests that the pointer can be nil (staticcheck)


[failure] 212-212:
Error return value of w.Write is not checked (errcheck)


[failure] 199-199:
Error return value of (*encoding/json.Encoder).Encode is not checked (errcheck)

v2/http/transport_test.go

[failure] 53-53:
Error return value of w.Write is not checked (errcheck)

🪛 Gitleaks (8.30.0)
v2/internal/eip3009/eip3009_test.go

[high] 17-17: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

v2/signers/evm/signer_test.go

[high] 12-12: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test

Comment on lines 298 to 336
// EnrichRequirements fetches supported payment types from the facilitator and
// enriches the provided payment requirements with network-specific data like feePayer.
// This is particularly useful for SVM chains where the feePayer must be specified.
func (c *FacilitatorClient) EnrichRequirements(requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
// Fetch supported payment types
supported, err := c.Supported(context.Background())
if err != nil {
return requirements, fmt.Errorf("failed to fetch supported payment types: %w", err)
}

// Create a lookup map for supported kinds by network and scheme
supportedMap := make(map[string]v2.SupportedKind)
for _, kind := range supported.Kinds {
key := kind.Network + "-" + kind.Scheme
supportedMap[key] = kind
}

// Enrich each requirement with extra data from the facilitator
enriched := make([]v2.PaymentRequirements, len(requirements))
for i, req := range requirements {
enriched[i] = req
key := req.Network + "-" + req.Scheme
if kind, ok := supportedMap[key]; ok && kind.Extra != nil {
// Initialize Extra map if it doesn't exist
if enriched[i].Extra == nil {
enriched[i].Extra = make(map[string]interface{})
}
// Merge facilitator's extra data into requirement
for k, v := range kind.Extra {
// Only set if not already present (user-specified values take precedence)
if _, exists := enriched[i].Extra[k]; !exists {
enriched[i].Extra[k] = v
}
}
}
}

return enriched, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t hardcode context.Background() in EnrichRequirements (kills cancellation/timeout control).

Proposed fix (API change, but new surface)
-func (c *FacilitatorClient) EnrichRequirements(requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
+func (c *FacilitatorClient) EnrichRequirements(ctx context.Context, requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
 	// Fetch supported payment types
-	supported, err := c.Supported(context.Background())
+	supported, err := c.Supported(ctx)
 	if err != nil {
 		return requirements, fmt.Errorf("failed to fetch supported payment types: %w", err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// EnrichRequirements fetches supported payment types from the facilitator and
// enriches the provided payment requirements with network-specific data like feePayer.
// This is particularly useful for SVM chains where the feePayer must be specified.
func (c *FacilitatorClient) EnrichRequirements(requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
// Fetch supported payment types
supported, err := c.Supported(context.Background())
if err != nil {
return requirements, fmt.Errorf("failed to fetch supported payment types: %w", err)
}
// Create a lookup map for supported kinds by network and scheme
supportedMap := make(map[string]v2.SupportedKind)
for _, kind := range supported.Kinds {
key := kind.Network + "-" + kind.Scheme
supportedMap[key] = kind
}
// Enrich each requirement with extra data from the facilitator
enriched := make([]v2.PaymentRequirements, len(requirements))
for i, req := range requirements {
enriched[i] = req
key := req.Network + "-" + req.Scheme
if kind, ok := supportedMap[key]; ok && kind.Extra != nil {
// Initialize Extra map if it doesn't exist
if enriched[i].Extra == nil {
enriched[i].Extra = make(map[string]interface{})
}
// Merge facilitator's extra data into requirement
for k, v := range kind.Extra {
// Only set if not already present (user-specified values take precedence)
if _, exists := enriched[i].Extra[k]; !exists {
enriched[i].Extra[k] = v
}
}
}
}
return enriched, nil
}
// EnrichRequirements fetches supported payment types from the facilitator and
// enriches the provided payment requirements with network-specific data like feePayer.
// This is particularly useful for SVM chains where the feePayer must be specified.
func (c *FacilitatorClient) EnrichRequirements(ctx context.Context, requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
// Fetch supported payment types
supported, err := c.Supported(ctx)
if err != nil {
return requirements, fmt.Errorf("failed to fetch supported payment types: %w", err)
}
// Create a lookup map for supported kinds by network and scheme
supportedMap := make(map[string]v2.SupportedKind)
for _, kind := range supported.Kinds {
key := kind.Network + "-" + kind.Scheme
supportedMap[key] = kind
}
// Enrich each requirement with extra data from the facilitator
enriched := make([]v2.PaymentRequirements, len(requirements))
for i, req := range requirements {
enriched[i] = req
key := req.Network + "-" + req.Scheme
if kind, ok := supportedMap[key]; ok && kind.Extra != nil {
// Initialize Extra map if it doesn't exist
if enriched[i].Extra == nil {
enriched[i].Extra = make(map[string]interface{})
}
// Merge facilitator's extra data into requirement
for k, v := range kind.Extra {
// Only set if not already present (user-specified values take precedence)
if _, exists := enriched[i].Extra[k]; !exists {
enriched[i].Extra[k] = v
}
}
}
}
return enriched, nil
}
🤖 Prompt for AI Agents
In @v2/http/facilitator.go around lines 298 - 336, EnrichRequirements currently
uses context.Background(), preventing callers from controlling
cancellation/timeouts; change the method signature of
FacilitatorClient.EnrichRequirements to accept a context.Context as the first
parameter (e.g., EnrichRequirements(ctx context.Context, requirements
[]v2.PaymentRequirements) ([]v2.PaymentRequirements, error)), replace the
internal call to c.Supported(context.Background()) with c.Supported(ctx), and
propagate this new ctx to all callers and unit tests (update any call sites and
mocks) so cancellation and deadlines flow through; keep the rest of the merging
logic unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @v2/config.go:
- Around line 45-58: The Validate method on TimeoutConfig currently checks
VerifyTimeout and SettleTimeout but omits RequestTimeout; update
TimeoutConfig.Validate to also ensure RequestTimeout is positive and that it
makes sense relative to other fields (e.g., RequestTimeout > 0 and optionally <=
SettleTimeout or >= VerifyTimeout depending on intended semantics). Modify the
function (TimeoutConfig.Validate) to return an error like "request timeout must
be positive, got %v" when RequestTimeout <= 0 and add any necessary comparative
checks (refer to existing SettleTimeout vs VerifyTimeout check) so all three
timeouts are validated consistently.

In @v2/http/client_test.go:
- Around line 156-169: The test TestClient_WithHTTPClient mistakenly sets
http.Client.Timeout to the integer 30 (nanoseconds) instead of a time.Duration;
update the customClient.Timeout to a proper duration (e.g., 30*time.Second or
time.Second*30) and ensure the time package is imported so the test asserts the
intended timeout when constructing via NewClient(WithHTTPClient(...)); leave the
rest of the test logic unchanged and reference the customClient used by the
NewClient call.

In @v2/internal/solana/solana.go:
- Around line 89-99: The fmt.Errorf call in GetRPCURL incorrectly places the %w
before contextual text which breaks error wrapping; update the return in
GetRPCURL to format the message with the network context first and the sentinel
error wrapped with %w (i.e., make the format "invalid network %s: %w" and pass
network then v2.ErrInvalidNetwork) so errors.Is/As can properly unwrap
v2.ErrInvalidNetwork.

In @v2/signers/svm/signer.go:
- Around line 220-223: The call site uses amount.Uint64() which silently
truncates big.Int values; fix by explicitly checking for overflow before
conversion: compare amount against a big.Int set to math.MaxUint64 (e.g., max :=
new(big.Int).SetUint64(math.MaxUint64)) and if amount.Cmp(max) > 0 return an
error (or handle as appropriate) instead of converting, otherwise safely cast
with amount.Uint64(); update the code around the call that passes mintAddress,
recipient, amount.Uint64(), decimals to perform this check and fail early to
avoid incorrect transfer amounts.
🧹 Nitpick comments (45)
v2/internal/solana/solana.go (2)

40-56: Consider using encoding/binary for clearer byte encoding.

The manual bit-shifting is correct but could be more readable using the standard library's binary.LittleEndian.PutUint32.

♻️ Suggested refactor
+import "encoding/binary"
+
 func BuildSetComputeUnitLimitInstruction(units uint32) solana.Instruction {
 	data := make([]byte, 5)
 	data[0] = 2 // SetComputeUnitLimit discriminator
-	data[1] = byte(units)
-	data[2] = byte(units >> 8)
-	data[3] = byte(units >> 16)
-	data[4] = byte(units >> 24)
+	binary.LittleEndian.PutUint32(data[1:], units)
 
 	return solana.NewInstruction(
 		ComputeBudgetProgramID,
 		solana.AccountMetaSlice{},
 		data,
 	)
 }

58-78: Same refactor opportunity with encoding/binary.

Consistent with the previous suggestion, this can be simplified using binary.LittleEndian.PutUint64.

♻️ Suggested refactor
 func BuildSetComputeUnitPriceInstruction(microlamports uint64) solana.Instruction {
 	data := make([]byte, 9)
 	data[0] = 3 // SetComputeUnitPrice discriminator
-	data[1] = byte(microlamports)
-	data[2] = byte(microlamports >> 8)
-	data[3] = byte(microlamports >> 16)
-	data[4] = byte(microlamports >> 24)
-	data[5] = byte(microlamports >> 32)
-	data[6] = byte(microlamports >> 40)
-	data[7] = byte(microlamports >> 48)
-	data[8] = byte(microlamports >> 56)
+	binary.LittleEndian.PutUint64(data[1:], microlamports)
 
 	return solana.NewInstruction(
 		ComputeBudgetProgramID,
 		solana.AccountMetaSlice{},
 		data,
 	)
 }
v2/http/transport_test.go (1)

297-328: Consider verifying OnPaymentFailure callback is invoked.

This test validates that an error is returned when no signers are configured, but doesn't verify that OnPaymentFailure callback is actually invoked. Consider adding callback verification for completeness.

💡 Suggested enhancement
 func TestTransport_FailureCallback(t *testing.T) {
+	var failureCalled bool
+	var failureEvent v2.PaymentEvent
+
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		paymentReq := v2.PaymentRequired{
 			X402Version: 2,
 			Accepts: []v2.PaymentRequirements{
 				{
 					Scheme:  "exact",
 					Network: "eip155:84532",
 					Amount:  "10000",
 				},
 			},
 		}
 		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(http.StatusPaymentRequired)
 		_ = json.NewEncoder(w).Encode(paymentReq)
 	}))
 	defer server.Close()

 	// No signer configured
 	transport := &X402Transport{
 		Base:     http.DefaultTransport,
 		Signers:  []v2.Signer{},
 		Selector: v2.NewDefaultPaymentSelector(),
+		OnPaymentFailure: func(event v2.PaymentEvent) {
+			failureCalled = true
+			failureEvent = event
+		},
 	}

 	req, _ := http.NewRequest("GET", server.URL+"/api/data", nil)
 	_, err := transport.RoundTrip(req)

 	// This should fail because no signers are configured
 	if err == nil {
 		t.Error("Expected error for no signers")
 	}
+
+	if !failureCalled {
+		t.Error("OnPaymentFailure callback was not called")
+	}
+	if failureEvent.Type != v2.PaymentEventFailure {
+		t.Errorf("Expected failure event type, got %s", failureEvent.Type)
+	}
 }
v2/types_test.go (1)

61-94: Consider extending PaymentRequirements test assertions.

The test only verifies Scheme, Network, and Amount after round-trip. Consider adding assertions for Asset, PayTo, MaxTimeoutSeconds, and Extra to ensure complete round-trip integrity.

💡 Suggested enhancement
 	if decoded.Amount != req.Amount {
 		t.Errorf("Amount = %s; want %s", decoded.Amount, req.Amount)
 	}
+	if decoded.Asset != req.Asset {
+		t.Errorf("Asset = %s; want %s", decoded.Asset, req.Asset)
+	}
+	if decoded.PayTo != req.PayTo {
+		t.Errorf("PayTo = %s; want %s", decoded.PayTo, req.PayTo)
+	}
+	if decoded.MaxTimeoutSeconds != req.MaxTimeoutSeconds {
+		t.Errorf("MaxTimeoutSeconds = %d; want %d", decoded.MaxTimeoutSeconds, req.MaxTimeoutSeconds)
+	}
 }
v2/http/internal/helpers/helpers_test.go (1)

333-340: Consider using io.NopCloser from stdlib instead of custom helper.

Go's standard library provides io.NopCloser which serves the same purpose.

💡 Use stdlib io.NopCloser
+import "io"
+
 // Replace nopCloser usage with io.NopCloser
 resp := &http.Response{
 	StatusCode: 402,
-	Body:       &nopCloser{strings.NewReader(string(body))},
+	Body:       io.NopCloser(strings.NewReader(string(body))),
 }
-
-// nopCloser is a helper to create a ReadCloser from a Reader
-type nopCloser struct {
-	*strings.Reader
-}
-
-func (n *nopCloser) Close() error {
-	return nil
-}
v2/http/client_test.go (1)

261-283: Header name casing may affect test reliability.

The test uses "X-Payment-Response" (line 271) while the production code uses "X-PAYMENT-RESPONSE". Go's http.Header is case-insensitive for lookups, so this works, but using consistent casing improves clarity.

💡 Use consistent header casing
 	resp := &http.Response{
 		Header: http.Header{
-			"X-Payment-Response": []string{encoded},
+			"X-PAYMENT-RESPONSE": []string{encoded},
 		},
 	}
v2/signers/svm/signer_test.go (2)

15-18: Address static analysis false positive with clearer documentation.

Gitleaks flagged this as a potential API key. While the comment indicates it's a test key, consider adding a more explicit marker that tools can recognize, or document that this is intentional.

💡 Add nosec comment to suppress false positive
-// Test private key (DO NOT use in production)
-// This is a randomly generated Solana key for testing purposes only
-const testPrivateKeyBase58 = "4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv8KrQk7h2ByqYCKQBWUrbXdqeqSHXv2YvPRzYMNL8hFmjXu"
+// testPrivateKeyBase58 is a randomly generated Solana key for testing purposes only.
+// DO NOT use in production. This key has no real funds and is publicly known.
+// nolint:gosec // Test key - intentionally committed for testing
+const testPrivateKeyBase58 = "4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv8KrQk7h2ByqYCKQBWUrbXdqeqSHXv2YvPRzYMNL8hFmjXu" //nolint:gosec

708-720: Use strings.Contains instead of custom helper.

The custom containsString and indexOfSubstring functions replicate stdlib functionality.

♻️ Use stdlib strings.Contains
+import "strings"
+
 // In test assertions, replace:
-if !containsString(err.Error(), tt.errContains) {
+if !strings.Contains(err.Error(), tt.errContains) {
 
-// Remove these helper functions:
-func containsString(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(substr) == 0 || indexOfSubstring(s, substr) >= 0)
-}
-
-func indexOfSubstring(s, substr string) int {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return i
-		}
-	}
-	return -1
-}
v2/internal/eip3009/eip3009.go (1)

99-99: Consider clearer slice construction.

The nested append pattern works correctly but can be made more readable by pre-allocating:

♻️ Suggested improvement
-	rawData := append([]byte{0x19, 0x01}, append(domainSeparator, messageHash...)...)
+	rawData := make([]byte, 0, 2+len(domainSeparator)+len(messageHash))
+	rawData = append(rawData, 0x19, 0x01)
+	rawData = append(rawData, domainSeparator...)
+	rawData = append(rawData, messageHash...)
v2/mcp/client/config.go (1)

98-106: Consider noting http.DefaultClient has no timeout.

Using http.DefaultClient as the default is convenient but worth noting in documentation that it has no timeout configured. Users making requests to untrusted servers may want to set a custom client with timeouts.

examples/v2/mcp/main.go (1)

184-191: Graceful shutdown could be improved.

The shutdown goroutine calls os.Exit(0) directly without allowing in-flight requests to complete. For an example, this is acceptable, but production code should use proper graceful shutdown with http.Server.Shutdown(ctx).

v2/errors.go (1)

123-127: WithDetails mutates the receiver in-place.

Unlike the value-receiver fluent methods in config.go, WithDetails mutates the receiver. This is acceptable for error building but could cause issues if an error is shared and modified. Document this behavior or consider defensive copying.

v2/mcp/errors.go (1)

48-54: Note: Two PaymentError types in different packages.

This PaymentError (in mcp package) has a different structure than v2.PaymentError. Both are valid but the naming overlap may cause confusion. Consider renaming to MCPPaymentError for clarity, or document the distinction.

v2/chains.go (1)

268-278: NewUSDCTokenConfig ignores chain.Decimals.

The function hardcodes Decimals: 6 instead of using chain.Decimals. While USDC is always 6 decimals, using the chain's value would be more consistent.

♻️ Suggested improvement
 func NewUSDCTokenConfig(chain ChainConfig, priority int) TokenConfig {
 	return TokenConfig{
 		Address:  chain.USDCAddress,
 		Symbol:   "USDC",
-		Decimals: 6,
+		Decimals: int(chain.Decimals),
 		Priority: priority,
 		Name:     "USD Coin",
 	}
 }
v2/types.go (2)

196-213: Missing JSON tags on TokenConfig - inconsistent with other types.

All other exported structs in this file include JSON tags for serialization, but TokenConfig lacks them. This inconsistency may cause issues if this type needs to be marshaled/unmarshaled in the future, or if it's used in contexts that expect JSON field names (e.g., configuration files, APIs).

♻️ Suggested fix to add JSON tags
 // TokenConfig defines a token supported by a signer.
 type TokenConfig struct {
 	// Address is the token contract address (EVM) or mint address (Solana).
-	Address string
+	Address string `json:"address"`

 	// Symbol is the token symbol (e.g., "USDC").
-	Symbol string
+	Symbol string `json:"symbol"`

 	// Decimals is the number of decimal places for the token.
-	Decimals int
+	Decimals int `json:"decimals"`

 	// Priority is the token's priority level within the signer.
 	// Lower numbers indicate higher priority (1 > 2 > 3).
-	Priority int
+	Priority int `json:"priority"`

 	// Name is an optional human-readable token name.
-	Name string
+	Name string `json:"name,omitempty"`
 }

215-234: Consider edge cases for decimal conversion.

The big.Float approach works for most cases, and the big.Exact accuracy check is good for catching precision loss. However, a couple of edge cases to consider:

  1. Negative decimals values would cause unexpected behavior with Exp()
  2. Very small fractional amounts near the precision limit could theoretically have rounding issues before the accuracy check catches them

These are unlikely in practice since token decimals are typically 6-18, but defensive validation could help.

♻️ Optional: Add defensive check for decimals
 func AmountToBigInt(amount string, decimals int) (*big.Int, error) {
+	if decimals < 0 {
+		return nil, ErrInvalidAmount
+	}
+
 	// Parse decimal string and convert to atomic units
 	value := new(big.Float)
 	if _, ok := value.SetString(amount); !ok {
 		return nil, ErrInvalidAmount
 	}
v2/chains_test.go (1)

362-374: Replace custom string helpers with strings.Contains.

The containsString and containsSubstring helper functions reimplement strings.Contains from the standard library. Using the stdlib function is clearer and more maintainable.

♻️ Suggested simplification
+import (
+	"errors"
+	"strings"
+	"testing"
+)

-// Helper function
-func containsString(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
-}
-
-func containsSubstring(s, substr string) bool {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
-}

Then update the usage at line 150:

-			if err == nil || !containsString(err.Error(), tt.errContains) {
+			if err == nil || !strings.Contains(err.Error(), tt.errContains) {
v2/validation/validation.go (2)

94-107: URL validation may be too permissive.

url.Parse accepts almost any string including empty paths and relative URLs. Consider validating that the URL has a scheme (http/https) for resource URLs.

♻️ Suggested stricter URL validation
 func ValidateResourceInfo(resource v2.ResourceInfo) error {
 	if resource.URL == "" {
 		return fmt.Errorf("resource URL cannot be empty")
 	}
 
 	// Validate URL format
-	if _, err := url.Parse(resource.URL); err != nil {
+	parsedURL, err := url.Parse(resource.URL)
+	if err != nil {
 		return fmt.Errorf("invalid resource URL: %w", err)
 	}
+	if parsedURL.Scheme == "" || parsedURL.Host == "" {
+		return fmt.Errorf("invalid resource URL: must be an absolute URL with scheme and host")
+	}
 
 	return nil
 }

169-201: Consider validating the full Accepted requirements in ValidatePaymentPayload.

Currently, ValidatePaymentPayload only validates Scheme and Network of payload.Accepted, but doesn't call ValidatePaymentRequirements on the full Accepted struct. This could allow invalid Amount, Asset, or PayTo values to pass through.

♻️ Suggested fix to validate full Accepted requirements
 func ValidatePaymentPayload(payload v2.PaymentPayload) error {
 	if payload.X402Version != v2.X402Version {
 		return fmt.Errorf("unsupported x402 version: %d (expected %d)", payload.X402Version, v2.X402Version)
 	}
 
-	// Validate accepted requirements
-	if payload.Accepted.Scheme == "" {
-		return fmt.Errorf("accepted scheme cannot be empty")
-	}
-
-	if payload.Accepted.Network == "" {
-		return fmt.Errorf("accepted network cannot be empty")
-	}
-
-	if _, err := v2.ValidateNetwork(payload.Accepted.Network); err != nil {
-		return fmt.Errorf("invalid accepted network: %w", err)
+	// Validate accepted requirements using the full validation
+	if err := ValidatePaymentRequirements(payload.Accepted); err != nil {
+		return fmt.Errorf("invalid accepted: %w", err)
 	}
 
 	if payload.Payload == nil {
 		return fmt.Errorf("payload cannot be nil")
 	}
v2/http/internal/helpers/helpers.go (1)

33-45: Consider handling JSON encoding error.

The error from json.NewEncoder(w).Encode(response) is silently discarded. While unlikely to fail for well-formed structs, logging or handling the error would improve observability.

♻️ Suggested error logging
 func SendPaymentRequired(w http.ResponseWriter, resource v2.ResourceInfo, requirements []v2.PaymentRequirements, errMsg string) {
 	response := v2.PaymentRequired{
 		X402Version: v2.X402Version,
 		Error:       errMsg,
 		Resource:    resource,
 		Accepts:     requirements,
 	}
 
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusPaymentRequired)
-	_ = json.NewEncoder(w).Encode(response)
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		// Log encoding failure - response headers already sent
+		// Consider using a logger if available
+	}
 }
v2/signers/evm/signer_test.go (1)

37-111: Test uses placeholder PayTo address "0x receiver".

The PayTo field "0x receiver" is not a valid EVM address. While this works for CanSign tests (which may only check network/scheme/asset), it could cause issues if CanSign is updated to validate addresses. Consider using a valid placeholder address for consistency.

♻️ Suggested fix
 		{
 			name: "valid requirements",
 			requirements: &v2.PaymentRequirements{
 				Scheme:            "exact",
 				Network:           "eip155:84532",
 				Asset:             "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
 				Amount:            "1000000",
-				PayTo:             "0x receiver",
+				PayTo:             "0x1234567890123456789012345678901234567890",
 				MaxTimeoutSeconds: 300,
 			},
 			expected: true,
 		},

Apply similar changes to other test cases.

v2/mcp/server/requirements.go (2)

10-25: Scheme validation is redundant with shared validation.

validation.ValidatePaymentRequirements already validates that Scheme is "exact" (see v2/validation/validation.go lines 137-144). The MCP-specific check at line 20 will never trigger because the shared validation would already return an error for non-"exact" schemes.

However, if MCP semantics might diverge from the shared validation in the future, keeping this explicit check provides clarity and future-proofing. The current code is correct but redundant.

♻️ If redundancy is intentional for clarity, add a comment
 	// Use shared validation for most fields
 	if err := validation.ValidatePaymentRequirements(req); err != nil {
 		return err
 	}
 
-	// MCP-specific validation: only 'exact' scheme is supported
+	// MCP-specific validation: only 'exact' scheme is supported.
+	// Note: This is also enforced by ValidatePaymentRequirements, but kept
+	// explicit here for MCP-specific error messaging and future-proofing.
 	if req.Scheme != "exact" {
 		return fmt.Errorf("invalid requirement: unsupported scheme %s (only 'exact' is supported in MCP)", req.Scheme)
 	}

27-33: Consider validating or sanitizing toolName.

SetToolResource directly interpolates toolName into the URL without validation. While mcp:// URLs may have different semantics than HTTP URLs, consider validating that toolName doesn't contain characters that could cause parsing issues (e.g., /, ?, #).

♻️ Suggested validation
+import (
+	"fmt"
+	"strings"
+
+	v2 "github.com/mark3labs/x402-go/v2"
+	"github.com/mark3labs/x402-go/v2/validation"
+)
+
 // SetToolResource sets the resource URL based on the tool name.
 // Returns a ResourceInfo with the standard MCP tool URL format.
 func SetToolResource(toolName string) v2.ResourceInfo {
+	// Sanitize toolName to prevent URL parsing issues
+	toolName = strings.ReplaceAll(toolName, "/", "_")
+	toolName = strings.ReplaceAll(toolName, "?", "_")
+	toolName = strings.ReplaceAll(toolName, "#", "_")
 	return v2.ResourceInfo{
 		URL: fmt.Sprintf("mcp://tools/%s", toolName),
 	}
 }
v2/mcp/client/transport_test.go (2)

100-105: Consider validating specific error types when wantErrType is set.

The test returns early on error without checking wantErrType. The "no accepts" case sets wantErrType: mcp.ErrNoPaymentRequirements but this is never verified.

Proposed fix
 			if tt.wantErr {
 				if err == nil {
 					t.Error("Expected error, got nil")
 				}
+				if tt.wantErrType != nil && !errors.Is(err, tt.wantErrType) {
+					t.Errorf("Expected error type %v, got %v", tt.wantErrType, err)
+				}
 				return
 			}

Note: You'd need to add "errors" to the imports.


122-126: Unused transport variable.

The transport is created but assigned to _ and never used. The test manually reimplements the injection logic rather than testing the actual transport method.

Suggested improvement

Either remove the unused transport creation or refactor to test the actual transport injection method if one exists:

-	_ = &Transport{
-		config: DefaultConfig("http://example.com"),
-	}
examples/v2/gin/main.go (1)

189-388: Consider extracting shared client code.

The runClient() function is nearly identical to the one in examples/v2/http/main.go. For example applications this duplication is acceptable as it keeps each example self-contained, but if you add more examples, consider extracting common client logic to a shared package.

v2/http/gin/middleware_test.go (2)

85-131: Test doesn't fully exercise VerifyOnly mode behavior.

This test verifies 402 is returned without a payment header, but that's the same behavior regardless of VerifyOnly. The distinctive behavior of VerifyOnly=true is that it skips settlement when a valid payment IS provided. Consider adding a test that provides a valid payment and verifies settlement is NOT called while the request still succeeds.


344-375: Consider table-driven test for GetPaymentFromContext.

Per coding guidelines, test files should use table-driven tests. This test has three distinct scenarios that could be consolidated.

♻️ Suggested table-driven approach
func TestGetPaymentFromContext(t *testing.T) {
	expectedPayment := &v2.VerifyResponse{
		IsValid: true,
		Payer:   "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
	}

	tests := []struct {
		name     string
		setup    func(*gin.Context)
		expected *v2.VerifyResponse
	}{
		{
			name:     "empty context",
			setup:    func(c *gin.Context) {},
			expected: nil,
		},
		{
			name: "valid payment in context",
			setup: func(c *gin.Context) {
				c.Set(PaymentContextKey, expectedPayment)
			},
			expected: expectedPayment,
		},
		{
			name: "wrong type in context",
			setup: func(c *gin.Context) {
				c.Set(PaymentContextKey, "not a payment")
			},
			expected: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			tt.setup(c)
			result := GetPaymentFromContext(c)
			if result != tt.expected {
				t.Errorf("expected %v, got %v", tt.expected, result)
			}
		})
	}
}
v2/http/facilitator_test.go (1)

351-395: Retry test has potential flakiness.

The test relies on http.Hijacker which may behave differently across environments. The fallback to 503 is good, but the assertion attempts < 2 at line 392-394 could theoretically pass if the server implementation changes. Consider asserting exact expected attempts count for the success case (should be exactly 3 based on the mock logic).

♻️ Suggested improvement
-	if atomic.LoadInt32(&attempts) < 2 {
-		t.Errorf("Expected at least 2 attempts, got %d", attempts)
+	finalAttempts := atomic.LoadInt32(&attempts)
+	if finalAttempts != 3 {
+		t.Errorf("Expected exactly 3 attempts (2 failures + 1 success), got %d", finalAttempts)
 	}
v2/http/client.go (2)

55-73: Refactor to use getOrCreateTransport helper.

The transport creation logic here duplicates the logic in getOrCreateTransport (lines 138-151). Using the helper would reduce duplication and ensure consistent behavior.

♻️ Proposed refactor
 func WithSigner(signer v2.Signer) ClientOption {
 	return func(c *Client) error {
-		// Get or create the X402Transport
-		transport, ok := c.Transport.(*X402Transport)
-		if !ok {
-			// Wrap the existing transport
-			transport = &X402Transport{
-				Base:     c.Transport,
-				Signers:  []v2.Signer{},
-				Selector: v2.NewDefaultPaymentSelector(),
-			}
-			c.Transport = transport
-		}
-
+		transport := getOrCreateTransport(c)
 		// Add the signer
 		transport.Signers = append(transport.Signers, signer)
 		return nil
 	}
 }

76-94: Same DRY issue - use getOrCreateTransport helper.

Apply the same refactor here for consistency.

♻️ Proposed refactor
 func WithSelector(selector v2.PaymentSelector) ClientOption {
 	return func(c *Client) error {
-		// Get or create the X402Transport
-		transport, ok := c.Transport.(*X402Transport)
-		if !ok {
-			// Wrap the existing transport
-			transport = &X402Transport{
-				Base:     c.Transport,
-				Signers:  []v2.Signer{},
-				Selector: selector,
-			}
-			c.Transport = transport
-		} else {
-			transport.Selector = selector
-		}
-
+		transport := getOrCreateTransport(c)
+		transport.Selector = selector
 		return nil
 	}
 }
v2/http/gin/middleware.go (2)

62-72: Consider making http.Client configurable or setting timeouts.

The http.Client{} is created without timeouts, which could cause requests to hang indefinitely if the facilitator is unresponsive. Consider using v2.DefaultTimeouts to configure the client's timeout or allowing the http.Client to be passed via config.

♻️ Suggested improvement
 	facilitator := &v2http.FacilitatorClient{
 		BaseURL:               config.FacilitatorURL,
-		Client:                &http.Client{},
+		Client:                &http.Client{Timeout: v2.DefaultTimeouts.VerifyTimeout + v2.DefaultTimeouts.SettleTimeout},
 		Timeouts:              v2.DefaultTimeouts,

Or better, allow the client to be configured via Config.


90-98: EnrichRequirements blocks middleware initialization.

This call happens during middleware creation and will block server startup if the facilitator is slow or unavailable. While the fallback behavior is good, consider whether this should be lazy-loaded on first request or performed asynchronously.

v2/mcp/server/server.go (1)

83-93: Consider using structured logging instead of fmt.Printf.

Other parts of this codebase use slog.Default() for logging (e.g., in middleware.go). Using fmt.Printf here is inconsistent. Consider using structured logging for consistency and better observability.

♻️ Suggested improvement
+import "log/slog"
+
 func (s *X402Server) Start(addr string) error {
 	handler := s.Handler()
 	if s.config.Verbose {
-		fmt.Printf("Starting x402 v2 MCP server on %s\n", addr)
-		fmt.Printf("Facilitator URL: %s\n", s.config.FacilitatorURL)
-		fmt.Printf("Verify-only mode: %v\n", s.config.VerifyOnly)
-		fmt.Printf("Protected tools: %d\n", len(s.config.PaymentTools))
+		slog.Info("starting x402 v2 MCP server",
+			"addr", addr,
+			"facilitatorURL", s.config.FacilitatorURL,
+			"verifyOnly", s.config.VerifyOnly,
+			"protectedTools", len(s.config.PaymentTools),
+		)
 	}
 	return http.ListenAndServe(addr, handler)
 }
v2/selector.go (1)

148-160: Consider case-insensitive matching for network identifiers.

The matching at line 151 is case-sensitive (req.Network == payment.Accepted.Network). While CAIP-2 identifiers should be normalized, defensive case-insensitive matching (like strings.EqualFold) would be more robust against inconsistent casing from different sources.

♻️ Suggested improvement
 func FindMatchingRequirement(payment *PaymentPayload, requirements []PaymentRequirements) (*PaymentRequirements, error) {
 	for i := range requirements {
 		req := &requirements[i]
-		if req.Network == payment.Accepted.Network && req.Scheme == payment.Accepted.Scheme {
+		if strings.EqualFold(req.Network, payment.Accepted.Network) && strings.EqualFold(req.Scheme, payment.Accepted.Scheme) {
 			return req, nil
 		}
 	}
v2/signers/svm/signer.go (1)

184-191: Decimals defaults to zero if token not found in configured tokens.

If the loop at lines 186-191 doesn't find a matching token (which shouldn't happen since CanSign passed), decimals remains at its zero value. While CanSign should prevent this path, defensive coding would ensure this edge case is handled explicitly.

Suggested improvement
 	// Get decimals for this token
 	var decimals uint8
+	var tokenFound bool
 	for _, token := range s.tokens {
 		if strings.EqualFold(token.Address, requirements.Asset) {
 			decimals = uint8(token.Decimals)
+			tokenFound = true
 			break
 		}
 	}
+	if !tokenFound {
+		return nil, fmt.Errorf("token %s not found in signer configuration", requirements.Asset)
+	}
v2/mcp/server/handler.go (3)

72-79: Consider returning an error instead of panicking.

Panicking at startup when the facilitator URL is missing can crash the application unexpectedly. Returning an error from NewX402Handler would allow callers to handle the configuration error gracefully.

Suggested approach
-func NewX402Handler(mcpHandler http.Handler, config *Config) *X402Handler {
+func NewX402Handler(mcpHandler http.Handler, config *Config) (*X402Handler, error) {
 	if config == nil {
 		config = DefaultConfig()
 	}

-	facilitator, fallbackFacilitator := initializeFacilitators(config)
+	facilitator, fallbackFacilitator, err := initializeFacilitators(config)
+	if err != nil {
+		return nil, err
+	}

-func initializeFacilitators(config *Config) (Facilitator, Facilitator) {
+func initializeFacilitators(config *Config) (Facilitator, Facilitator, error) {
 	var facilitator, fallbackFacilitator Facilitator

 	primaryURL := config.FacilitatorURL
 	if primaryURL == "" {
-		panic("x402: at least one facilitator URL must be provided")
+		return nil, nil, fmt.Errorf("x402: at least one facilitator URL must be provided")
 	}

159-172: Double JSON parsing can be consolidated.

The _meta field is parsed twice - once via struct unmarshaling and again via map[string]interface{}. This could be simplified by parsing directly into a map.


253-277: Marshal/unmarshal roundtrip for payment extraction.

The extractPayment method marshals and unmarshals the payment data from the already-parsed map. Consider using mapstructure or direct field mapping to avoid the roundtrip, though this approach is functionally correct.

v2/mcp/client/transport.go (1)

244-258: Payment failure callback fires for any non-402 error response.

The callback at lines 246-256 only fires when resp.Error.Code == 402. Other error codes (e.g., -32603 for internal errors, -32602 for invalid params) after payment injection won't trigger the failure callback. This may be intentional but could miss some failure scenarios.

Consider broadening failure callback scope
 	// Check if payment succeeded
 	if resp.Error != nil {
-		if resp.Error.Code == 402 && t.config.OnPaymentFailure != nil {
+		if t.config.OnPaymentFailure != nil {
 			t.config.OnPaymentFailure(v2.PaymentEvent{
 				Type:      v2.PaymentEventFailure,
 				Timestamp: time.Now(),
 				Method:    "MCP",
-				Error:     fmt.Errorf("payment rejected: %s", resp.Error.Message),
+				Error:     fmt.Errorf("request failed after payment (code %d): %s", resp.Error.Code, resp.Error.Message),
 				Network:   payment.Accepted.Network,
 				Scheme:    payment.Accepted.Scheme,
 				Duration:  duration,
 			})
 		}
 		return resp, nil
 	}
v2/signers/evm/signer.go (2)

166-168: Use v2.X402Version constant instead of hardcoded value.

The constant v2.X402Version (value 2) is defined in v2/types.go and should be used for consistency.

Suggested fix
 	payload := &v2.PaymentPayload{
-		X402Version: 2,
+		X402Version: v2.X402Version,
 		Accepted:    *requirements,

201-222: Consider extracting chain ID from CAIP-2 identifier programmatically.

The GetChainID function uses a switch statement with hardcoded network strings. Since EVM CAIP-2 identifiers follow the pattern eip155:<chainId>, the chain ID could be parsed directly from the string, reducing maintenance burden when adding new networks.

Suggested approach
func GetChainID(network string) (int64, error) {
    if !strings.HasPrefix(network, "eip155:") {
        return 0, v2.ErrInvalidNetwork
    }
    chainIDStr := strings.TrimPrefix(network, "eip155:")
    chainID, err := strconv.ParseInt(chainIDStr, 10, 64)
    if err != nil {
        return 0, fmt.Errorf("%w: invalid chain ID format", v2.ErrInvalidNetwork)
    }
    return chainID, nil
}

This would automatically support any EVM network without code changes.

v2/http/facilitator.go (2)

262-270: Supported endpoint reuses VerifyTimeout - consider dedicated timeout.

The /supported endpoint uses VerifyTimeout which may not be appropriate since it's a simple GET request that should complete quickly. A dedicated SupportedTimeout or a shorter default would be more fitting.


298-306: EnrichRequirements uses context.Background() - consider accepting context parameter.

Using context.Background() means this call cannot be cancelled or have its deadline controlled by the caller. Since this is called during middleware initialization (not per-request), it may be acceptable, but accepting a context parameter would provide more flexibility.

Suggested improvement
-func (c *FacilitatorClient) EnrichRequirements(requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
+func (c *FacilitatorClient) EnrichRequirements(ctx context.Context, requirements []v2.PaymentRequirements) ([]v2.PaymentRequirements, error) {
 	// Fetch supported payment types
-	supported, err := c.Supported(context.Background())
+	supported, err := c.Supported(ctx)
 	if err != nil {
 		return requirements, fmt.Errorf("failed to fetch supported payment types: %w", err)
 	}
v2/mcp/server/facilitator.go (1)

15-21: Interface uses pointer *v2.PaymentPayload while underlying client uses value.

The Facilitator interface accepts *v2.PaymentPayload, but the underlying v2http.FacilitatorClient.Verify/Settle methods accept v2.PaymentPayload by value (see lines 107, 117 where it's dereferenced). This inconsistency works but adds cognitive overhead. Consider aligning the interface signature with the underlying implementation.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d563bb and 2f65c82.

📒 Files selected for processing (49)
  • AGENTS.md
  • examples/v2/gin/main.go
  • examples/v2/http/main.go
  • examples/v2/mcp/main.go
  • v2/chains.go
  • v2/chains_test.go
  • v2/config.go
  • v2/encoding/encoding.go
  • v2/encoding/encoding_test.go
  • v2/errors.go
  • v2/events.go
  • v2/facilitator/interface.go
  • v2/http/client.go
  • v2/http/client_test.go
  • v2/http/facilitator.go
  • v2/http/facilitator_test.go
  • v2/http/gin/middleware.go
  • v2/http/gin/middleware_test.go
  • v2/http/internal/helpers/helpers.go
  • v2/http/internal/helpers/helpers_test.go
  • v2/http/middleware.go
  • v2/http/middleware_test.go
  • v2/http/transport.go
  • v2/http/transport_test.go
  • v2/internal/eip3009/eip3009.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/internal/solana/solana.go
  • v2/mcp/client/config.go
  • v2/mcp/client/transport.go
  • v2/mcp/client/transport_test.go
  • v2/mcp/errors.go
  • v2/mcp/server/config.go
  • v2/mcp/server/facilitator.go
  • v2/mcp/server/handler.go
  • v2/mcp/server/handler_test.go
  • v2/mcp/server/requirements.go
  • v2/mcp/server/server.go
  • v2/mcp/types.go
  • v2/selector.go
  • v2/selector_test.go
  • v2/signer.go
  • v2/signers/evm/signer.go
  • v2/signers/evm/signer_test.go
  • v2/signers/svm/signer.go
  • v2/signers/svm/signer_test.go
  • v2/types.go
  • v2/types_test.go
  • v2/validation/validation.go
  • v2/validation/validation_test.go
💤 Files with no reviewable changes (1)
  • AGENTS.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Group imports as: stdlib, external dependencies, then internal packages with blank lines between groups
Use camelCase for variables and functions, PascalCase for exported items, avoid abbreviations
Always check errors; wrap with context using fmt.Errorf("context: %w", err) for error handling
For exported functions, start comments with function name; use // for inline comments, /* */ for block comments
Prefer channels over mutexes for concurrency; always handle goroutine lifecycles properly

Files:

  • v2/http/middleware.go
  • v2/events.go
  • v2/signers/svm/signer_test.go
  • v2/mcp/server/requirements.go
  • examples/v2/http/main.go
  • v2/http/internal/helpers/helpers_test.go
  • examples/v2/gin/main.go
  • v2/mcp/types.go
  • v2/http/facilitator_test.go
  • v2/mcp/client/transport_test.go
  • v2/http/gin/middleware.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/selector.go
  • v2/mcp/client/config.go
  • v2/facilitator/interface.go
  • v2/http/gin/middleware_test.go
  • v2/encoding/encoding.go
  • v2/config.go
  • v2/http/middleware_test.go
  • examples/v2/mcp/main.go
  • v2/validation/validation.go
  • v2/http/transport.go
  • v2/mcp/errors.go
  • v2/http/client_test.go
  • v2/encoding/encoding_test.go
  • v2/http/client.go
  • v2/http/transport_test.go
  • v2/http/internal/helpers/helpers.go
  • v2/errors.go
  • v2/internal/eip3009/eip3009.go
  • v2/mcp/server/handler.go
  • v2/chains_test.go
  • v2/mcp/server/server.go
  • v2/signer.go
  • v2/selector_test.go
  • v2/types_test.go
  • v2/validation/validation_test.go
  • v2/mcp/server/handler_test.go
  • v2/signers/svm/signer.go
  • v2/chains.go
  • v2/signers/evm/signer_test.go
  • v2/internal/solana/solana.go
  • v2/mcp/server/config.go
  • v2/types.go
  • v2/mcp/client/transport.go
  • v2/mcp/server/facilitator.go
  • v2/signers/evm/signer.go
  • v2/http/facilitator.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Test files must end with _test.go; use table-driven tests; mock external dependencies

Files:

  • v2/signers/svm/signer_test.go
  • v2/http/internal/helpers/helpers_test.go
  • v2/http/facilitator_test.go
  • v2/mcp/client/transport_test.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/http/gin/middleware_test.go
  • v2/http/middleware_test.go
  • v2/http/client_test.go
  • v2/encoding/encoding_test.go
  • v2/http/transport_test.go
  • v2/chains_test.go
  • v2/selector_test.go
  • v2/types_test.go
  • v2/validation/validation_test.go
  • v2/mcp/server/handler_test.go
  • v2/signers/evm/signer_test.go
🧠 Learnings (1)
📚 Learning: 2025-12-04T10:19:11.236Z
Learnt from: CR
Repo: mark3labs/x402-go PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:19:11.236Z
Learning: Applies to **/*_test.go : Test files must end with `_test.go`; use table-driven tests; mock external dependencies

Applied to files:

  • v2/http/internal/helpers/helpers_test.go
  • v2/mcp/client/transport_test.go
  • v2/internal/eip3009/eip3009_test.go
  • v2/http/transport_test.go
  • v2/chains_test.go
  • v2/types_test.go
  • v2/mcp/server/handler_test.go
🧬 Code graph analysis (31)
v2/http/middleware.go (3)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/http/internal/helpers/helpers.go (4)
  • BuildResourceURL (93-99)
  • SendPaymentRequired (34-45)
  • ParsePaymentHeader (14-31)
  • AddPaymentResponseHeader (48-55)
v2/signers/svm/signer_test.go (4)
v2/signers/svm/signer.go (5)
  • Option (32-32)
  • WithPriority (107-112)
  • WithMaxAmount (99-104)
  • NewSigner (35-42)
  • NewSignerFromKeygenFile (78-96)
v2/chains.go (2)
  • NetworkSolanaMainnet (36-36)
  • NetworkSolanaDevnet (37-37)
v2/types.go (1)
  • X402Version (15-15)
v2/internal/solana/solana.go (1)
  • ComputeBudgetProgramID (15-15)
v2/mcp/server/requirements.go (3)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/validation/validation.go (1)
  • ValidatePaymentRequirements (111-167)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/mcp/types.go (1)
v2/types.go (3)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • Extension (55-61)
v2/http/gin/middleware.go (3)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/http/internal/helpers/helpers.go (2)
  • BuildResourceURL (93-99)
  • ParsePaymentHeader (14-31)
v2/types.go (3)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
v2/internal/eip3009/eip3009_test.go (1)
v2/internal/eip3009/eip3009.go (4)
  • GenerateNonce (46-52)
  • CreateAuthorization (26-44)
  • SignAuthorization (54-110)
  • Authorization (17-24)
v2/selector.go (3)
v2/signer.go (1)
  • Signer (8-32)
v2/types.go (2)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
v2/errors.go (8)
  • NewPaymentError (114-121)
  • ErrCodeNoValidSigner (67-67)
  • ErrNoValidSigner (8-8)
  • ErrCodeInvalidRequirements (73-73)
  • ErrInvalidRequirements (14-14)
  • ErrCodeSigningFailed (76-76)
  • ErrCodeUnsupportedScheme (82-82)
  • ErrUnsupportedScheme (59-59)
v2/facilitator/interface.go (1)
v2/types.go (6)
  • PaymentPayload (82-99)
  • PaymentRequirements (31-52)
  • VerifyResponse (140-149)
  • SettleResponse (152-167)
  • SupportedResponse (185-194)
  • X402Version (15-15)
v2/http/gin/middleware_test.go (2)
v2/http/gin/middleware.go (4)
  • Config (18-18)
  • NewX402Middleware (60-208)
  • GetPaymentFromContext (225-235)
  • PaymentContextKey (21-21)
v2/types.go (3)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
  • X402Version (15-15)
v2/http/middleware_test.go (2)
v2/types.go (4)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
  • SettleResponse (152-167)
v2/http/middleware.go (3)
  • Config (16-59)
  • NewX402Middleware (71-216)
  • GetPaymentFromContext (305-315)
examples/v2/mcp/main.go (3)
v2/chains.go (1)
  • GetChainConfig (186-192)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/signers/svm/signer.go (1)
  • NewSignerFromKeygenFile (78-96)
v2/validation/validation.go (2)
v2/chains.go (3)
  • ValidateNetwork (197-231)
  • NetworkTypeEVM (16-16)
  • NetworkTypeSVM (18-18)
v2/types.go (5)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • PaymentRequired (64-79)
v2/http/transport.go (1)
v2/http/internal/helpers/helpers.go (3)
  • ParsePaymentRequirements (58-70)
  • BuildPaymentHeader (88-90)
  • ParseSettlement (74-85)
v2/http/client_test.go (3)
v2/http/client.go (8)
  • NewClient (21-40)
  • Client (13-15)
  • WithSigner (55-73)
  • WithSelector (76-94)
  • WithPaymentCallback (97-116)
  • WithPaymentCallbacks (120-136)
  • WithHTTPClient (43-51)
  • GetSettlement (155-161)
v2/http/transport.go (1)
  • X402Transport (13-31)
v2/types.go (1)
  • SettleResponse (152-167)
v2/encoding/encoding_test.go (2)
v2/types.go (8)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • SettleResponse (152-167)
  • PaymentRequired (64-79)
  • Extension (55-61)
  • VerifyResponse (140-149)
v2/encoding/encoding.go (8)
  • EncodePayment (17-23)
  • DecodePayment (28-41)
  • EncodeSettlement (47-53)
  • DecodeSettlement (58-71)
  • EncodeRequirements (76-82)
  • DecodeRequirements (87-100)
  • EncodeVerifyResponse (105-111)
  • DecodeVerifyResponse (116-129)
v2/http/client.go (3)
v2/http/transport.go (1)
  • X402Transport (13-31)
v2/types.go (1)
  • SettleResponse (152-167)
v2/http/internal/helpers/helpers.go (1)
  • ParseSettlement (74-85)
v2/http/transport_test.go (2)
v2/types.go (4)
  • X402Version (15-15)
  • PaymentRequired (64-79)
  • ResourceInfo (18-27)
  • SettleResponse (152-167)
v2/http/transport.go (1)
  • X402Transport (13-31)
v2/http/internal/helpers/helpers.go (1)
v2/types.go (4)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
  • SettleResponse (152-167)
v2/chains_test.go (4)
v2/chains.go (16)
  • ChainConfig (41-56)
  • BaseMainnet (62-68)
  • PolygonMainnet (72-78)
  • AvalancheMainnet (82-88)
  • EthereumMainnet (92-98)
  • BaseSepolia (105-111)
  • PolygonAmoy (115-121)
  • AvalancheFuji (125-131)
  • Sepolia (135-141)
  • SolanaMainnet (148-154)
  • SolanaDevnet (158-164)
  • ValidateNetwork (197-231)
  • GetChainID (235-251)
  • GetSolanaGenesisHash (255-266)
  • GetChainConfig (186-192)
  • NewUSDCTokenConfig (270-278)
v2/validation/validation_test.go (1)
  • TestValidateNetwork (76-128)
v2/validation/validation.go (1)
  • ValidateNetwork (49-61)
v2/errors.go (1)
  • ErrInvalidNetwork (29-29)
v2/signer.go (1)
v2/types.go (3)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • TokenConfig (197-213)
v2/selector_test.go (4)
v2/types.go (6)
  • TokenConfig (197-213)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • EVMPayload (102-108)
  • EVMAuthorization (111-129)
v2/selector.go (1)
  • FindMatchingRequirement (148-160)
v2/signer.go (1)
  • Signer (8-32)
v2/errors.go (5)
  • ErrorCode (63-63)
  • ErrCodeNoValidSigner (67-67)
  • ErrCodeInvalidRequirements (73-73)
  • PaymentError (86-98)
  • ErrCodeSigningFailed (76-76)
v2/types_test.go (1)
v2/types.go (15)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • PaymentRequired (64-79)
  • PaymentPayload (82-99)
  • EVMPayload (102-108)
  • EVMAuthorization (111-129)
  • Extension (55-61)
  • VerifyResponse (140-149)
  • SettleResponse (152-167)
  • SupportedResponse (185-194)
  • SupportedKind (170-182)
  • AmountToBigInt (217-234)
  • BigIntToAmount (238-249)
  • TokenConfig (197-213)
v2/validation/validation_test.go (2)
v2/validation/validation.go (7)
  • ValidateAmount (28-45)
  • ValidateNetwork (49-61)
  • ValidateAddress (66-92)
  • ValidateResourceInfo (96-107)
  • ValidatePaymentRequirements (111-167)
  • ValidatePaymentPayload (171-201)
  • ValidatePaymentRequired (204-224)
v2/types.go (5)
  • ResourceInfo (18-27)
  • PaymentRequirements (31-52)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • PaymentRequired (64-79)
v2/signers/svm/signer.go (2)
v2/internal/solana/solana.go (7)
  • GetRPCURL (90-99)
  • DeriveAssociatedTokenAddress (81-87)
  • BuildSetComputeUnitLimitInstruction (43-56)
  • DefaultComputeUnits (18-18)
  • BuildSetComputeUnitPriceInstruction (61-78)
  • DefaultComputeUnitPrice (21-21)
  • BuildTransferCheckedInstruction (24-38)
v2/types.go (1)
  • X402Version (15-15)
v2/chains.go (3)
v2/errors.go (1)
  • ErrInvalidNetwork (29-29)
v2/validation/validation.go (1)
  • ValidateNetwork (49-61)
v2/types.go (1)
  • TokenConfig (197-213)
v2/signers/evm/signer_test.go (3)
v2/signers/evm/signer.go (3)
  • NewSigner (28-57)
  • WithMaxAmount (91-96)
  • GetChainID (201-222)
v2/types.go (1)
  • X402Version (15-15)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/internal/solana/solana.go (1)
v2/chains.go (2)
  • NetworkSolanaMainnet (36-36)
  • NetworkSolanaDevnet (37-37)
v2/mcp/server/config.go (3)
v2/types.go (1)
  • ResourceInfo (18-27)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/mcp/server/handler.go (4)
  • AuthorizationProvider (50-50)
  • OnBeforeFunc (53-53)
  • OnAfterVerifyFunc (56-56)
  • OnAfterSettleFunc (59-59)
v2/types.go (2)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/errors.go (1)
  • ErrInvalidAmount (23-23)
v2/mcp/client/transport.go (4)
v2/mcp/client/config.go (3)
  • Config (11-35)
  • Option (38-38)
  • DefaultConfig (99-106)
v2/mcp/errors.go (1)
  • WrapX402Error (74-82)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/types.go (2)
  • ResourceInfo (18-27)
  • X402Version (15-15)
v2/signers/evm/signer.go (2)
v2/internal/eip3009/eip3009.go (3)
  • CreateAuthorization (26-44)
  • SignAuthorization (54-110)
  • Authorization (17-24)
v2/types.go (1)
  • X402Version (15-15)
🪛 Gitleaks (8.30.0)
v2/signers/svm/signer_test.go

[high] 18-18: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

Return ErrNoPaymentRequirements when resp.Error.Data is nil or empty
instead of attempting to unmarshal and causing 'unexpected end of JSON
input' errors. This provides clearer error messages for clients when
payment requirements are missing from 402 responses.
- Fix case-insensitive base58 address comparison (Solana base58 is case-sensitive)
- Add uint64 overflow check before converting big.Int amounts
- Add nil requirements validation in CanSign
- Add negative/zero amount validation
- Add token decimals range validation (0-255)
- Add explicit token not found error when asset doesn't match
- Update test to expect case-sensitive behavior

This fixes a critical security issue where incorrect token addresses could
match due to case-insensitive comparison, and prevents silent truncation
of large amounts during uint64 conversion.
Fix error wrapping in GetRPCURL to place sentinel error at end with %w
verb, allowing errors.Is/As to properly unwrap v2.ErrInvalidNetwork.
Previous format broke error chain unwrapping.
Add idempotent associated token account creation instruction before
SPL token transfers to ensure the destination ATA exists. The facilitator
sponsors the rent-exempt balance as fee payer.

Without this instruction, transfers fail when the recipient's ATA does
not exist, which is a functional requirement of the exact_svm specification.

Fixes critical issue from code review: v2/signers/svm/signer.go lines 283-314
Fixes issue where Hijack() could bypass payment settlement for WebSocket
upgrades and other connection takeover scenarios. Now settles payment first
before allowing hijack, preventing potential payment bypass.
The GetPaymentConfig function was returning a pointer to a local copy
of the map value, which is misleading for callers who might expect to
modify the stored configuration. Changed the signature to return
(ToolPaymentConfig, bool) for clearer read-only semantics that match
Go idioms (similar to map lookups).
Replace panic() with error returns in NewX402Handler and initializeFacilitators.
Panicking in library code crashes consuming applications; returning errors
allows callers to handle configuration failures gracefully.

Changes:
- NewX402Handler now returns (*X402Handler, error) instead of *X402Handler
- initializeFacilitators now returns (Facilitator, Facilitator, error)
- Handler() methods in X402Server now return (http.Handler, error)
- Updated Start() methods to handle handler creation errors
- All tests pass with race detection

Fixes issue from scratch/comments.md (v2/mcp/server/handler.go:72-79)
Reject payment payloads with unsupported x402Version to prevent
processing incompatible payments. This ensures verify/settle logic
only operates on version 2 payloads as expected by the protocol.
- Change EnrichRequirements signature to accept context.Context as first parameter
- Allows callers to control timeouts and cancellation for facilitator requests
- Update all call sites to use context.WithTimeout(context.Background(), RequestTimeout)
- Add context import to http/pocketbase/middleware.go
- Update both v1 and v2 implementations for consistency
- All tests pass with race detection
- Set HTTP client timeout to DefaultTimeouts.RequestTimeout in all middleware
- Prevents indefinite hangs when facilitator is unresponsive
- Affects v1 and v2 standard middleware, Gin middleware, and PocketBase middleware
- All 6 facilitator client instantiations now have explicit timeout
- Tests pass with race detection

Resolves issue from scratch/comments.md: HTTP clients without timeout configuration
The GetLatestBlockhash call was using context.Background() which could hang indefinitely if the RPC node is unresponsive. Now uses a timeout context with DefaultTimeouts.VerifyTimeout to prevent hangs.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
http/middleware.go (1)

294-300: Hijack() may bypass settlement.

If the handler calls Hijack() before Write() or WriteHeader(), the committed flag remains false and settlement never occurs. This could allow bypassing payment for WebSocket upgrades or similar connection hijacking scenarios.

Consider triggering settlement before allowing the hijack, similar to how Write triggers WriteHeader:

🔧 Proposed fix
 func (i *settlementInterceptor) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	// Ensure settlement occurs before connection hijack
+	if !i.committed {
+		i.WriteHeader(http.StatusOK)
+	}
+	if i.hijacked {
+		return nil, nil, errors.New("settlement failed, cannot hijack")
+	}
 	if hijacker, ok := i.w.(http.Hijacker); ok {
 		return hijacker.Hijack()
 	}
 	return nil, nil, errors.New("hijacking not supported")
 }
🤖 Fix all issues with AI agents
In `@ralph.nu`:
- Around line 1-19: The file ralph.nu is a local development automation script
(defines prompt and a for loop using variables like prompt and the loop "for i
in 1..10") and should not be committed; remove ralph.nu from the PR or add its
path to .gitignore and delete it from the commit (or move it to a non-tracked
location), then amend the commit so the script is no longer in the repo; if you
prefer to keep it but not track changes, add its filename to .gitignore and run
git rm --cached on ralph.nu before committing.

In `@v2/signers/svm/signer.go`:
- Around line 336-342: The ATA creation call currently uses
associatedtokenaccount.NewCreateInstruction(feePayer, recipient, mint).Build()
but that is not idempotent; replace it with
associatedtokenaccount.NewCreateIdempotentInstruction(feePayer, recipient,
mint).Build() so the transaction succeeds if the ATA already exists, and update
the inline comment to reflect true idempotency; ensure any references to
NewCreateInstruction in the same file (e.g., the instruction assembly around
signer.go's ATA creation) are updated accordingly.
🧹 Nitpick comments (15)
v2/internal/solana/solana.go (1)

40-56: Consider using encoding/binary for little-endian encoding.

The manual byte manipulation is correct, but using binary.LittleEndian.PutUint32 could improve readability and reduce the chance of encoding errors in future modifications.

♻️ Optional refactor
+import "encoding/binary"
+
 func BuildSetComputeUnitLimitInstruction(units uint32) solana.Instruction {
 	data := make([]byte, 5)
 	data[0] = 2 // SetComputeUnitLimit discriminator
-	data[1] = byte(units)
-	data[2] = byte(units >> 8)
-	data[3] = byte(units >> 16)
-	data[4] = byte(units >> 24)
+	binary.LittleEndian.PutUint32(data[1:], units)

 	return solana.NewInstruction(
 		ComputeBudgetProgramID,
 		solana.AccountMetaSlice{},
 		data,
 	)
 }
http/pocketbase/middleware.go (1)

202-252: Consider using shared helpers to reduce code duplication.

The parsePaymentHeaderFromRequest and findMatchingRequirementPocketBase functions duplicate logic that exists in the helpers package. The Gin middleware (line 205 in http/gin/middleware.go) uses helpers.ParsePaymentHeaderFromRequest. Consider refactoring to use the shared helpers for consistency and maintainability.

♻️ Suggested refactor
+import (
+	"github.com/mark3labs/x402-go/http/internal/helpers"
+)

-// parsePaymentHeaderFromRequest parses the X-PAYMENT header from an http.Request.
-// It decodes the base64-encoded JSON, unmarshals it, and validates the protocol version.
-func parsePaymentHeaderFromRequest(r *http.Request) (x402.PaymentPayload, error) {
-	// ... implementation ...
-}
+// parsePaymentHeaderFromRequest parses the X-PAYMENT header from an http.Request.
+func parsePaymentHeaderFromRequest(r *http.Request) (x402.PaymentPayload, error) {
+	return helpers.ParsePaymentHeaderFromRequest(r)
+}

-// findMatchingRequirementPocketBase finds a payment requirement matching the payment's scheme and network.
-func findMatchingRequirementPocketBase(payment x402.PaymentPayload, requirements []x402.PaymentRequirement) (x402.PaymentRequirement, error) {
-	// ... implementation ...
-}
+// findMatchingRequirementPocketBase finds a payment requirement that matches the provided payment.
+func findMatchingRequirementPocketBase(payment x402.PaymentPayload, requirements []x402.PaymentRequirement) (x402.PaymentRequirement, error) {
+	return helpers.FindMatchingRequirement(payment, requirements)
+}
mcp/server/handler.go (1)

406-413: Clarify the semantics of Success=false in verify-only mode.

The comment states that Success=false indicates settlement was skipped, but this could be confusing to API consumers who might interpret it as a failure. Consider using a separate field (e.g., Skipped: true) or documenting this behavior more explicitly in the response structure.

v2/signers/svm/signer_test.go (3)

95-112: Consider using errors.Is() for sentinel error assertions.

For test cases with errTarget, use errors.Is() to properly handle wrapped errors, which aligns with the project's error wrapping practices using fmt.Errorf("context: %w", err).

Proposed fix
 		t.Run(tt.name, func(t *testing.T) {
 			signer, err := NewSigner(tt.network, tt.key, tt.tokens, tt.opts...)
 			if tt.wantErr {
 				if err == nil {
 					t.Fatal("expected error, got nil")
 				}
+				if tt.errTarget != nil && !errors.Is(err, tt.errTarget) {
+					t.Errorf("expected error %v, got %v", tt.errTarget, err)
+				}
 				return
 			}

728-740: Use strings.Contains from the standard library.

The helper functions containsString and indexOfSubstring duplicate functionality available in strings.Contains.

Proposed fix
+import "strings"
+
 // Helper function to check if error message contains expected string
-func containsString(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(substr) == 0 || indexOfSubstring(s, substr) >= 0)
-}
-
-func indexOfSubstring(s, substr string) int {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return i
-		}
-	}
-	return -1
-}
+func containsString(s, substr string) bool {
+	return strings.Contains(s, substr)
+}

358-366: Inconsistent error assertion logic.

The test uses both err != tt.wantErr (direct comparison) and containsString as a fallback. For sentinel errors, use errors.Is() for proper comparison of wrapped errors.

Proposed fix
 			if tt.wantErr != nil {
 				if err == nil {
 					t.Fatalf("expected error %v, got nil", tt.wantErr)
 				}
-				if err != tt.wantErr && !containsString(err.Error(), tt.wantErr.Error()) {
+				if !errors.Is(err, tt.wantErr) {
 					t.Fatalf("expected error %v, got %v", tt.wantErr, err)
 				}
 				return
 			}
v2/mcp/client/config.go (1)

101-109: Consider documenting the shared http.DefaultClient usage.

Using http.DefaultClient is convenient but callers modifying its transport will affect all users. Consider adding a comment noting this, or creating a new client instance with default settings.

Alternative: create a new client instance
 func DefaultConfig(serverURL string) *Config {
 	return &Config{
 		ServerURL:  serverURL,
-		HTTPClient: http.DefaultClient,
+		HTTPClient: &http.Client{},
 		Selector:   v2.NewDefaultPaymentSelector(),
 		Signers:    make([]v2.Signer, 0),
 	}
 }
v2/mcp/client/transport.go (3)

34-37: Redundant selector initialization.

DefaultConfig at line 106 in config.go already initializes Selector to v2.NewDefaultPaymentSelector(). This check is unnecessary unless opts can set it to nil.

Consider removing redundant check
-	// Use default selector if none provided
-	if config.Selector == nil {
-		config.Selector = v2.NewDefaultPaymentSelector()
-	}

220-226: Shallow copy may cause unintended side effects.

Line 222-223 creates a shallow copy of req, but params is a map that was potentially extracted from req.Params. Modifications to params (line 218-219) could affect the original request if the caller retains a reference.

Consider deep copying the params map
 	// Create modified request
-	modifiedReq := req
-	modifiedReq.Params = params
+	modifiedReq := transport.JSONRPCRequest{
+		JSONRPC: req.JSONRPC,
+		Method:  req.Method,
+		ID:      req.ID,
+		Params:  params,
+	}

 	return modifiedReq, nil

248-262: Non-402 errors after retry should also trigger failure callback.

Currently, only 402 errors trigger OnPaymentFailure after retry. Other error codes (e.g., 500, 400) indicate the payment attempt failed but don't invoke the callback, which could leave observability gaps.

Trigger failure callback for all error responses
 	// Check if payment succeeded
 	if resp.Error != nil {
-		if resp.Error.Code == 402 && t.config.OnPaymentFailure != nil {
+		if t.config.OnPaymentFailure != nil {
 			t.config.OnPaymentFailure(v2.PaymentEvent{
 				Type:      v2.PaymentEventFailure,
 				Timestamp: time.Now(),
 				Method:    "MCP",
-				Error:     fmt.Errorf("payment rejected: %s", resp.Error.Message),
+				Error:     fmt.Errorf("request failed (code %d): %s", resp.Error.Code, resp.Error.Message),
 				Network:   payment.Accepted.Network,
 				Scheme:    payment.Accepted.Scheme,
 				Duration:  duration,
 			})
 		}
 		return resp, nil
 	}
v2/mcp/server/handler_test.go (1)

52-95: Consider adding assertion for response body content.

The test verifies status code but doesn't validate the actual response body matches mcpResponse. This could miss serialization issues.

🔧 Suggested enhancement
 	if resp.StatusCode != http.StatusOK {
 		t.Errorf("Expected status 200, got %d", resp.StatusCode)
 	}
+
+	var jsonrpcResp map[string]interface{}
+	if err := json.NewDecoder(resp.Body).Decode(&jsonrpcResp); err != nil {
+		t.Fatalf("Failed to decode response: %v", err)
+	}
+	if _, hasResult := jsonrpcResp["result"]; !hasResult {
+		t.Error("Expected result in response")
+	}
v2/http/facilitator_test.go (1)

93-96: Consider setting Timeouts field for consistency.

This test creates a FacilitatorClient without the Timeouts field, which may rely on zero-value behavior. For consistency with other tests, consider setting Timeouts: v2.DefaultTimeouts.

v2/mcp/server/server.go (1)

90-96: Consider using structured logger instead of fmt.Printf for verbose output.

The verbose startup messages use fmt.Printf directly, while the rest of the codebase uses slog.Logger. For consistency and to enable structured logging in production, consider using s.config.Logger (if available) instead.

♻️ Suggested refactor
 	if s.config.Verbose {
-		fmt.Printf("Starting x402 v2 MCP server on %s\n", addr)
-		fmt.Printf("Facilitator URL: %s\n", s.config.FacilitatorURL)
-		fmt.Printf("Verify-only mode: %v\n", s.config.VerifyOnly)
-		fmt.Printf("Protected tools: %d\n", len(s.config.PaymentTools))
+		s.config.Logger.Info("Starting x402 v2 MCP server",
+			"address", addr,
+			"facilitatorURL", s.config.FacilitatorURL,
+			"verifyOnly", s.config.VerifyOnly,
+			"protectedTools", len(s.config.PaymentTools))
 	}
v2/mcp/server/handler.go (1)

122-128: Consider limiting body read size to prevent memory exhaustion.

The io.ReadAll(r.Body) at line 123 reads the entire request body without a size limit. A malicious client could send an extremely large payload to exhaust server memory.

♻️ Suggested fix using io.LimitReader
+const maxBodySize = 10 * 1024 * 1024 // 10MB limit
+
 // Read request body
-bodyBytes, err := io.ReadAll(r.Body)
+bodyBytes, err := io.ReadAll(io.LimitReader(r.Body, maxBodySize))
 if err != nil {
 	h.writeError(w, nil, -32700, "Parse error", nil)
 	return
 }
v2/http/facilitator.go (1)

262-296: Consider adding retry capability to Supported for consistency.

The Supported method doesn't use the retry mechanism that Verify and Settle use. While this may be intentional (since it's typically called at startup), transient network issues during server initialization could cause unnecessary failures.

Is the absence of retry on Supported intentional? If it's called during middleware initialization (like in the Gin middleware), a transient failure would prevent the server from starting.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f65c82 and eb2f030.

📒 Files selected for processing (22)
  • http/facilitator.go
  • http/gin/middleware.go
  • http/middleware.go
  • http/pocketbase/middleware.go
  • mcp/server/handler.go
  • mcp/server/server.go
  • ralph.nu
  • v2/config.go
  • v2/http/client_test.go
  • v2/http/facilitator.go
  • v2/http/facilitator_test.go
  • v2/http/gin/middleware.go
  • v2/http/middleware.go
  • v2/internal/solana/solana.go
  • v2/mcp/client/config.go
  • v2/mcp/client/transport.go
  • v2/mcp/server/config.go
  • v2/mcp/server/handler.go
  • v2/mcp/server/handler_test.go
  • v2/mcp/server/server.go
  • v2/signers/svm/signer.go
  • v2/signers/svm/signer_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • v2/mcp/server/config.go
  • v2/http/middleware.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Group imports as: stdlib, external dependencies, then internal packages with blank lines between groups
Use camelCase for variables and functions, PascalCase for exported items, avoid abbreviations
Always check errors; wrap with context using fmt.Errorf("context: %w", err) for error handling
For exported functions, start comments with function name; use // for inline comments, /* */ for block comments
Prefer channels over mutexes for concurrency; always handle goroutine lifecycles properly

Files:

  • http/pocketbase/middleware.go
  • v2/http/gin/middleware.go
  • http/gin/middleware.go
  • mcp/server/handler.go
  • http/facilitator.go
  • v2/mcp/server/server.go
  • v2/signers/svm/signer.go
  • v2/internal/solana/solana.go
  • v2/mcp/server/handler.go
  • v2/signers/svm/signer_test.go
  • v2/http/facilitator_test.go
  • v2/mcp/client/transport.go
  • v2/mcp/server/handler_test.go
  • v2/mcp/client/config.go
  • mcp/server/server.go
  • v2/http/client_test.go
  • http/middleware.go
  • v2/http/facilitator.go
  • v2/config.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Test files must end with _test.go; use table-driven tests; mock external dependencies

Files:

  • v2/signers/svm/signer_test.go
  • v2/http/facilitator_test.go
  • v2/mcp/server/handler_test.go
  • v2/http/client_test.go
🧠 Learnings (2)
📚 Learning: 2025-12-04T10:19:11.236Z
Learnt from: CR
Repo: mark3labs/x402-go PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:19:11.236Z
Learning: Applies to **/*.go : Always check errors; wrap with context using `fmt.Errorf("context: %w", err)` for error handling

Applied to files:

  • v2/internal/solana/solana.go
📚 Learning: 2025-12-04T10:19:11.236Z
Learnt from: CR
Repo: mark3labs/x402-go PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:19:11.236Z
Learning: Applies to **/*_test.go : Test files must end with `_test.go`; use table-driven tests; mock external dependencies

Applied to files:

  • v2/mcp/server/handler_test.go
🧬 Code graph analysis (14)
v2/http/gin/middleware.go (3)
http/middleware.go (3)
  • Config (16-56)
  • NewX402Middleware (68-223)
  • PaymentContextKey (62-62)
v2/http/middleware.go (3)
  • Config (16-59)
  • NewX402Middleware (71-218)
  • PaymentContextKey (65-65)
v2/types.go (3)
  • X402Version (15-15)
  • ResourceInfo (18-27)
  • PaymentRequired (64-79)
http/gin/middleware.go (6)
http/client.go (1)
  • Client (12-14)
config.go (1)
  • DefaultTimeouts (21-25)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
http/facilitator.go (1)
  • AuthorizationProvider (26-26)
v2/http/facilitator.go (1)
  • AuthorizationProvider (26-26)
mcp/types.go (1)
  • PaymentRequirements (9-13)
mcp/server/handler.go (2)
v2/mcp/server/handler.go (2)
  • NewX402Handler (24-40)
  • X402Handler (16-21)
mcp/server/facilitator.go (1)
  • Facilitator (18-24)
http/facilitator.go (1)
types.go (1)
  • PaymentRequirement (60-93)
v2/mcp/server/server.go (6)
v2/mcp/server/config.go (3)
  • Config (21-64)
  • DefaultConfig (67-75)
  • ToolPaymentConfig (12-18)
mcp/server/server.go (2)
  • NewX402Server (19-36)
  • X402Server (13-16)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/mcp/server/requirements.go (2)
  • ValidateRequirement (13-25)
  • SetToolResource (29-33)
v2/mcp/server/handler.go (1)
  • NewX402Handler (24-40)
mcp/server/handler.go (1)
  • NewX402Handler (26-42)
v2/signers/svm/signer.go (2)
v2/internal/solana/solana.go (7)
  • GetRPCURL (90-99)
  • DeriveAssociatedTokenAddress (81-87)
  • BuildSetComputeUnitLimitInstruction (43-56)
  • DefaultComputeUnits (18-18)
  • BuildSetComputeUnitPriceInstruction (61-78)
  • DefaultComputeUnitPrice (21-21)
  • BuildTransferCheckedInstruction (24-38)
v2/types.go (1)
  • X402Version (15-15)
v2/internal/solana/solana.go (1)
v2/chains.go (2)
  • NetworkSolanaMainnet (36-36)
  • NetworkSolanaDevnet (37-37)
v2/http/facilitator_test.go (6)
v2/types.go (7)
  • VerifyResponse (140-149)
  • PaymentPayload (82-99)
  • X402Version (15-15)
  • PaymentRequirements (31-52)
  • SettleResponse (152-167)
  • SupportedResponse (185-194)
  • SupportedKind (170-182)
http/facilitator.go (1)
  • AuthorizationProvider (26-26)
v2/http/facilitator.go (1)
  • AuthorizationProvider (26-26)
v2/config.go (2)
  • DefaultTimeouts (21-25)
  • TimeoutConfig (9-18)
v2/internal/eip3009/eip3009.go (1)
  • Authorization (17-24)
v2/errors.go (3)
  • ErrVerificationFailed (47-47)
  • ErrSettlementFailed (50-50)
  • ErrFacilitatorUnavailable (44-44)
v2/mcp/client/transport.go (5)
v2/mcp/client/config.go (3)
  • Config (11-35)
  • Option (38-38)
  • DefaultConfig (102-109)
v2/mcp/server/config.go (2)
  • Config (21-64)
  • DefaultConfig (67-75)
v2/mcp/errors.go (2)
  • ErrNoPaymentRequirements (27-27)
  • WrapX402Error (74-82)
v2/mcp/types.go (1)
  • PaymentRequirements (15-30)
v2/types.go (2)
  • ResourceInfo (18-27)
  • X402Version (15-15)
v2/mcp/client/config.go (1)
v2/mcp/server/config.go (2)
  • Config (21-64)
  • DefaultConfig (67-75)
mcp/server/server.go (1)
v2/mcp/server/server.go (1)
  • X402Server (13-16)
v2/http/client_test.go (3)
v2/http/client.go (8)
  • NewClient (21-40)
  • Client (13-15)
  • WithSigner (55-73)
  • WithSelector (76-94)
  • WithPaymentCallback (97-116)
  • WithPaymentCallbacks (120-136)
  • WithHTTPClient (43-51)
  • GetSettlement (155-161)
v2/http/transport.go (1)
  • X402Transport (13-31)
v2/types.go (3)
  • PaymentRequired (64-79)
  • X402Version (15-15)
  • SettleResponse (152-167)
http/middleware.go (3)
http/client.go (1)
  • Client (12-14)
config.go (1)
  • DefaultTimeouts (21-25)
mcp/types.go (1)
  • PaymentRequirements (9-13)
v2/http/facilitator.go (7)
v2/types.go (6)
  • PaymentPayload (82-99)
  • PaymentRequirements (31-52)
  • VerifyResponse (140-149)
  • SettleResponse (152-167)
  • X402Version (15-15)
  • SupportedResponse (185-194)
v2/config.go (1)
  • TimeoutConfig (9-18)
v2/facilitator/interface.go (1)
  • Interface (16-28)
http/middleware.go (1)
  • Config (16-56)
v2/http/middleware.go (1)
  • Config (16-59)
retry/retry.go (1)
  • WithRetry (32-81)
v2/errors.go (3)
  • ErrFacilitatorUnavailable (44-44)
  • ErrVerificationFailed (47-47)
  • ErrSettlementFailed (50-50)
🪛 Gitleaks (8.30.0)
v2/signers/svm/signer_test.go

[high] 18-18: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (67)
v2/internal/solana/solana.go (6)

1-12: LGTM!

Package declaration and imports are well-organized. Import grouping follows the coding guidelines with stdlib, external dependencies, and internal packages properly separated by blank lines.


14-21: LGTM!

Constants are properly typed and documented. Using MustPublicKeyFromBase58 for the well-known Compute Budget program ID is appropriate since the value is static and validated at compile/init time.


23-38: LGTM!

Clean implementation using the SPL Token library's builder pattern. The function signature is clear and the builder chain is readable.


58-78: LGTM!

Correct implementation of the SetComputeUnitPrice instruction. The same optional refactor using binary.LittleEndian.PutUint64 could apply here for consistency with the suggestion above.


80-87: LGTM!

Proper error wrapping with context following the coding guidelines. Discarding the bump seed is appropriate here since it's only needed when creating the ATA, not when deriving its address.


89-99: LGTM!

Error wrapping format is now correct with context preceding the wrapped error. The network constants properly reference v2.NetworkSolanaMainnet and v2.NetworkSolanaDevnet from v2/chains.go, ensuring consistency with the CAIP-2 identifiers defined there.

http/pocketbase/middleware.go (2)

56-87: LGTM!

The timeout-bound enrichment and facilitator client configuration look correct. The context timeout is appropriately scoped to the initialization phase, and the fallback to original requirements on enrichment failure provides good resilience.


90-199: LGTM!

The middleware handler correctly uses request-scoped context (e.Request.Context()) for verification and settlement operations, implements proper fallback to the secondary facilitator, and stores payment info in the request store for downstream handler access.

http/facilitator.go (4)

323-361: LGTM!

The EnrichRequirements method correctly propagates the context to the underlying Supported call, enabling timeout-aware enrichment. The logic properly prioritizes user-specified Extra values over facilitator-provided values.


92-191: LGTM!

The Verify method correctly handles context propagation, applies timeout only when the parent context lacks a deadline, and properly wraps errors for unwrapping. The retry logic with exponential backoff is well-structured.


194-227: LGTM!

The Supported method correctly uses the provided context for timeout control. Using VerifyTimeout is reasonable for this fast lookup endpoint.


230-321: LGTM!

The Settle method correctly uses SettleTimeout (60 seconds) which is appropriate for blockchain settlement operations. The retry logic and callback handling mirror the Verify method for consistency.

http/gin/middleware.go (2)

50-82: LGTM!

The facilitator client initialization and timeout-bound enrichment follow the same correct pattern as the PocketBase middleware. The context is properly scoped to the initialization phase.


85-200: LGTM!

The middleware handler correctly uses request-scoped context, leverages shared helpers from the internal/helpers package, and provides dual storage in both Gin and stdlib contexts for maximum compatibility.

v2/http/client_test.go (5)

99-155: LGTM!

The callback tests effectively verify that the payment callbacks are properly wired through the transport by using closure flags to confirm invocation.


157-170: LGTM!

The timeout is now correctly specified as 30 * time.Second instead of the previous incorrect value of 30 (nanoseconds). Good fix.


181-249: LGTM!

Excellent end-to-end test that covers the complete automatic payment flow: initial 402 response, payment signing via mockSigner, retry with payment header, and settlement extraction. The test properly validates the X402Transport's handling of 402 responses.


251-284: LGTM!

The settlement helper tests correctly verify both the nil case for missing headers and the parsing of valid encoded settlements. The mixed-case header name (X-Payment-Response) works correctly since Go's http.Header.Get is case-insensitive.


29-49: No action required. The mockSigner type is properly defined in v2/http/transport_test.go and is correctly accessible from v2/http/client_test.go since all *_test.go files in a package are compiled together during testing. Moving it to a separate helper file is unnecessary.

mcp/server/handler.go (2)

26-42: LGTM! Proper error propagation for handler initialization.

The change from panicking to returning errors is a good improvement for production robustness. The error handling flow is clean and allows callers to handle initialization failures gracefully.


65-115: LGTM! Clean error propagation in facilitator initialization.

The refactored initializeFacilitators properly validates configuration and returns contextual errors. The error message at line 88 clearly indicates the missing requirement.

v2/signers/svm/signer_test.go (1)

16-18: Test key is appropriately documented - Gitleaks alert is a false positive.

The static analysis flagged this as a potential API key leak, but this is a randomly generated test key clearly documented as not for production use. The comment is sufficient to clarify intent.

v2/mcp/client/config.go (1)

40-48: Nil signer guard properly implemented.

The fix correctly prevents nil signers from being appended, avoiding potential nil-interface panics during signing operations.

v2/signers/svm/signer.go (4)

132-156: LGTM! Case-sensitive base58 address matching implemented correctly.

The direct string comparison token.Address == requirements.Asset properly handles Solana's case-sensitive base58 encoding, and the nil check for requirements prevents panics.


165-185: LGTM! Comprehensive amount validation.

The code properly validates:

  • Invalid amount format (line 167-169)
  • Non-positive amounts (lines 172-174)
  • Max amount limits (lines 177-179)
  • uint64 overflow (lines 182-185)

This prevents silent truncation and invalid payment amounts.


232-238: LGTM! Timeout added for RPC blockhash fetch.

The context.WithTimeout prevents indefinite hangs when fetching the latest blockhash from Solana RPC.


199-214: LGTM! Token decimals validation with explicit not-found error.

The code validates decimal range (0-255) and returns ErrInvalidToken if the token is not found, providing clear error semantics.

v2/mcp/client/transport.go (2)

58-74: LGTM! Nil data check properly handles missing 402 payload.

The guard at line 62-64 returns ErrNoPaymentRequirements when resp.Error.Data is nil, preventing the "unexpected end of JSON input" error from the previous review.


115-134: LGTM! Robust payment requirements extraction.

The function properly validates:

  • Empty data (line 117-119)
  • x402 version compatibility (line 125-127)
  • Presence of accepts array (line 129-131)
mcp/server/server.go (3)

68-76: LGTM on Handler() error propagation.

The updated signature correctly returns errors from NewX402Handler, enabling callers to handle configuration or initialization failures gracefully.


79-90: LGTM on Start() error handling.

Error is properly checked before proceeding with verbose output and server startup. The error wrapping with fmt.Errorf("failed to create handler: %w", err) follows the coding guidelines for contextual error wrapping.


44-66: LGTM on AddPayableTool implementation.

The function properly validates requirements, sets the resource URL for each requirement, and stores payment configuration before registering the tool with the underlying MCP server.

http/middleware.go (2)

72-73: LGTM on timeout configuration for HTTP clients.

Both primary and fallback facilitator clients are correctly configured with x402.DefaultTimeouts.RequestTimeout for overall request timeouts and the Timeouts field for operation-specific timeouts.

Also applies to: 87-88


99-101: LGTM on context-aware enrichment.

The timeout-bounded context for EnrichRequirements ensures the enrichment operation doesn't block indefinitely during middleware initialization.

v2/mcp/server/handler_test.go (3)

14-50: LGTM on mock implementations.

The mockFacilitator properly tracks method invocations and allows configurable responses/errors. The mockMCPHandler provides a simple way to simulate MCP server responses with configurable status codes.


634-686: Good use of table-driven tests.

The TestValidateRequirement function follows Go testing best practices with clear test case names and well-structured table entries. This aligns with the coding guidelines for test files.


521-593: Verify settlement is not called on tool execution errors.

Good test coverage for ensuring settlement doesn't occur when the tool returns an error. This is critical for preventing charges when the requested operation fails.

v2/config.go (3)

45-61: LGTM on Validate() implementation.

The validation now includes RequestTimeout check (addressing the past review comment). All three timeouts are validated for positive values, and the relative constraint between SettleTimeout and VerifyTimeout is enforced.


20-25: LGTM on default timeout values.

The defaults are reasonable: 5s for verification (quick check), 60s for settlement (blockchain finality), and 120s for overall request timeout (allows for retries and network delays).


27-43: LGTM on fluent configuration methods.

The With* methods follow the builder pattern idiomatically by returning a copy of the struct with the updated field, enabling chained configuration.

v2/http/facilitator_test.go (5)

16-77: LGTM on basic Verify test.

Comprehensive test covering path, method, content-type validation, and response parsing. Properly verifies both IsValid and Payer fields.


191-227: Good test for AuthorizationProvider precedence.

This test verifies that AuthorizationProvider takes precedence over static Authorization when both are set, and uses atomic operations to safely count invocations.


351-395: Clever retry simulation using connection hijacking.

The test effectively simulates transient failures by hijacking and closing connections on early attempts. The fallback to 503 status ensures the test works even if hijacking isn't supported.


733-786: Good test for enrichment merge behavior.

This test verifies that user-specified values in Extra are preserved while facilitator-provided values are merged in. This is important for allowing user overrides of facilitator defaults.


811-836: LGTM on timeout test.

Verifies that VerifyTimeout is honored and that timeout errors are properly wrapped as ErrFacilitatorUnavailable.

v2/http/gin/middleware.go (6)

6-15: LGTM! Imports properly grouped.

Imports follow the coding guidelines with stdlib first, external dependencies second, and internal packages last with blank lines between groups.


60-88: HTTP client timeout issue has been addressed.

The http.Client instances now include Timeout: v2.DefaultTimeouts.RequestTimeout, resolving the previous concern about requests potentially hanging indefinitely.


90-100: LGTM! Enrichment context handling is correct.

The context timeout and cancellation pattern is properly implemented. The synchronous EnrichRequirements call completes before defer cancel() executes, and the fallback to original requirements on error provides resilience.


145-157: Pointer dereference after FindMatchingRequirement is safe.

The requirement returned by FindMatchingRequirement (line 136) is checked for error before use at line 145. The dereference *requirement is safe since error handling at lines 137-141 returns early if no match is found.


200-209: LGTM! Dual context storage provides good compatibility.

Storing the payment response in both Gin context and stdlib context enables consumers to use either pattern for accessing verified payment information.


225-237: LGTM! Safe context extraction pattern.

GetPaymentFromContext properly handles both the case where the key doesn't exist and the case where the type assertion fails, returning nil in both scenarios.

v2/mcp/server/server.go (3)

3-10: LGTM! Imports properly grouped.

Standard library, external MCP dependencies, and internal v2 package are correctly separated with blank lines.


18-36: LGTM! Defensive constructor with proper nil handling.

The constructor properly handles nil config with a default and initializes the PaymentTools map to prevent nil map assignment panics in subsequent AddPayableTool calls.


44-72: LGTM! Thorough validation with contextual errors.

AddPayableTool properly validates requirements before registration, with error messages that include both the requirement index and tool name for easier debugging. Error wrapping uses %w as per coding guidelines.

v2/mcp/server/handler.go (6)

3-13: LGTM! Imports properly grouped.

Standard library imports are grouped together, followed by the internal v2 package import, following the coding guidelines.


23-40: Past review concern addressed: constructor now returns error instead of panicking.

The NewX402Handler signature correctly returns (*X402Handler, error), allowing callers to handle configuration failures gracefully instead of crashing.


75-108: LGTM! Facilitator initialization returns errors instead of panicking.

The function correctly returns a descriptive error when the primary facilitator URL is missing, allowing callers to handle the failure appropriately.


279-285: Past review concern addressed: X402Version is now validated.

The extractPayment function now validates payment.X402Version against v2.X402Version before returning, preventing operations on incompatible payloads.


408-421: Past review concern addressed: verify-only mode now sets Success: true.

When verification succeeds in verify-only mode, the response now correctly sets Success: true with an empty Transaction field, properly indicating that verification passed but settlement was not attempted.


448-466: LGTM! JSON-RPC error handling follows specification.

The writeError function correctly returns HTTP 200 with the error in the JSON-RPC response body, as per JSON-RPC 2.0 specification.

v2/http/facilitator.go (7)

4-17: LGTM! Imports properly grouped.

Standard library, retry package, and v2 packages are correctly separated following the coding guidelines.


40-81: LGTM! Well-documented struct with comprehensive configuration options.

The FacilitatorClient struct provides clear documentation for each field, including thread-safety considerations for AuthorizationProvider. The lifecycle hooks enable observability and custom logic integration.


109-127: LGTM! Retry configuration correctly distinguishes retries from attempts.

The MaxAttempts = maxRetries + 1 calculation correctly interprets MaxRetries as the number of retry attempts after the initial request, with sensible defaults for delay and exponential backoff.


150-189: LGTM! Robust Verify implementation with proper retry and timeout handling.

The implementation correctly:

  • Respects caller-provided context deadlines while applying defaults when none exists
  • Uses retry with appropriate isFacilitatorUnavailableError predicate
  • Wraps errors with context as per coding guidelines
  • Extracts payer from payload as fallback when not in response

298-336: Past review concern addressed: EnrichRequirements now accepts context.

The method signature correctly accepts ctx context.Context as the first parameter and propagates it to the Supported call, enabling proper cancellation and timeout control.


376-380: LGTM! Proper use of errors.Is for wrapped error detection.

Using errors.Is correctly handles the case where ErrFacilitatorUnavailable is wrapped with additional context via fmt.Errorf("%w", ...).


361-374: SVM payer extraction not implemented—verify if needed for SVM payment handling.

The extractPayer function handles only EVM payloads by extracting the payer from authorization.from. For SVM (Solana) payloads, it returns an empty string. While the facilitator does handle Solana payments, the payer information is embedded within the base64-encoded transaction in the payload, which would require decoding to extract. Determine whether payer extraction from SVM transactions is necessary in the current payment flow, or if the empty string return is acceptable for non-critical use (e.g., logging).

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +1 to +19
let prompt = $"
STUDY scratch/comments.md
STUDY scratch/NOTES.md

Pick the one most important issue to fix and then fix it.

If there are no issues left to fix, simply kill this PID: ($nu.pid)

Important:
- make sure all tests pass when done
- git commit after you are done
- save any notes or note any problems in scratch/NOTES.md
"

for i in 1..10 {
print $"Iteration #($i)..."
opencode run $prompt -m anthropic/claude-sonnet-4-5
print "Done!"
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This file appears to be a development automation script and should not be committed.

This Nu shell script appears to be a local development tool for iteratively running AI-assisted fixes. It references scratch/comments.md and scratch/NOTES.md which are likely temporary working files. Consider adding this to .gitignore or removing it from the PR, as it doesn't appear to be part of the x402 v2 protocol implementation.

🤖 Prompt for AI Agents
In `@ralph.nu` around lines 1 - 19, The file ralph.nu is a local development
automation script (defines prompt and a for loop using variables like prompt and
the loop "for i in 1..10") and should not be committed; remove ralph.nu from the
PR or add its path to .gitignore and delete it from the commit (or move it to a
non-tracked location), then amend the commit so the script is no longer in the
repo; if you prefer to keep it but not track changes, add its filename to
.gitignore and run git rm --cached on ralph.nu before committing.

Comment on lines +336 to +342
// Instruction 2: Create associated token account (idempotent - won't fail if it exists)
// The feePayer sponsors the rent-exempt balance for the destination ATA
associatedtokenaccount.NewCreateInstruction(
feePayer, // payer (facilitator sponsors the rent)
recipient, // wallet address (owner of the ATA)
mint, // SPL token mint address
).Build(),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

solana-go CreateIdempotent associated token account instruction

💡 Result:

Short answer: use the Associated Token Account program's CreateIdempotent instruction variant (the ATA instruction variant whose instruction ID is CreateIdempotent). In solana-go there are packages that expose that variant (the program has a CreateIdempotent type / NewCreateIdempotentInstruction helper). Example (pseudo-Go; adapt to the solana-go package you use):

  • derive ATA:
    ata, _, _ := common.FindAssociatedTokenAddress(ownerPubKey, mintPubKey)

  • build idempotent ATA instruction (example names vary by fork; adapt to your import):
    inst := associatedtokenaccount.NewCreateIdempotentInstruction(
    payerPubKey, // funding account (payer)
    ata, // associated token account (PDA)
    ownerPubKey, // owner of ATA
    mintPubKey, // token mint
    systemProgramID, // system program id (usually solana.SystemProgramID)
    tokenProgramID, // token program id (usually spl-token program id)
    )

  • add inst to transaction and send.

References:

  • associated-token-account Go package (shows Instruction_CreateIdempotent / CreateIdempotent type). [1]
  • gaggliardetto/solana-go issue discussing adding idempotent create instruction. [2]
  • SPL Token docs showing the createAssociatedTokenAccountIdempotent instruction behavior (JS reference). [3]

If you want, tell me which solana-go repo/version you are using (gagliardetto, xetrics, BBleae, etc.) and I’ll give an exact compile-ready snippet for that package.


🏁 Script executed:

# First, let's examine the actual code at the specified location
find . -name "signer.go" -path "*/v2/signers/svm/*" | head -5

Repository: mark3labs/x402-go

Length of output: 87


🏁 Script executed:

# Let's examine the actual code at the specified lines
sed -n '330,350p' ./v2/signers/svm/signer.go

Repository: mark3labs/x402-go

Length of output: 1028


Replace NewCreateInstruction with NewCreateIdempotentInstruction for ATA creation.

The code currently uses associatedtokenaccount.NewCreateInstruction(), which is not idempotent and will fail if the ATA already exists. Use NewCreateIdempotentInstruction() instead, which is the recommended approach per Solana best practices and ensures the transaction succeeds whether the ATA exists or not. The inline comment claiming idempotency is misleading—the actual code must match the intended behavior.

🤖 Prompt for AI Agents
In `@v2/signers/svm/signer.go` around lines 336 - 342, The ATA creation call
currently uses associatedtokenaccount.NewCreateInstruction(feePayer, recipient,
mint).Build() but that is not idempotent; replace it with
associatedtokenaccount.NewCreateIdempotentInstruction(feePayer, recipient,
mint).Build() so the transaction succeeds if the ATA already exists, and update
the inline comment to reflect true idempotency; ensure any references to
NewCreateInstruction in the same file (e.g., the instruction assembly around
signer.go's ATA creation) are updated accordingly.

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.

2 participants