feat(rust): unified dual-protocol axum payment gate (paid_get)#171
Merged
Conversation
A convenience that parses an Authorization: Payment header, rebuilds the route's expected request from the given amount, and verifies via verify_credential_with_expected — so the amount is pinned and a credential minted for a different price is rejected. The unified pay-kit axum gate builds on this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a facade-level gate to solana-pay-kit that accepts both MPP charge and
x402 on one route:
- PayKit::new(PayKitConfig{..}) derives both an Mpp and an X402 from one
shared config (the fee-payer signer drives MPP sponsorship and supplies
x402's fee-payer address).
- paid_get/paid_post one-liner; an unpaid request returns 402 carrying BOTH
the MPP WWW-Authenticate and the x402 PAYMENT-REQUIRED challenges. The paid
retry is detected by header and verified by the matching protocol, with the
resolved price pinned (cross-route replay rejected).
- Payment handler extractor (amount/protocol/reference), Price::dynamic for
per-request pricing.
- axum_quickstart example + crate README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the solana-pay-kit README to mirror the TypeScript README's structure (banner, progressive quick start, run-the-example, MPP, x402, client, vocabulary, primitives, pricing, signers, install/features, test, harness, spec, repo layout) adapted to the Rust dual-protocol gate. Point the workspace rust/README.md at it as the SDK guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the comprehensive TypeScript-structured guide to rust/README.md (the canonical place) and drop the duplicate at crates/kit/README.md. The kit crate's `readme` points at ../../README.md so crates.io renders the same guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
936865c to
8040768
Compare
EfeDurmaz16
previously approved these changes
Jun 15, 2026
EfeDurmaz16
left a comment
Collaborator
There was a problem hiding this comment.
Light review of the gate logic and the amount-pinned verify — looks solid.
- Fail-closed. The handler runs only after a successful verify, and the
Paymentextension is inserted only on success, so the extractor is unreachable on an unpaid request; missing / unknown / failed payment all fall through to a 402.challenge_responsebuilds the 402 best-effort, so a header-format error still denies access rather than opening it. - Cross-route replay pinned.
verify_payment_for_amountrebuilds the expected request from the resolved price (and x402process_paymentpins the same amount), so a credential minted for a cheaper route is rejected.verify_payment_for_amountreuses the existingverify_credential_with_expected— no new trust surface. - Protocol disambiguation is unambiguous (
Authorization: Payment→ MPP,PAYMENT-SIGNATURE/X-PAYMENT→ x402), MPP taking precedence when both are present.
Two non-blocking nits: Err(_) on verify swallows the reason (no log) — correct behavior, just harder to debug a rejected payment; and query_param returns the raw, undecoded query value, worth a doc note for the dynamic-price closure.
LGTM.
- P1: percent-decode query params in PriceCtx::query_param so a percent- encoded tier (e.g. ?tier=premi%75m) can't bypass the premium price and land on the cheaper default. Adds a regression test. - Log (tracing::warn) each error arm in challenge_response so a dropped MPP/x402 challenge header is diagnosable instead of silently missing. - x402_reference now returns Option; a verified payment with no signature is rejected with a fresh 402 rather than handing the handler an empty settlement reference. - Add an x402-path test (invalid PAYMENT-SIGNATURE -> 402). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Addressed the Greptile review in
|
EfeDurmaz16
approved these changes
Jun 15, 2026
EfeDurmaz16
left a comment
Collaborator
There was a problem hiding this comment.
Re-checked 27d49d1, all four hold:
percent_decodecovers%XXand+with safe bounds, so thepremi%75mbypass is closed and the regression test pins it.challenge_responsenow warns on every dropped-header arm, so a dropped MPP/x402 challenge is visible to operators instead of silent.x402_referencereturnsOption, so an unsigned tx 402s instead of reaching the handler with an empty reference.x402_invalid_signature_returns_402locks the x402 rejection path.
Fail-closed gating and price pinning still hold. Good to go.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Brings the Rust SDK to ergonomic parity with the other pay-kit SDKs: a one-liner axum gate that accepts both MPP charge and x402 on a single route, in the abstracted
paid_get/Paymentstyle (not intent-specific).How it works
PayKit::new(PayKitConfig)(insolana-pay-kit) derives both anMppcharge handler and anX402handler from one shared config. The optionalfee_payer_signerdrives MPP fee-sponsorship and supplies x402's fee-payer address from the same key.paid_get/paid_postgate a handler. An unpaid request gets a 402 carrying both challenges —WWW-Authenticate(MPP) andPAYMENT-REQUIRED(x402). The headers are disjoint, so the paid retry is detected unambiguously (Authorization: Payment→ MPP,PAYMENT-SIGNATURE/X-PAYMENT→ x402) and verified by the matching protocol.Paymenthandler extractor exposesamount/protocol/reference; the response carries the protocol's settlement header.Price::dynamicprices a route per request.Supporting change in
solana-mpp: a reusableMpp::verify_payment_for_amount(credential, amount)(the amount-pinned verify the gate builds on). The existingMppCharge<C>typed extractor is unchanged.Tests / docs
cargo test -p solana-pay-kit --features axum— 5 gate tests + a doctest, including "unpaid → 402 with both protocol challenges" and "cross-route replay → 402". Adds a runnableexamples/axum_quickstart.rsand a crate README.Notes
Separate from the cross-language audit PR (#169). Independent of it — based directly on
main.🤖 Generated with Claude Code