Skip to content

Latest commit

 

History

418 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MPP

CI Code Scanning GitHub

Elixir implementation of the Machine Payments Protocol (MPP) — HTTP 402 payment middleware for AI agents and machine-to-machine commerce.

What is MPP?

MPP is an open standard for machine-to-machine payments via HTTP 402, co-developed by Stripe and Tempo Labs. It enables any API to charge per-request without user accounts, API keys, or signup flows.

Payment is authentication. An agent hits your endpoint, gets a 402 challenge, pays, and receives the response — all in a single HTTP roundtrip.

How It Works

Client                                    Server
  │                                         │
  │─── GET /api/data ──────────────────────►│
  │                                         │
  │◄── 402 Payment Required ───────────────│
  │    WWW-Authenticate: Payment            │
  │    (challenge with price + method)      │
  │                                         │
  │    [Client fulfills payment]            │
  │                                         │
  │─── GET /api/data ──────────────────────►│
  │    Authorization: Payment <credential>  │
  │                                         │
  │◄── 200 OK + Payment-Receipt ───────────│
  │    (resource + proof of payment)        │
  │                                         │

Quick Start

Mount MPP.Plug in your Phoenix router to gate any endpoint behind payment:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :paid do
    plug MPP.Plug,
      secret_key: "your-hmac-secret",
      realm: "api.example.com",
      method: MPP.Methods.Stripe,
      amount: "5000",
      currency: "usd",
      method_config: %{
        "stripe_secret_key" => "sk_test_...",
        "network_id" => "profile_1Mqx...",
        "payment_method_types" => ["card"]
      }
  end

  scope "/premium", MyAppWeb do
    pipe_through [:api, :paid]
    get "/data", DataController, :show
  end
end

Tempo (Stablecoins)

pipeline :paid_tempo do
  plug MPP.Plug,
    secret_key: "your-hmac-secret",
    realm: "api.example.com",
    method: MPP.Methods.Tempo,
    amount: "1000000",
    currency: "0x...(pathUSD token address)",
    recipient: "0x...your-address",
    method_config: %{
      "rpc_url" => "https://rpc.tempo.xyz",
      "chain_id" => 4217,
      "fee_payer" => true,
      # Sponsorship requires an explicitly selected atomic store.
      # ConCache is single-node; use one shared backend across nodes that sponsor
      # the same wallet.
      "store" => MPP.Tempo.ConCacheStore,
      # Either use a local fee-payer key...
      "fee_payer_private_key" => "0x...",
      # ...or delegate co-signing to a hosted eth_fillTransaction endpoint.
      # "fee_payer_url" => "https://sponsor.example.com",
      # "sponsor_budget_id" => "0x...hosted-sponsor-wallet",
      "fee_token" => "0x...(fee token address)",
      "fee_payer_policy" => %{
        "max_in_flight_total_fee" => 500_000_000_000_000_000,
        "max_in_flight_reservations" => 100
      },
      "wait_for_confirmation" => false,
      "memo" => "0x...(optional 32-byte memo)"
    }
end

EVM (Ethereum, Base, Polygon, etc.)

pipeline :paid_evm do
  plug MPP.Plug,
    secret_key: "your-hmac-secret",
    realm: "api.example.com",
    method: MPP.Methods.EVM,
    amount: "1000000",
    currency: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    recipient: "0x...your-address",
    method_config: %{
      "rpc_url" => "https://mainnet.infura.io/v3/YOUR_KEY",
      "chain_id" => 1
    }
end

Currency is the ERC-20 token contract address (e.g., USDC above). For native ETH, use "ETH" or the zero address. "chain_id" is required — the EIP-155 chain ID of the target network (e.g. 1 for Ethereum mainnet). Hash credentials: the client broadcasts a transaction, then sends the hash. For Circle USDC/EURC, set "private_key" (server-only settlement key) to advertise type="authorization" and settle EIP-3009 transferWithAuthorization with challengeHash as the nonce.

Solana (SOL and SPL tokens)

