Packages

Elixir implementation of the Machine Payments Protocol (MPP) — HTTP 402 payment middleware for AI agents and machine-to-machine commerce. Supports Stripe, Tempo, generic EVM, Solana, Stellar, XRPL, and NEAR Intents payment methods with pluggable architecture.

Current section

Files

Jump to
mpp README.md
Raw

README.md

# MPP

[![GitHub](https://img.shields.io/github/license/ZenHive/mpp)](https://github.com/ZenHive/mpp/blob/main/LICENSE)

Elixir implementation of the [Machine Payments Protocol](https://mpp.dev) (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](https://stripe.com/blog/machine-payments-protocol) and [Tempo Labs](https://tempo.xyz). 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:

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :paid do
    plug MPP.Plug,
      secret_key: "a-random-secret-of-at-least-32-bytes",
      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)

```elixir
pipeline :paid_tempo do
  plug MPP.Plug,
    secret_key: "a-random-secret-of-at-least-32-bytes",
    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
    }
end
```

Tempo and EVM charge routes accept `currencies: [token_a, token_b]` instead of
`currency:` to offer an ordered list. The amount is in each token's base units;
use separate method entries for different prices. Duplicate token addresses
are removed case-insensitively, preserving the first spelling and offer order.
Tempo routes with neither option offer OUSD then USDC.e on mainnet (4217), or
OUSD then pathUSD on Moderato (42431, the default chain). Other Tempo chains
require an explicit currency. EVM routes require `currency:` or `currencies:`.

Tempo charge receipts expose `funding_currency` (wire `fundingCurrency`) when
the funding token can be identified: the charge token for a direct single-call
payment, or the machine token for its supported swap route. Other multi-call
routes omit it.

### EVM (Ethereum, Base, Polygon, etc.)

```elixir
pipeline :paid_evm do
  plug MPP.Plug,
    secret_key: "a-random-secret-of-at-least-32-bytes",
    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. Enable Permit2 with `"permit2" => true` and `"private_key"`: the client signs an off-chain witness authorization (`MPP.Methods.EVM.Permit2.sign/5`) and the server submits, paying gas. Set `"transaction" => true` to accept and advertise `type="transaction"` for split-free ERC-20 charges: the client signs a complete EIP-1559 `transfer` (and pays its gas), the server validates chain/token/recipient/amount plus the challenge expiry, broadcasts it, and requires a matching `Transfer` event. Optional `"splits"` add ordered extra recipient/amount legs (Permit2 only). See [EVM Permit2 credentials](https://github.com/ZenHive/mpp/blob/main/docs/evm-permit2.md) for the full configuration and signing walkthrough.

The transaction flag gates acceptance and advertisement together. [draft-evm-charge-00 § Credential Type Negotiation](https://github.com/tempoxyz/mpp-specs/blob/main/specs/methods/evm/draft-evm-charge-00.md#credential-type-negotiation) requires accepting transactions by default only when `credentialTypes` is omitted. EVM challenges always populate that list; without `"transaction" => true`, transaction credentials are rejected before decoding or RPC calls.

#### Custom EIP-3009 settlement

Use the server-only `"settle_authorization" => {callback, configuration}` option
instead of an MPP settlement key. Both options advertise the same authorization
credential; without a callback, direct token submission is unchanged. Neither
the callback nor its configuration is serialized into the challenge.

```elixir
method_config = %{
  "rpc_url" => sepolia_rpc_url,
  "chain_id" => 11_155_111,
  "settle_authorization" => {&MyRelayer.settle/2, relayer_configuration}
}
```

The callback takes `%{authorization: parsed, currency: token, chain_id: chain}`
and the caller's opaque configuration. `parsed` contains `from`, `to`, `value`
(decimal string), `valid_after`, `valid_before` (Unix seconds), `nonce` and
`signature` (hex). It returns `{:ok, transaction_hash}` for a **mined** transaction
or `{:error, reason}`. It owns signing, gas payment, transaction nonce coordination,
broadcasting and waiting for confirmation. It may call an external relayer;
MPP needs no private key. Callback errors return a generic settlement error;
exceptions propagate. Neither path falls back to direct submission.

MPP validates signature/domain/chain, recipient, exact amount, challenge nonce,
validity and optional credential source before calling it, then checks the RPC
chain and unused authorization state. A callback is trusted executable server
configuration, but its success is not payment evidence: MPP independently fetches
the receipt, requires success and the token's matching `AuthorizationUsed` event,
checks the exact payer/token/recipient/amount `Transfer`, and atomically consumes
the transaction in its replay store. A pending, reverted, or unrelated hash grants
nothing. A reverted receipt is a settlement failure, as with direct submission, and
can leave the authorization unused. A log marked `removed` does not match; a log
that omits `removed` still matches.
Do not disable the replay store; multi-node deployments must share an atomic store.

Concurrent requests can invoke the callback more than once. Deduplicate relayer
work by `(chain, token, from, nonce)` and coordinate the relayer account's transaction
nonce. Submit the supplied EIP-3009 authorization, never replace it with an ordinary
transfer. The token's nonce rule prevents a second payment; MPP's atomic store
prevents two receipts. A timeout or lost response can follow a successful payment:
retain the transaction reference and reconcile it in the consuming application.
MPP does not automatically retry or recover a consumed authorization.

The runnable [consumer callback](examples/eip3009_push_split.exs) uses Sepolia
Multicall3 `aggregate3`: payment has `allowFailure=false`, distribution has
`allowFailure=true`. It verifies the deployed PushSplit clone/implementation,
owner-zero state, fixed recipients/allocations and zero distributor incentive.
Those are consumer policy, not MPP policy. The split address is the charge recipient;
do not set the EVM `"splits"` option (that option belongs to Permit2).
A receipt proves full payment to that split, **not distribution completion**.
Distribution failures leave funds at the split and can be retried independently,
without resubmitting payment. The example uses explicit distribution amounts;
allocation rounding can leave dust for allocations that do not divide the amount.

Run the example with the live integration driver (four small testnet transactions):

```sh
export ETH_SEPOLIA_RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"
export ETH_SEPOLIA_PRIVATE_KEY="0x<funded-testnet-key>"
export EVM_SETTLEMENT_EVIDENCE_DIR=".harness/evidence/custom-settlement"
mix test test/mpp/methods/evm_custom_settlement_integration_test.exs --include integration
```

Fund gas with [Sepolia ETH](https://www.alchemy.com/faucets/ethereum-sepolia) and
[Circle testnet USDC](https://faucet.circle.com/). The driver uses the deployed,
immutable test split `0x8889c332727d5f3865526391bfb124cfab74c05f`, checks recipient
balance deltas, deliberately supplies invalid distribution parameters, retries
only distribution, and proves a reverted payment earns no receipt. Missing
credentials fail. Evidence includes chain/block, transactions, runtime hashes,
proxy implementations, configuration and source fingerprints for reviewer binding
to the delivery commit. No deployed-bytecode audit coverage is claimed.

Authority: [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009),
[Circle EIP3009 source](https://github.com/circlefin/stablecoin-evm/blob/master/contracts/v2/EIP3009.sol),
[PushSplit](https://github.com/0xSplits/splits-contracts-monorepo/blob/main/packages/splits-v2/src/splitters/push/PushSplit.sol),
[official Sepolia deployments](https://github.com/0xSplits/splits-contracts-monorepo/blob/main/packages/splits-v2/deployments/11155111.json),
and [Multicall3](https://github.com/mds1/multicall/blob/main/src/Multicall3.sol).
[mppx at c0ce0fe](https://github.com/wevm/mppx/blob/c0ce0fecccf29f0dab8457c4356c0a3838af3390/src/evm/server/Charge.ts)
is compatibility precedent, not the payment oracle.

### Solana (SOL and SPL tokens)

```elixir
pipeline :paid_solana do
  plug MPP.Plug,
    secret_key: "a-random-secret-of-at-least-32-bytes",
    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 and is off by default: enable it with `"push" => "challenge_memo"` (the transfer must carry the Memo `MPP.Methods.Solana.push_memo(challenge.id)`, binding it to one challenge) or `"push" => "unbound"` (the spec base flow, which binds no challenge). 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"`.

### USDC (Circle native USDC)

`MPP.Methods.USDC` is the `usdc` charge method from `draft-usdc-charge-00`.
`"profile"` selects the direct leg. EVM settles EIP-3009 through
`MPP.Methods.EVM.Authorization` and binds the nonce to the USDC challenge,
not the generic EVM `challengeHash`. Solana settles a legacy SPL
`transferChecked` through `MPP.Methods.Solana`. `currency` must be the
Circle-published USDC address or mint for that chain.

```elixir
pipeline :paid_usdc do
  plug MPP.Plug,
    secret_key: "a-random-secret-of-at-least-32-bytes",
    realm: "api.example.com",
    method: MPP.Methods.USDC,
    amount: "1000000",
    currency: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
    recipient: "0xrecipient",
    method_config: %{
      "profile" => "evm",
      "rpc_url" => "https://ethereum-sepolia-rpc.publicnode.com",
      "chain_id" => 11_155_111,
      "private_key" => "0x..."
    }
end
```

### Stellar (SEP-41 tokens)

`MPP.Methods.Stellar` verifies one-time SEP-41 token transfers on Stellar.
Pull mode (`type="transaction"`) accepts a base64 TransactionEnvelope XDR and
submits it via Soroban RPC. Push mode (`type="hash"`) fetches a confirmed hash
with `getTransaction`. Set `"feePayer" => true` with `"fee_payer_secret"` to
sponsor fees: the client signs authorization entries against the all-zeros
source account and the server rebuilds, signs and submits.

```elixir
pipeline :paid_stellar do
  plug MPP.Plug,
    secret_key: "a-random-secret-of-at-least-32-bytes",
    realm: "api.example.com",
    method: MPP.Methods.Stellar,
    amount: "10000000",
    currency: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
    recipient: "G...",
    method_config: %{
      "rpc_url" => "https://soroban-testnet.stellar.org",
      "network" => "stellar:testnet"
    }
end
```

Currency is the C-prefixed SEP-41 contract address (native XLM SAC on testnet
shown above). `network` is the CAIP-2 identifier `stellar:pubnet` or
`stellar:testnet`.

### XRPL (XRP, issued currencies and MPTs)

`MPP.Methods.XRPL` supports signed-blob and transaction-hash charge credentials,
validated-ledger settlement, exact amounts and challenge-bound InvoiceID attribution.
It requires an explicitly configured atomic, durable shared store. See
[XRPL configuration and live test setup](https://github.com/ZenHive/mpp/blob/main/docs/xrpl-charge.md).

`MPP.Methods.XRPL.Session` maps the session intent onto XRP payment channels:
open submits `PaymentChannelCreate`, voucher and close are off-ledger claims
over a cumulative drop total. The server retains the highest verified claim;
close redeems it with `PaymentChannelClaim` unless redemption is deferred.
See [XRPL session](https://github.com/ZenHive/mpp/blob/main/docs/xrpl-session.md) for the destination signing seed and
deferred-redemption configuration.

Session methods built on `MPP.Session.Method` must verify channel funding on the
server: `open` and `topUp` take the `:verify_open` / `:verify_top_up` callbacks
(or the server-only `"verify_open"` / `"verify_top_up"` method-config keys),
which return the confirmed `%{deposit, settled, close_requested, finalized}`
state. Without them both actions fail closed. Every `voucher`, and every
metered WebSocket deduction, also re-reads the escrow through the required
`:verify_channel_state` callback (or the `"verify_channel_state"` key), called
with the stored channel and returning the same state map: a read error, a
pending close request, a finalized or unfunded channel stops acceptance and
delivery, and a detected close is recorded on the stored channel. `close` likewise requires the
`:settle_close` callback (or the `"settle_close"` key), which submits the escrow
close and returns the mined `tx_hash`; the channel closes only after it
succeeds. A `close` credential must carry `closeSignature`, a separate EIP-712
`CloseAuthorization(bytes32 channelId,uint96 cumulativeAmount)` signature by the
channel payer or its authorized signer. See `MPP.Session.Actions`.

### 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`.

```elixir
{: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: "a-random-secret-of-at-least-32-bytes",
  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`, and should also implement the optional token-checked `delete/2` so a pre-broadcast failure releases only that attempt's reserved slot instead of burning the signed transaction until its TTL expires. 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.

### x402 interoperability

`MPP.Plug` accepts x402 v2 `exact` payments (EVM EIP-3009) next to native
Payment-auth on the same endpoint: pass `:x402` with a facilitator URL or client
and the plug verifies and settles `PAYMENT-SIGNATURE` credentials through it.
On the client, `MPP.X402` reads `PAYMENT-REQUIRED` offers as challenges and
`MPP.Client.Providers.X402Exact` signs them. See
[x402 interoperability](https://github.com/ZenHive/mpp/blob/main/docs/x402-interoperability.md).

### Multi-Method (Stripe + Tempo)

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

```elixir
pipeline :paid_multi do
  plug MPP.Plug,
    secret_key: "a-random-secret-of-at-least-32-bytes",
    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 the receipt in `conn.assigns[:mpp_receipt]`. When a successful (2xx) response is sent, MPP attaches `Payment-Receipt` and merges `private` into the response’s `Cache-Control` directives.

For endpoints that also require ordinary authentication, set `requires_auth: true`
on `MPP.Plug`. Challenges advertise `header="Payment-Authorization"`, and the
Plug accepts payment credentials only in that field, leaving `Authorization`
for application authentication. `MPP.Client.Req` honors the advertised field
and preserves an existing Bearer credential in `Authorization`.

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. Stripe activation allows one live subscription per payment method and plan; an activation whose Stripe outcome is uncertain stays blocked until an operator uses `inspect_activation/2` and `resolve_activation/3`. `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.

Tempo subscription store, broadcast and settlement-lookup failures return
`internal-payment-error` (HTTP 500). An uncertain broadcast or a settled payment
whose finalization failed retains its claim and requires reconciliation before
another charge.

## 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 |
| XRPL | MPP | XRP, issued currencies and MPTs | Implemented |
| 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), challenge-bound attribution memos, zero-amount proof credentials, delegated access-key proof authorization, opt-in presenter-identity binding for hash/transaction credentials, first-party machine-token (MACH and 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. Tempo payments must use challenge-bound attribution metadata; a static `"memo"` in `method_config` is rejected at startup, and plain transfers are rejected by the 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](https://docs.tempo.xyz/quickstart/connection-details#mainnet) (chain ID `4217`, `rpc.tempo.xyz`) | [Testnet (Moderato)](https://docs.tempo.xyz/quickstart/connection-details#testnet) (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-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.EVM.Permit2` | Permit2 witness credentials, gas-sponsored settlement, and ordered splits |
| `MPP.Methods.EVM.Transaction` | Client-signed EIP-1559 ERC-20 transfer validated against the charge, then server-broadcast |
| `MPP.Methods.Solana` | Solana native SOL and SPL token charge verification via `onchain_solana` |
| `MPP.Methods.XRPL` | XRPL signed-blob and hash charge verification via JSON-RPC |
| `MPP.Methods.XRPL.Session` | XRPL payment-channel session verification (open / voucher / close) |
| `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, parameters, response schemas; `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`, session `needVoucher` metering, JSON-RPC `message` frames |
| `MPP.Methods.USDC` | Direct Circle USDC charge (`draft-usdc-charge-00`): EVM EIP-3009 and Solana SPL profiles |
| `MPP.X402` | x402 v2 `exact` client interop: `PAYMENT-REQUIRED` offers as challenges |
| `MPP.X402.Plug` | Server-side x402 settlement through a configurable facilitator (`MPP.Plug` `:x402`) |
| `MPP.X402.Facilitator` | x402 facilitator client (`/verify`, `/settle`) |
| `MPP.Client.Providers.X402Exact` | Client provider signing x402 exact EIP-3009 payments |
| `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`/`needVoucher` frames, `Payment` credential frames, retry/backoff |
| `MPP.Client.MCP` | Payment-aware MCP client — select, approve, pay; at most two payment attempts (`-32042` plus one `-32043` re-challenge) |
| `MPP.Client.AcceptPolicy` | Gates `Accept-Payment` header injection on outgoing requests |

## Client

```elixir
provider =
  MPP.Client.MultiProvider.new([
    {MPP.Client.Providers.Tempo,
     %{
       private_key: tempo_private_key,
       rpc_url: "https://rpc.tempo.xyz",
       expected_chain_id: 4217,
       expected_recipients: ["0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00"],
       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 always creates
the challenge-bound attribution memo. A `memo` advertised by the server is ignored.
Machine-token payments use MACH by default; set `machine_token_deployment: :machine_usd`
for a server that accepts only machineUSD. The advertised `machineTokenEnabled`
flag does not distinguish deployments.
When `:expected_recipients` is set, the provider refuses a challenge whose primary
`recipient` or any `splits` recipient is outside that allowlist — before signing a
transaction or a zero-amount proof. Address comparison is checksum-agnostic. Omit
the option to keep the previous unrestricted-recipient behaviour.
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.

```elixir
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`
(or a `-32043` that still carries challenges), selects a challenge, asks
`on_payment_required` for approval on every payment, and retries with the
credential at `params._meta["org.paymentauth/credential"]`. Payment attempts
are bounded at two (initial `-32042` plus at most one `-32043` re-challenge) —
a deliberate divergence from mppx `maxPaymentAttempts = 3` so a malicious
server cannot drain a wallet through repeated re-challenges. A `-32043`
without challenges is returned immediately.

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).

For a hand-written MCP server, use `MPP.Mcp.payment_required_error/1` to issue
challenges without a problem (`-32042`), or `MPP.Mcp.verification_failed_error/2`
when you have an `MPP.Errors` problem. Despite its historical name, the latter
uses the problem title as its message and `MPP.Mcp.error_code/1` for its code:
`payment_required` / `sponsor_capacity_exhausted` → `-32042`,
`malformed_credential` / `invalid_payload` → `-32602`,
`internal_payment_error` → `-32603`, all other problems → `-32043`.

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. Session-intent sockets can meter a
generator: deduct per tick, emit `needVoucher` when the channel is exhausted,
resume after a voucher credential, and finish with a session receipt.
`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, no second payment after a drop that left a credential
unacknowledged, and a fatal second `needVoucher` while a voucher is in flight.

## Installation

```elixir
def deps do
  [
    {:mpp, "~> 0.21.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.

### Native hashing dependency

Keccak hashing uses `Onchain.Hash`, backed by the Rust-based `ex_keccak` NIF.
On supported platforms, the build downloads a precompiled binary from GitHub;
you do not need Rust or Cargo installed. Offline builds need the matching
binary cached in advance (Rustler Precompiled supports
`RUSTLER_PRECOMPILED_GLOBAL_CACHE_PATH`).

For targets without a precompiled binary, or to build from source, install
Rust/Cargo and add `:rustler` to your application's dependencies, then set
`config :rustler_precompiled, :force_build, ex_keccak: true`. See the
[ex_keccak build instructions](https://hexdocs.pm/ex_keccak/0.7.8/ExKeccak.html#module-force-compilation).
This dependency change does not alter MPP's public API or payment wire formats.

## Live Example

[Strip0x](https://strip0x.com) — blockchain tools API using MPP with Tempo payments. $0.0001 per paid request (100 base units USDC.e on Tempo mainnet).

```bash
# 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](https://github.com/ZenHive/mpp/issues) 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).

## Project checks

Run `mix deps.get`, then `mix ci` for the full project gate. `mix check.dispatch`
is format-check plus compile-with-warnings-as-errors; reviewers add focused tests.
Elixir/OTP versions are pinned in `.tool-versions`. The full gate runs formatting,
compilation with warnings as errors, Credo, Doctor, tests with 95% coverage,
Sobelow, clone and architecture checks, Dialyzer, and dependency auditing.
Host tooling also checks `AGENTS.md` freshness and advisory-mirror freshness.

Live provider tests run separately with `mix test --include integration`.
Each integration test module documents its credentials and RPC configuration;
missing required credentials fail loudly. Cross-validation tests run with
`mix test --include cross_validation`.

`mix mutation.security` applies payment-security mutants and runs their tests;
a surviving canary fails the command. It runs separately from `mix ci`.
GitHub Actions workflows have been removed; `mix ci` is the project gate.
Security vulnerabilities should be reported privately — see [SECURITY.md](SECURITY.md).

## References

- [MPP Specification](https://github.com/tempoxyz/mpp-specs) — IETF draft, core protocol
- [x402 Documentation](https://docs.x402.org) — On-chain payment standard
- [Stripe MPP Announcement](https://stripe.com/blog/machine-payments-protocol) — Stripe's agent commerce vision
- [mpp.dev](https://mpp.dev) — Protocol overview and SDK links

## License

MIT — see [LICENSE](LICENSE) for details.