Ruby SDK for the Machine Payments Protocol
Full documentation, API reference, and guides are available at mpp.dev/sdk/ruby.
gem install mpp-rbOr add to your Gemfile:
gem "mpp-rb"require "mpp-rb"
server = Mpp.create(
method: Mpp::Methods::Tempo.tempo(
intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
recipient: "0x0000000000000000000000000000000000000001",
),
)
# In your request handler (Sinatra, Rails, Rack, etc.)
result = server.charge(authorization_header, "0.50", description: "Paid endpoint")
if result.is_a?(Mpp::Challenge)
# Return 402 with WWW-Authenticate header
resp = Mpp::Server::Decorator.make_challenge_response(result, server.realm)
# resp["status"], resp["headers"], resp["body"]
else
credential, receipt = result
# credential.source — payer address
# receipt.to_payment_receipt — Payment-Receipt header value
endIf the endpoint already uses Authorization (API keys, Bearer tokens), create the server with requires_auth: true. Challenges then advertise header="Payment-Authorization", and clients send the Payment credential in that header instead of Authorization.
server = Mpp.create(method: tempo, requires_auth: true)
result = server.charge(
env["HTTP_AUTHORIZATION"],
"0.50",
payment_authorization: env["HTTP_PAYMENT_AUTHORIZATION"],
)server = Mpp.create(methods: [tempo, evm, stripe])
paid = server.compose(
[tempo, {amount: "0.01"}],
[evm, {amount: "0.01"}],
[stripe, {amount: "0.01", currency: "usd"}]
)
result = paid.call(
authorization: env["HTTP_AUTHORIZATION"],
payment_authorization: env["HTTP_PAYMENT_AUTHORIZATION"],
payment_signature: env["HTTP_PAYMENT_SIGNATURE"],
accept_payment: env["HTTP_ACCEPT_PAYMENT"],
url: request.url,
http_method: request.request_method
)
if result.payment_required?
resp = result.to_response
else
credential, receipt = result.payment
endMpp::Methods::Stripe.create configures Stripe Payment Tokens and static
crypto deposit addresses from an injected Stripe client. Add the stripe gem
to your application, then configure the methods:
require "stripe"
stripe_client = Stripe::StripeClient.new(ENV.fetch("STRIPE_SECRET_KEY"))
payments = Mpp::Methods::Stripe.create(
client: stripe_client,
network_id: ENV.fetch("STRIPE_NETWORK_ID"),
livemode: false,
deposit_addresses: {
tempo: ENV.fetch("TEMPO_DEPOSIT_ADDRESS"),
base: ENV.fetch("BASE_DEPOSIT_ADDRESS")
},
metadata: {"product" => "example"}
)
server = Mpp.create(methods: payments.default_methods)With no deposit addresses, defaults are Stripe Payment Tokens only.
default_methods adds Tempo when its static address is configured. Add Base
explicitly with its x402 facilitator:
base = payments.base.charge(
x402: {facilitator: "https://x402.org/facilitator"}
)
server = Mpp.create(methods: [*payments.default_methods, base])When composed, SPT offers below $0.50 and Tempo/Base offers below one cent are not advertised. Successful Tempo and Base payments are best-effort recorded as Stripe crypto transaction-verification PaymentIntents. Metadata is included on every Stripe PaymentIntent.
evm.charge additionally emits PAYMENT-REQUIRED and accepts PAYMENT-SIGNATURE (x402 v2 exact) when a facilitator is configured:
# Public / testnet facilitator
x402: {facilitator: "https://x402.org/facilitator"}
# Per-request headers (bearer token, CDP JWT, etc.)
x402: {facilitator: {url: facilitator_url, headers: -> { {"Authorization" => "Bearer #{token}"} }}}
# The proc may take the request path (`/verify`, `/settle`) when headers differ per call
x402: {facilitator: {url: cdp_url, headers: ->(path) { cdp_headers(path) }}}
# Any client with #verify / #settle
x402: {facilitator: cdp_client}Tempo charge can sponsor gas through a hosted fee payer, or skip local RPC by sending credentials to a Tempo API-compatible relay. Both use the same {url:, headers:} shape as the x402 facilitator:
# Hosted fee payer (JSON-RPC eth_signRawTransaction)
fee_payer: {url: sponsor_url, headers: -> { {"Authorization" => "Bearer #{token}"} }}
# Local co-sign
fee_payer: Mpp::Methods::Tempo::Account.from_key(ENV.fetch("FEE_PAYER_KEY"))
# Relay (POST /v1/mpp/validate then /v1/mpp/broadcast)
relay: {url: "https://api.tempo.xyz", headers: -> { {"tempo-api-key" => ENV.fetch("TEMPO_API_KEY")} }}require "mpp-rb"
account = Mpp::Methods::Tempo::Account.from_key("0x...")
transport = Mpp::Client::Transport.new(
methods: [
Mpp::Methods::Tempo.tempo(
account: account,
intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
),
],
)
response = transport.request(:get, "https://mpp.dev/api/ping/paid")Register hooks to observe the automatic payment lifecycle. Each registration returns an unsubscribe proc.
server.on_challenge_created do |payload|
puts "challenge: #{payload[:challenge].id}"
end
server.on_payment_success do |payload|
puts "paid: #{payload[:receipt].reference}"
end
transport.on_challenge_received do |payload|
puts "received: #{payload[:challenge].id}"
nil
end
transport.on_payment_response do |payload|
puts "retry status: #{payload[:response].code}"
end
transport.on("*") do |event|
puts "payment event: #{event.name}"
endClient events are challenge.received, credential.created, payment.response, and payment.failed. Server events are challenge.created, payment.success, and payment.failed.
When a side effect belongs to one payment method, attach it to the method instead. The hook only receives successful payments for that method and intent; an exception in the hook does not invalidate the payment.
method = Mpp::Methods::Tempo.tempo(
intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
recipient: "0x0000000000000000000000000000000000000001",
on_payment_success: ->(payload) {
record_payment(payload[:receipt], payload[:request])
},
)require "mpp-rb"
handler = Mpp.create(
method: Mpp::Methods::Tempo.tempo(
intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
recipient: "0x0000000000000000000000000000000000000001",
),
)
# In your config.ru or Rails middleware stack:
use Mpp::Server::Middleware, handler: handler
# In your app, signal that payment is required:
env["mpp.charge"] = { amount: "0.50", description: "Paid endpoint" }| Example | Description |
|---|---|
| tempo_charge | Tempo testnet payments via Sinatra |
| stripe_charge | Stripe payments via Shared Payment Tokens |
| compose | Tempo + Base USDC + Stripe SPTs on one endpoint |
| evm_x402 | EVM charge with x402 exact compatibility |
| tempo_feepayer | Tempo charge with a hosted fee-payer {url:, headers:} |
| tempo_relay | Tempo charge delegated to an MPP relay {url:, headers:} |
Each example is a standalone Sinatra app with /free and /paid endpoints. To run one:
cd examples/tempo_charge
bundle install
ruby app.rbThen test with mppx, a CLI that handles the full 402 challenge/credential flow:
npx mppx http://localhost:4567/paid| Method | Charge Client | Charge Server |
|---|---|---|
| Tempo | Yes | Yes |
| Stripe | Yes | Yes |
EVM (evm.charge, x402 exact) |
No | Yes |
Tempo charge transaction construction is implemented directly in Ruby. Runtime dependency: keccak (Tempo attribution memos). Optional dependencies: eth (account signing, EIP-3009 recovery) and rlp (fee payer envelope).
Mpp.create accepts a single method: (unchanged) or methods: to register several payment methods. server.compose presents every method as multiple WWW-Authenticate challenges; evm.charge also emits PAYMENT-REQUIRED and accepts PAYMENT-SIGNATURE when a facilitator is configured. The Ruby HTTP client does not yet sign EVM or x402 credentials.
Built on the "Payment" HTTP Authentication Scheme. See mpp-specs for the full specification.
- Create a release PR:
- Update the version in
lib/mpp/version.rb - Run
bundle lock --update mpp-rbin the root and eachexamples/subdirectory - Commit and open a PR to verify CI passes
- Update the version in
- Merge the PR
- Tag the merge commit:
git tag v0.x.x - Push the tag:
git push origin --tags
MIT