pipeline :paid_solana do
  plug MPP.Plug,
    secret_key: "your-hmac-secret",
    realm: "api.example.com",
    method: MPP.Methods.Solana,
    amount: "10000000",
    currency: "sol",
    recipient: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    method_config: %{
      "rpc_url" => "https://api.mainnet-beta.solana.com",
      "network" => "mainnet"
    }
end

Currency is "sol" for native SOL (amount in lamports) or a base58 mint address for SPL tokens. Pull mode (type="transaction") sends signed transaction bytes for the server to broadcast; push mode (type="signature") sends a confirmed signature. Set "fee_payer" => true with "fee_payer_private_key" to co-sign as fee payer. Optional "splits" (at most 8) add extra payment legs. Set "confidential" => true (Token-2022 mints only) to require the confidential transfer profile: the client submits a type="bundle" credential whose final transaction carries the single Token-2022 confidential Transfer/TransferWithFee, and the server confirms the amount by decrypting the recipient pending-balance delta with "recipient_elgamal_secret_key".

NEAR Intents (1Click)

Hash-only charges. Call MPP.Methods.NearIntents.quote/1 to mint a wet EXACT_OUTPUT 1Click quote, then mount the returned amount, origin asset, deposit address, and method_config on MPP.Plug. The client deposits on the origin chain and retries with type="hash". Verification waits for 1Click SUCCESS (and can check EVM origin RPC when "origin_rpc_url" is set). A configured "store" must implement atomic MPP.Tempo.Store.update/3. There is no Intents testnet — live tests use production 1Click plus historical deposits. Optional partner JWT: "one_click_jwt" / NEAR_INTENTS_ONE_CLICK_JWT.

{:ok, quote} =
  MPP.Methods.NearIntents.quote(%{
    "origin_asset" => "eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7",
    "origin_asset_id" => "nep141:eth-usdt.omft.near",
    "destination_asset" => "tron:mainnet/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "destination_asset_id" => "nep141:tron-usdt.omft.near",
    "destination_recipient" => "TJ4FU4NFMqFDtcLYxFnJvfv3rWfLN9vCB7",
    "amount_out" => "1000000",
    "refund_to" => "0x...",
    "deadline" => deadline
  })

plug MPP.Plug,
  secret_key: "your-hmac-secret",
  realm: "api.example.com",
  method: MPP.Methods.NearIntents,
  amount: quote.amount,
  currency: quote.currency,
  recipient: quote.recipient,
  method_config: quote.method_config

Replay protection is on by default. When you don't configure a "store", MPP uses the app-started MPP.Tempo.ConCacheStore so each transaction hash is accepted only once out of the box. For multi-node deployments, configure method_config["store"] with a shared MPP.Tempo.Store implementation (Redis, Postgres, …); a configured store must implement the atomic check_and_mark/2. When multiple endpoints share one ConCacheStore, add key_prefix: "tenant:" in the store opts to namespace dedup keys. Pass store: false (Plug opt) or "store" => false (method_config) to explicitly opt out of dedup — not recommended.

Multi-Method (Stripe + Tempo)

Offer multiple payment options in a single 402 response — the agent picks whichever it can pay with:

pipeline :paid_multi do
  plug MPP.Plug,
    secret_key: "your-hmac-secret",
    realm: "api.example.com",
    methods: [
      [
        method: MPP.Methods.Stripe,
        amount: "5000",
        currency: "usd",
        method_config: %{
          "stripe_secret_key" => "sk_test_...",
          "network_id" => "profile_1Mqx..."
        }
      ],
      [
        method: MPP.Methods.Tempo,
        amount: "5000000",
        currency: "0x...(pathUSD)",
        recipient: "0x...",
        method_config: %{"rpc_url" => "https://rpc.tempo.xyz", "chain_id" => 4217}
      ]
    ]
end

Requests without payment get a 402 Payment Required with a challenge. Requests with a valid Authorization: Payment credential pass through with a Payment-Receipt header and the receipt in conn.assigns[:mpp_receipt].

Each route can have its own pricing — just mount MPP.Plug with different amount/currency per pipeline or scope.

Recurring subscriptions

Set intent: "subscription" with period_unit and period_count to use the shared MPP.Intents.Subscription schema. MPP.Methods.Stripe activates a constrained fixed-price Stripe subscription, verifies its paid first invoice, and records the activation durably; MPP.Methods.Stripe.Subscription.process_invoice/3 maps paid renewal cycle invoices onto canonical billing periods with atomic event/invoice dedup, and cancel/2 schedules Stripe cancellation at the end of the last paid period. MPP.Methods.Tempo activates a scoped access key, settles the first period, and exposes MPP.Methods.Tempo.Subscription.authorize/2 for later renewals. Both methods use MPP.Subscription.ETSStore by default; configure a shared MPP.Subscription.Store backend when renewals must coordinate across nodes or survive restarts.

What This Means for Your API

Today, monetizing an API means building a billing system: user accounts, API key provisioning, usage tracking, rate limiting, a pricing page, a dashboard. That's months of work before you earn a cent.

With MPP, you add one Plug to your router and your API charges per-request. No accounts. No API keys. No billing infrastructure. The payment is the authentication.

Use cases:

  • Charge $0.01 per AI inference call
  • Charge $0.50 per premium data query
  • Charge $5.00 per document generation
  • Different prices per route — one Plug per endpoint

For AI agents: Your API becomes callable by any agent with a wallet. No onboarding flow, no API key provisioning, no approval process. The agent discovers the price from the 402 response, pays, and gets the resource. That's it — your API just acquired a customer in one HTTP roundtrip.

Why MPP?

  • No user management. No accounts, no API keys, no dashboards, no onboarding. The 402 flow handles auth and billing in one protocol.
  • Agent-native. AI agents can't click buttons or fill out forms. They can make HTTP requests and hold wallets. MPP meets agents where they are.
  • Sticky by default. When your API is a line of code in a deployed system, the switching cost is engineering hours — not emotional preference.
  • Payment-method agnostic. Stripe cards, stablecoins, on-chain tokens, Lightning — all pluggable via the same Method behaviour.

Payment Methods

Method Protocol Settlement Status
Stripe MPP Fiat (cards, wallets) v0.1.0
Tempo MPP Stablecoins (TIP-20) v0.2.0
EVM MPP Any EVM chain (ETH, USDC, ERC-20) v0.3.0
Solana MPP Native SOL and SPL tokens (incl. Token-2022 confidential) v0.14.0
NEAR Intents MPP Cross-chain deposits via 1Click (hash-only) v0.14.0
Lightning MPP Bitcoin (BOLT11) Future

The server can offer multiple payment methods in a single 402 response. The agent picks whichever it can pay with.

Tempo capabilities: Local or hosted fee-payer co-signing (server sponsors gas), fee-token allowlists, optimistic broadcast (respond before block inclusion), memo matching for transaction tagging, zero-amount proof credentials, delegated access-key proof authorization, opt-in presenter-identity binding for hash/transaction credentials, first-party machine-token (MPP Credits / machineUSD) charge payments via "machine_token_enabled", and pluggable dedup stores with a built-in ETS+TTL option via ConCache, including per-store key prefixes for shared-cache tenancy.

Tempo security note: Challenges expire by default. On routes without a configured static memo, Tempo payments must use challenge-bound attribution metadata; plain transfers are rejected by the hardened verifier. Sponsored transactions are bounded by fee-payer gas policy and returned hosted fee tokens are checked against the sponsor allowlist before broadcast. Setting "require_presenter_binding" => true in the Tempo method_config additionally requires hash/transaction credential presenters to prove control of the transfer sender's wallet with a "presenterSignature" (the proof path's EIP-712 envelope, signed by the sender wallet or an authorized access key; the client signs MPP.Methods.Tempo.Proof.hash/1 typed data) — closing the front-running residual documented in GHSA-34g7-vx6g-82mq. The requirement is advertised as "presenterBinding": true in the 402 method details. Opt-in because neither reference SDK binds the presenter on the hash path.

Tempo networks: Mainnet (chain ID 4217, rpc.tempo.xyz) | Testnet (Moderato) (chain ID 42431, rpc.moderato.tempo.xyz)

Modules

Module Purpose
MPP.Plug Plug middleware — the main integration point
MPP.Plug.Config Validated endpoint config (shared settings + method entries)
MPP.Plug.MethodEntry Per-method config within a multi-method endpoint
MPP.Challenge HMAC-SHA256 bound challenge creation/verification
MPP.Credential Payment credential encoding/decoding
MPP.Receipt Proof-of-payment receipt serialization
MPP.Headers WWW-Authenticate (incl. multi-challenge), Authorization, Payment-Receipt headers
MPP.AcceptPayment Accept-Payment client-preference header: parse, format, rank, apply_header
MPP.Errors RFC 9457 Problem Detail error types (incl. session error types)
MPP.Verifier Transport-neutral verification pipeline (HMAC, realm, expiry, request match, method.verify)
MPP.JCS RFC 8785 JSON Canonicalization (MPP subset) for cross-SDK HMAC interop
MPP.BodyDigest SHA-256 body digest compute/verify for request body binding
MPP.Amount Amount/decimals helpers: parse_units, with_base_units, parse_dollar_amount
MPP.Expires Expiration helpers: seconds, minutes, hours, days, weeks, months, years, assert!
MPP.DID DID helpers for EVM credential sources
MPP.Method Behaviour for pluggable payment methods
MPP.Intents.Charge Charge intent request schema
MPP.Intents.Session Session intent request schema (pay-as-you-go)
MPP.Intents.Subscription Shared recurring-subscription intent schema
MPP.Session.Channel Session channel state, balance, and action wire mapping
MPP.Session.Voucher EIP-712 voucher typed data and signature verification
MPP.Session.Payload Session credential payload schema (open / voucher / topUp / close)
MPP.Session.Actions Session credential action handlers and per-channel balance tracking
MPP.Session.Method use wrapper that dispatches verify/2 through session actions
MPP.Session.Store Pluggable session-channel persistence
MPP.Session.ETSStore ETS-backed default session store
MPP.Subscription.Store Pluggable recurring-subscription persistence
MPP.Subscription.ETSStore Application-started single-node subscription store
MPP.Subscription.Record Persisted recurring-payment authority and settlement state
MPP.Methods.Stripe Stripe SPT payment verification
MPP.Methods.Stripe.Subscription Stripe fixed-price subscription activation, durable renewal (process_invoice/3), and period-end cancellation (cancel/2)
MPP.Methods.Tempo Tempo on-chain TIP-20 transfer verification via onchain_tempo
MPP.Methods.Tempo.Subscription Tempo access-key subscription activation, authorization, and renewal
MPP.Methods.Tempo.KeyAuthorization Tempo subscription key-authorization wire codec and verifier
MPP.Methods.Tempo.FeePayerPolicy Fee-payer gas and fee-token sponsorship policy
MPP.Methods.Tempo.HostedFeePayer Hosted eth_fillTransaction fee-payer fill support
MPP.Methods.Tempo.MachineToken Canonical first-party machine-token (MPP Credits) charge-route construction and match
MPP.Methods.Tempo.Proof EIP-712 proof credentials for zero-amount Tempo flows
MPP.Methods.Tempo.SessionReceipt Tempo session receipt wire format
MPP.Methods.EVM Generic EVM on-chain transfer verification (any chain) via onchain
MPP.Methods.EVM.Authorization EIP-3009 transferWithAuthorization settlement for Circle USDC/EURC
MPP.Methods.Solana Solana native SOL and SPL token charge verification via cartouche
MPP.Methods.NearIntents NEAR Intents hash-credential charges via 1Click Swap + origin RPC
MPP.Tempo.Store Behaviour for pluggable transaction dedup stores
MPP.Tempo.ConCacheStore Built-in ETS dedup store with TTL via ConCache
MPP.Telemetry Server-side payment telemetry events for challenges, verification, and receipts
MPP.Discovery.OpenApi OpenAPI 3.1.0 discovery document generation (x-payment-info, 402 responses; mix mpp.openapi)
MPP.Discovery.PaymentInfo Parser/normalizer for the x-payment-info discovery extension
MPP.Mcp MCP (JSON-RPC) transport: server adapter (init/1 + call/3), initialize capabilities/1, error codes, meta keys, client helpers
MPP.Transports.JsonRpc Bare JSON-RPC transport: root-level _meta credential/receipt, -32042 challenges
MPP.Transports.JsonRpc.Plug Plug adapter for JSON-RPC-over-HTTP payment verification
MPP.Transports.WebSocket WebSocket adapter: handshake challenge, credential/receipt frames, JSON-RPC message frames
MPP.Client.PaymentProvider Behaviour for client-side payment providers (supports?/3, pay/2)
MPP.Client.MultiProvider Multi-provider dispatch with first-match routing
MPP.Client.Providers.Tempo Built-in Tempo charge provider — chain-pinned, attribution-bound TIP-20 payments, including machine-token [approve, swapTo] when advertised
MPP.Client.Providers.Stripe Built-in Stripe charge provider — Shared Payment Token creation
MPP.Client.SelectionPolicy Transport-neutral challenge selection/ordering (default: server offer order)
MPP.Client.Req Payment-aware Req plugin — 402 detect, pay, retry (attach/2)
MPP.Client.Transport Client transport behaviour — 402 detection, challenge fetch, credential attach
MPP.Client.Transport.HTTP HTTP transport over Req
MPP.Client.Transport.MCP MCP/JSON-RPC transport: -32042 detection, challenge extract, _meta credential attach
MPP.Client.Transport.JsonRpc Bare JSON-RPC transport: -32042 detection, root-level _meta credential attach
MPP.Client.Transport.WebSocket WebSocket transport: challenge frames, Payment credential frames, retry/backoff
MPP.Client.MCP Payment-aware MCP client — select, approve, pay, retry the tool call once
MPP.Client.AcceptPolicy Gates Accept-Payment header injection on outgoing requests

