Skip to content

feat(rpc): crypto-micropayment lane for qn rpc call (--x402/--mpp)#56

Open
johnpmitsch wants to merge 8 commits into
mainfrom
x402_MPP
Open

feat(rpc): crypto-micropayment lane for qn rpc call (--x402/--mpp)#56
johnpmitsch wants to merge 8 commits into
mainfrom
x402_MPP

Conversation

@johnpmitsch

Copy link
Copy Markdown
Collaborator

Adds a crypto-micropayment lane to qn rpc call so a call can pay per request with a stablecoin instead of an account API key. No login and no Tooling Access needed: --x402 pays on EVM/Solana, --mpp settles on Tempo. The private key resolves from a file, env var, or config path (never a flag, never stored, never printed), and every call is bounded by a --max-amount spend ceiling.

This moves real funds (testnet tokens are still real transfers), so paid calls never auto-retry and the 2/3 exit-code split carries payment semantics: 2 = gateway refused, nothing settled; 3 = outcome unknown, payment may have settled — check the wallet before re-running.

Examples

Pay per call with an explicit wallet:

qn rpc call eth_blockNumber --network base-sepolia --x402 \
    --payment-key-file ~/.keys/payer \
    --pay-network base-sepolia \
    --asset 0x036CbD53842c5426634e7929541eC2318f3dCF7e \
    --max-amount 10000

Store the parameters once in ~/.config/qn/config.toml (the raw key still lives in a file, never config) and the invocation shrinks to just the scheme flag:

[rpc.payment]
key_file    = "/home/me/.config/qn/payment.key"
max_amount  = "10000"
pay_network = "base-sepolia"
asset       = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
# EVM/Solana stablecoin
qn rpc call eth_blockNumber --network base-sepolia --x402

# MPP on Tempo, with a settlement receipt
qn rpc call eth_blockNumber --network tempo-testnet --mpp --receipt

Config supplies values but never activates payment on its own — the scheme flag is always required.

Notes

  • --network names the chain you query (the gateway path slug); --pay-network names the chain the payment settles on, and takes a Quicknode network name (base-sepolia, solana-devnet, tempo-testnet, ...) or a raw CAIP-2 id (eip155:84532) passed through verbatim.
  • --receipt wraps stdout as {"result": ..., "payment_receipt": ...}: an object on MPP (with reference = settlement tx hash), null on x402. Without it, paid output is shaped exactly like unpaid output.
  • README, qn agent context, and error/exit-code mapping all updated for the new lane.

Tests

23 integration tests in tests/rpc_payment.rs cover the happy path (x402 + MPP), receipt shaping, over-cap refusal, mutual exclusivity, config-vs-flag precedence, no-auto-retry, and the full 2-vs-3 exit-code split for refused vs. unknown-outcome payments. All passing.

Before merge

  • Cargo.toml currently uses a local path dependency on the SDK's unreleased payment features (commit 6ce43ee). Swap back to a published crates.io version with the same features once the SDK ships them. The release is gated on this.

Points the SDK dependency at the local checkout's crypto-micropayment
branch and enables the payments, payments-svm, and payments-tempo
features. Dev-only: the release swaps back to a published crates.io
version once the SDK ships this feature.
Parameter defaults (key_file, max_amount, pay_network, asset,
svm_rpc_url) for qn rpc call --x402/--mpp. The section only supplies
values; activation stays with the per-invocation scheme flag. The raw
private key never lives in config.toml: key_file points at a file, and
an inline key = ... is captured by a trap field so payment resolution
can reject it with an actionable error instead of serde silently
ignoring it. max_amount accepts a TOML string or integer. 7 new tests.
…ance

PaymentUnsupported and PaymentRejected map to exit 2 (the gateway
refused); PaymentIndeterminate and the new PaymentMaybeCharged wrapper
map to exit 3 (request sent, outcome unknown), so scripts can
distinguish safe-to-retry from check-your-wallet-first. Render arms
state whether anything was charged and warn against blind re-runs;
gateway bodies appear only under --verbose. 6 new tests.
Ctx::from_global_keyless_payment builds a keyless SDK carrying only the
payment config, so paid calls work with no API key configured. It skips
API-key resolution, the token seed, [rpc] endpoint_url, and --base-url
sub-client overrides (the paid lane's test hook is
PaymentConfig.base_url_override). The User-Agent install is factored
into a helper shared with the keyed path.
Adds per-request stablecoin payment to qn rpc call via the SDK's
402 -> sign -> resend handshake. Activation is explicit and per
invocation: --x402 or --mpp both turns the lane on and selects the
scheme; [rpc.payment] config only supplies parameter defaults.

The lane lives in src/commands/rpc/payment.rs (rpc.rs became a
directory module) and branches off before the default lane's machinery,
so the token cache, Tooling Access enable/recovery, networks map, and
retrying() are structurally unreachable: paid calls are never
auto-retried, and a lost or uninterpretable post-payment response maps
to PaymentMaybeCharged (exit 3, check-your-wallet guidance).

Key resolution: --payment-key-file <PATH|-> > QN_PAYMENT_KEY >
key_file in config; the raw key is never a flag value, never inline in
config, and never logged. The spend ceiling (--max-amount, base units)
has no built-in default and is integer-validated before any request.
--receipt opts stdout into {result, payment_receipt} (settlement tx
hash on MPP, null on x402); the default paid output shape is identical
to an unpaid call. 20 new unit tests.
18 tests against a wiremock gateway using the SDK's 402-handshake
shapes (x402 menu + signed resend; MPP WWW-Authenticate challenge +
Payment-Receipt header) and public fixtures (anvil key #0, Base Sepolia
test USDC). Safety-critical assertions:

- lane isolation: a paid call sends exactly the two gateway POSTs,
  never touches /v0 control-plane routes, and writes no tokens.toml
- config presence never auto-activates payment (gateway .expect(0))
- the paid lane ignores --retries (one request, ever)
- an over-cap offer is refused after one request, before signing
- every pre-flight failure (missing network/key/cap, non-integer cap,
  inline config key, double-stdin) exits before any request
- --receipt wraps stdout with the MPP settlement reference and null on
  x402; without it the paid output is the bare result; the raw key
  never appears on either stream (subprocess assertions)

Also applies rustfmt to the new payment modules.
Covers the --x402/--mpp surface, the key-resolution ladder (file/stdin
flag > QN_PAYMENT_KEY > key_file in config; never a raw key on argv or
in config), the per-call spend ceiling, --receipt output shape, the
query-chain vs pay-chain distinction, and the payment exit-code
semantics: 2 = the gateway refused, 3 = outcome unknown, the payment
may have settled — check the wallet before re-running. Notes that paid
calls never auto-retry and that config supplies parameters but never
activates payment.
…e errors

Adds human-readable names to the paid RPC lane: --pay-network (and
pay_network in [rpc.payment]) now takes a Quicknode network name like
base-sepolia, solana-devnet, or tempo-testnet, resolved to CAIP-2 before
reaching the SDK. Raw CAIP-2 ids still pass through verbatim, so every
chain stays reachable without a table entry. EVM chain ids are verified
against the public registry at chainid.network; names that could not be
verified are deliberately absent, since a wrong id is worse than an
error pointing at the CAIP-2 escape hatch.

Also sharpens paid-lane error semantics: a gateway 5xx on the paid
resend is wrapped as PaymentMaybeCharged (exit 3, payment submitted,
check the wallet), so exit 2 (PaymentRejected 4xx / PaymentUnsupported)
always and only means the gateway refused without settling. Messages
reworded to match.

9 new tests: 5 unit on the resolver, 3 unit on payment config
resolution, 2 integration (name matches a CAIP-2 offer end-to-end;
unknown name fails preflight with zero requests sent); MPP flow with
--pay-network tempo-testnet verified against the live gateway.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant