Skip to content

feat(rpc): x402/MPP crypto-micropayment lane for rpc.call#54

Open
johnpmitsch wants to merge 4 commits into
mainfrom
x402_MPP
Open

feat(rpc): x402/MPP crypto-micropayment lane for rpc.call#54
johnpmitsch wants to merge 4 commits into
mainfrom
x402_MPP

Conversation

@johnpmitsch

Copy link
Copy Markdown
Collaborator

Adds a crypto-micropayment payment lane to rpc.call: pay per RPC request with a stablecoin instead of an account API key, against Quicknode's x402/MPP gateways. The SDK runs the 402 -> sign -> resend handshake, guards against overcharge, and surfaces a typed payment-error hierarchy across all four languages.

What's new

  • Three signing constructions, feature-gated (payments / payments-svm / payments-tempo): x402/EVM (EIP-712 TransferWithAuthorization), x402/Solana (SPL TransferChecked), and MPP/Tempo (native type-0x76 tx). Internal enum Signer { Evm, Svm, Tempo } over a SecretString with a redacting Debug.
  • Keyless SDK: api_key is now Option (SdkFullConfig::keyless()); the payment lane needs no account key. from_env stays strict.
  • call_with_receipt returns RpcCallResponse { result, payment_receipt }. call is unchanged.
  • Guards: max_amount is a required spend ceiling (the SDK refuses to sign above it); PaymentIndeterminate on a lost response after paying so callers don't blind-retry into a double charge.
  • New error variants PaymentUnsupported / PaymentRejected / PaymentIndeterminate fanned out to the Python/Node/Ruby typed hierarchies (base PaymentError).
  • CI feature-matrix job (5 combos).

Examples added (one per language)

Each opens a keyless SDK, configures a payment block on rpc, reads the key from QN_PAYMENT_KEY (never hard-coded), pays for eth_blockNumber on Base Sepolia testnet, and handles PaymentIndeterminate / PaymentRejected.

Rustcrates/core/examples/rpc_payment.rs

let mut config = SdkFullConfig::keyless();
config.rpc = Some(RpcConfig {
    payment: Some(PaymentConfig {
        scheme: "x402".into(),
        key,
        pay_network: "eip155:84532".into(),       // Base Sepolia USDC
        asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".into(),
        max_amount: "10000".into(),               // spend ceiling (required)
        svm_rpc_url: None,
        base_url_override: None,
    }),
    ..Default::default()
});
let qn = QuicknodeSdk::new(&config)?;
let resp = qn.rpc.call_with_receipt("eth_blockNumber", None, Some("base-sepolia".into()), None).await?;

Node/TypeScriptnpm/examples/rpc_payment.ts

const qn = new QuicknodeSdk({
  rpc: { payment: {
    scheme: "x402", key,
    payNetwork: "eip155:84532",
    asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    maxAmount: "10000",
  } },
});
const { result, paymentReceipt } = await qn.rpc.callWithReceipt("eth_blockNumber", [], "base-sepolia");

Pythonpython/examples/rpc_payment.py — same shape via PaymentConfig(...) keyword args, except PaymentIndeterminateError / PaymentRejectedError.

Rubyruby/examples/rpc_payment.rb — same shape via a payment hash on rpc:, rescue QuicknodeSdk::Error::PaymentIndeterminateError.

Notes

  • The examples move real funds when they settle — use a throwaway, minimally-funded wallet. Do not log the config object; its key field is readable (like ethers .privateKey). The SDK never prints the key in its own errors/Debug.
  • Live end-to-end smoke (one real settled payment per path) remains a gated manual step; mock/unit coverage is complete (275 core tests, byte-for-byte EIP-712 and Tempo vectors).

The menu selector admits payment amounts as u128, but SPL TransferChecked
encodes the amount as a u64. An amount the selector accepted that overflows
u64 was silently mapped to a vague "missing/invalid amount" config error.
Parse as u128 and narrow explicitly so the overflow surfaces as a clear
message. Adds a regression test.
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