Client

provider =
  MPP.Client.MultiProvider.new([
    {MPP.Client.Providers.Tempo,
     %{
       private_key: tempo_private_key,
       rpc_url: "https://rpc.tempo.xyz",
       expected_chain_id: 4217,
       client_id: "my-agent"
     }},
    {MPP.Client.Providers.Stripe,
     %{
       secret_key: stripe_secret_key,
       payment_method: "pm_..."
     }}
  ])

Req.new()
|> MPP.Client.Req.attach(provider: provider)
|> Req.get(url: "https://api.example.com/resource")

MPP.Client.Req intercepts HTTP 402, pays, and retries with Authorization: Payment. Provider credentials and endpoints are passed explicitly; the providers do not read application configuration or environment variables. The Tempo provider verifies that the RPC serves the challenge's advertised chain before signing and automatically creates the challenge-bound attribution memo required by routes without a static memo. A payment credential must never be created or attached after a redirect changed the request origin — Req follows redirects by default. MPP.Client.Req.attach/2 refuses that path (:cross_origin_redirect, mpp-rs #379). Callers that drive MPP.Client.Transport.HTTP themselves must apply the same rule: do not call set_credential/2 on a request whose origin (scheme/host/port) differs from the URL the caller asked for.

client = MPP.Client.MCP.new(provider: my_provider)
MPP.Client.MCP.call(client, request, &MyTransport.send/1)

MPP.Client.MCP does the same pay-and-retry over JSON-RPC: it detects -32042, selects a challenge, asks on_payment_required for approval, pays, and retries once with the credential at params._meta["org.paymentauth/credential"].

On the server side, MPP.Mcp.init/1 validates the transport config and MPP.Mcp.call/3 gates tool calls with replay dedup. Merge MPP.Mcp.capabilities(config) into the initialize result to advertise the configured payment methods, their intents, and credential types under experimental.payment.methods (draft-payment-transport-mcp-00).

Generic (non-MCP) JSON-RPC uses root-level _meta so params can be an array. MPP.Transports.JsonRpc.Plug mounts on a Plug route; MPP.Client.Transport.JsonRpc attaches the credential at _meta["org.paymentauth/credential"] on the request envelope.

WebSocket endpoints use typed MPP frames (mpp-rs / alloy-transport-mpp). MPP.Transports.WebSocket is library-agnostic: open/1 emits the handshake challenge, handle_text/2 verifies a credential frame and then dispatches JSON-RPC carried in message frames. MPP.Client.Transport.WebSocket detects challenge frames and attaches Payment <base64url> credential frames. MPP.Client.Transport.WebSocket.Retry matches upstream reconnect posture: capped exponential backoff, fatal latch on protocol errors, and no second payment after a drop that left a credential unacknowledged.

Installation

def deps do
  [
    {:mpp, "~> 0.16.0"}
  ]
end

onchain, onchain_tempo, and con_cache are pulled in automatically — no extra setup for EVM, Tempo, or the built-in MPP.Tempo.ConCacheStore dedup store.

Live Example

Strip0x — blockchain tools API using MPP with Tempo payments. $0.0001 per paid request (100 base units USDC.e on Tempo mainnet).

# Free endpoint (no payment needed)
curl "https://strip0x.com/api/hex/encode?value=hello"

# See the 402 challenge on a paid endpoint
curl -i "https://strip0x.com/api/address/validate?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

# Pay and get the response (~2s round-trip including on-chain settlement)
tempo request -t -X GET "https://strip0x.com/api/address/validate?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

# Machine-readable discovery (OpenAPI 3.1 with x-payment-info extensions)
curl https://strip0x.com/openapi.json

Observed latency: ~2s end-to-end for a paid request (402 challenge + Tempo on-chain TIP-20 transfer + credential retry). Free endpoints respond in ~70ms (network only — business logic is sub-10μs on the BEAM).

Try it and open an issue if anything breaks.

Local demo

No credentials needed — mix mpp.demo starts a demo 402 server on port 4402 (--port to override) with a toy method that accepts a magic "demo-token" payload. The startup banner prints copy-paste curl commands for the full challenge → pay → receipt flow. Requires Bandit ({:bandit, "~> 1.10", only: :dev} when using mpp as a dependency).

Continuous Integration

GitHub Actions workflows (Elixir/OTP pinned via .tool-versions, so CI never drifts from local mix format):

  • CI (.github/workflows/ci.yml) — runs on every push/PR to development and main: format check, --warnings-as-errors compile, Credo strict, Doctor, Sobelow, tests with a 95% coverage gate, and Dialyzer. Mirrors mix precommit.full.

  • Integration (.github/workflows/integration.yml) — runs the credential-gated :integration suite nightly (and on PR / manual dispatch). These live round-trips catch the bug class unit tests are blind to (wrong gas limit, wrong request shape, on-chain accounting drift). It requires the following repo secrets — when any are absent the suite flunks loudly rather than reporting a green 0-test run:

    Secret Purpose
    TEMPO_RPC_URL Moderato testnet RPC (https://rpc.moderato.tempo.xyz)
    STRIPE_SECRET_KEY Stripe test-mode secret key (sk_test_…)
    ETH_SEPOLIA_RPC_URL / ETH_SEPOLIA_PRIVATE_KEY Sepolia RPC + funded key
    EVM_RPC_URL / EVM_PRIVATE_KEY Generic EVM RPC + funded key (falls back to Sepolia)
  • Mutation security (.github/workflows/mutation-security.yml) — nightly and workflow_dispatch only (not on PRs). Runs mix mutation.security, which applies each payment-security mutant, compiles it, and runs its tests. A surviving canary fails the job. Kept out of mix ci / mix precommit.full.

A further workflow, Code Scanning (.github/workflows/code-scanning.yml), uploads Sobelow findings to the Security → Code scanning tab (CodeQL has no Elixir support). Security vulnerabilities should be reported privately — see SECURITY.md.

References

License

MIT — see LICENSE for details.

About

Elixir implementation of the Machine Payments Protocol (MPP) — HTTP 402 payment middleware for AI agents and machine-to-machine commerce

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages