This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
@/.claude/includes/critical-rules.md
@/.claude/includes/harness-workflow.md
@/.claude/includes/task-prioritization.md
@/.claude/includes/task-writing.md
@/.claude/includes/rmap.md
@/.claude/includes/web-command.md
@/.claude/includes/code-style.md
@/.claude/includes/development-philosophy.md
@/.claude/includes/development-commands.md
@/.claude/includes/ex-unit-json.md
@/.claude/includes/dialyzer-json.md
@/.claude/includes/workflow-philosophy.md
@/.claude/includes/elixir-volt.md
@/.claude/includes/quickbeam.md
@/.claude/includes/oxc.md
@/.claude/includes/ethereum-rpc.md
@~/.claude/includes/upstream-pr-workflow.md
MPP (Machine Payments Protocol) — Elixir library implementing HTTP 402 payment middleware for AI agents and machine-to-machine commerce. Built on the MPP spec co-developed by Stripe and Tempo Labs.
Repo: ZenHive/mpp | Org: ZenHive
Core idea: payment is authentication. No user accounts, no API keys. A client hits an endpoint, gets a 402 challenge with price + payment method, pays, and retries with an Authorization: Payment credential.
mix test.json # tests (AI-friendly JSON output)
mix test.json --failed # re-run only failures
mix test path/to/file.exs # single test file
mix test path/to/file.exs:42 # single test at line
mix dialyzer.json # type checking (AI-friendly output)
mix credo --strict --format json # static analysis
mix sobelow # security scanner
mix doctor # docs/specs coverage
mix mpp.demo # start demo server on port 4402 (--port to override)
mix format # auto-format (Styler runs as plugin)
mix docs # generate ExDocFor cross-family reviewers (codex / cursor / grok) who don't inherit this repo's Claude Code hooks or skills:
- Canonical gate:
mix precommit.full— runs format-check, compile (warnings-as-errors), credo--strict, doctor, the test+cover gate (95% — MPP is critical-tier: money, signing, wire-format encoding), sobelow, and dialyzer.mix precommitis the same minus dialyzer;mix check.fastis the seconds-long inner-loop (format + compile + credo). mix test.json(ex_unit_json) andmix dialyzer.json(dialyzer_json) emit JSON by design — parse it for real failures (summary.result,coverage.threshold_met,warnings[]); never flag the JSON envelope itself as a build failure. A non-empty JSON document on stdout is a successful run, not an error.- When
dialyzer.json's encoder can't serialize a warning shape, plainmix dialyzeris the authoritative dialyzer check. - Integration tests (
:integrationtag) are excluded from the gate — they need live testnet/Stripe credentials. Run explicitly withmix test.json --include integration.
This is a library (not a Phoenix app). It provides Plug middleware that any Phoenix or Plug app can mount.
- Request hits a protected resource
- Server responds
402 Payment RequiredwithWWW-Authenticate: Paymentheader containing a challenge (price, accepted payment methods) - Client fulfills payment off-band (Stripe charge, on-chain tx, etc.)
- Client retries with
Authorization: Payment <credential>header - Server verifies payment, returns resource with
Payment-Receiptheader
MPP — Root module, Discoverable entry point (describe/0-2 for progressive API discovery)
MPP.Challenge — Challenge struct, HMAC-SHA256 ID binding, create/verify
MPP.Credential — Credential parsing, challenge echo validation, payload extraction
MPP.Receipt — Receipt struct, base64url JSON serialization
MPP.Headers — Parse/format WWW-Authenticate, Authorization, Payment-Receipt
MPP.Errors — RFC 9457 problem types (paymentauth.org/problems/*), includes session error types
MPP.Intents.Charge — Charge intent request schema (amount, currency, recipient, ...)
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.JCS — RFC 8785 JSON Canonicalization Scheme (MPP subset: ASCII keys, no floats) for cross-SDK HMAC interop
MPP.Verifier — Transport-neutral verification pipeline (HMAC, realm, expiry, request match, method.verify)
MPP.Method — Behaviour for pluggable payment methods (verify/2)
MPP.Methods.Stripe — Stripe SPT → PaymentIntent verification (Req, no Stripe SDK)
MPP.Methods.Tempo — Tempo on-chain TIP-20 transfer verification (delegates chain ops to onchain_tempo)
MPP.Methods.EVM — Generic EVM on-chain transfer verification (any chain: Ethereum, Base, Polygon, etc.)
MPP.Methods.Tempo.SessionReceipt — Session-intent receipt for Tempo (to_header/from_header, camelCase wire keys)
MPP.Tempo.Store — Behaviour for optional tx dedup stores (get/put + optional atomic check_and_mark)
MPP.Tempo.ConCacheStore — Built-in ETS dedup store with TTL via ConCache (optional dep)
MPP.Plug — HTTP Plug middleware, delegates verification to MPP.Verifier
MPP.Plug.MethodEntry — Per-method config within a multi-method endpoint (method, charge, request, method_config)
MPP.Plug.Config — Validated endpoint config struct (shared settings + list of MethodEntry structs)
MPP.Mcp — MCP (JSON-RPC) transport: constants (-32042/-32043, meta keys), server/client helpers
MPP.Client.PaymentProvider — Behaviour for client-side payment providers (supports?/3, pay/2)
MPP.Client.MultiProvider — Multi-provider dispatch: wraps [{module, config}], routes to first match
MPP.Client.Transport — Transport behaviour: payment_required?/1, get_challenges/1, set_credential/2 + select_challenge/2 helper
MPP.Client.Transport.HTTP — HTTP transport over Req: 402 detection, WWW-Authenticate parsing, Authorization: Payment attach
MPP.Demo.Method — Toy payment method accepting "demo-token" (for mix mpp.demo)
MPP.Demo.Router — Plug.Router demo server with protected /resource endpoint
- Stateless HMAC-bound challenges. Challenge ID =
base64url(HMAC-SHA256(secret, realm|method|intent|request|expires|digest|opaque)). No challenge store needed — the server recomputes and does constant-time comparison on verification. - Intent = Schema, Method = Implementation.
MPP.Intents.Chargedefines the shared request schema (amount, currency, recipient).MPP.Methodimplementations only handle verification. All methods share the same intent structs. - Explicit credentials. Per
library-design.md: noApplication.get_env, no ENV fallback. Passsecret_key,realm,methodmodule, and pricing explicitly via Plug opts. - Per-route pricing via Plug opts. Each route mounts
MPP.Plugwith its own amount/currency. No global pricing config. - Base64url encoding preserves original bytes. Critical for HMAC verification — never re-serialize, always use the raw base64url string from the original challenge.
- Server-only method_config.
MPP.Plugaccepts:method_config(a map) for secrets likestripe_secret_key. Public fields go to the client viachallenge_method_details/1; private fields are merged intocharge.method_detailsat verify time only, never serialized into challenges.
| Constant | Value |
|---|---|
| Auth scheme | Payment |
| Challenge header | WWW-Authenticate |
| Credential header | Authorization |
| Receipt header | Payment-Receipt |
| Problem base URI | https://paymentauth.org/problems/ |
| HMAC algorithm | HMAC-SHA256 |
| HMAC input separator | | (pipe) |
| Encoding | base64url (no padding) |
| Network | Chain ID | RPC URL | Docs |
|---|---|---|---|
| Tempo Mainnet | 4217 |
https://rpc.tempo.xyz |
connection-details#mainnet |
| Tempo Testnet (Moderato) | 42431 |
https://rpc.moderato.tempo.xyz |
connection-details#testnet |
Our code defaults to 42431 (Moderato testnet) — see @moderato_chain_id in MPP.Methods.Tempo. README examples use 4217 (mainnet).
plug— HTTP middleware framework (the integration surface)jason— JSON encoding/decoding for challenge/receipt payloadsreq— HTTP client for payment method API calls (Stripe, etc.)descripex— Self-describing API metadata (api()macro,Discoverable)onchain— Ethereum RPC, address validation, and ERC-20 transfer parsingonchain_tempo— Tempo chain primitives: 0x76 transaction handling, TIP-20 calldata, Tempo RPC, TransferWithMemo event parsingcon_cache— ETS-based TTL cache forMPP.Tempo.ConCacheStorededup store
Three tools for verifying our implementation against the mppx TypeScript reference impl (refs/mppx/). These are NEVER production dependencies. MPP is a library — consumers must not pull in JS runtimes.
| Question type | Tool | Example |
|---|---|---|
| Understand logic/flow of one file | Read | "How does mppx's auth-param parser handle escapes?" |
| Structural query across files | OXC | "What functions does mppx export?" / "Who imports Challenge?" |
| Extract schemas/types to compare against our Elixir structs | OXC | "Do our Receipt fields match mppx's?" |
| Compliance check (do our error types match?) | OXC | Extract all mppx error URIs, compare against MPP.Errors |
| Verify runtime behavior matches | QuickBEAM | "Does mppx's HMAC produce the same output as ours for this input?" |
| Load ox/tempo for runtime cross-validation | esbuild + QuickBEAM | MPP.Test.OxTempoBundle.load!(rt) -- see below |
| Small file (<150 lines) | Read | Receipt.ts is 131 lines -- OXC adds overhead for no benefit |
OXC's bundler can't produce clean IIFEs for packages with mixed ESM/CJS deps (like ox with @noble/*). Use esbuild instead:
# In tests -- OxTempoBundle handles bundling + caching automatically
{:ok, rt} = QuickBEAM.start(apis: :browser)
MPP.Test.OxTempoBundle.load!(rt)
{:ok, result} = QuickBEAM.call(rt, "TxET.deserialize", ["0x76..."])How it works:
test/support/ox_tempo_entry.mjs-- thin entry importingdeserialize/serializefrom ox/tempotest/support/ox_tempo_bundle.ex-- shells out tonpx esbuildwith--format=iife --platform=browser- Bundle cached to
_build/test/ox_tempo_bundle.js, rebuilt when entry or ox version changes - esbuild resolves all deps via ESM export conditions -- no scope collisions in QuickJS
OXC excels at: cross-file function inventories (OXC.collect across all src/*.ts), import graph analysis (OXC.imports/2), schema field extraction from Zod objects, finding which functions use specific APIs (Base64, Hash, etc.).
OXC struggles with: complex AST node types your collection logic doesn't handle (SpreadElement, ConditionalExpression in object literals). When the JS uses patterns beyond simple properties, the collector crashes. Read doesn't have this problem.
OXC comparison scripts need domain awareness: OXC extracts mppx data perfectly, but comparing against our Elixir code requires understanding how we structure things (e.g., @base_uri <> suffix vs literal URI strings). Naive String.contains? misses these patterns.
# Parse a file
{:ok, ast} = OXC.parse(File.read!("refs/mppx/src/Challenge.ts"), "Challenge.ts")
# Collect exported functions with arities
OXC.collect(ast, fn
%{type: "ExportNamedDeclaration", declaration: %{type: "FunctionDeclaration", id: %{name: name}, params: params}} ->
{:keep, {name, length(params)}}
_ -> :skip
end)
# Extract z.object schema fields with required/optional
OXC.collect(ast, fn
%{type: "CallExpression", callee: %{property: %{name: "object"}}, arguments: [%{type: "ObjectExpression", properties: props}]} ->
fields = Enum.map(props, fn p ->
key = Map.get(p.key, :name) || Map.get(p.key, :value)
optional? = match?(%{callee: %{property: %{name: "optional"}}}, p.value)
{key, if(optional?, do: :optional, else: :required)}
end)
{:keep, fields}
_ -> :skip
end)
# Import graph (fast, no full parse)
{:ok, imports} = OXC.imports(File.read!("refs/mppx/src/Credential.ts"), "Credential.ts")
# => ["ox", "./Challenge.js", "./PaymentRequest.js"]
# Cross-file: find which functions touch Base64
for file <- ~w[Challenge.ts Credential.ts Receipt.ts] do
source = File.read!("refs/mppx/src/#{file}")
{:ok, ast} = OXC.parse(source, file)
fns = OXC.collect(ast, fn
%{type: "FunctionDeclaration", id: %{name: name}, body: body} ->
if String.contains?(String.slice(source, body.start..body.end), "Base64"),
do: {:keep, name}, else: :skip
_ -> :skip
end)
if fns != [], do: IO.puts("#{file}: #{Enum.join(fns, ", ")}")
endRun scripts with: MIX_ENV=dev mix run /tmp/script.exs
Explore freely. These patterns are starting points — try your own OXC queries against refs/mppx/ to discover what works best for your specific question.
api_cache is the first consumer — Phase 7, Tasks 47-51 in its roadmap. The Plug API must be mountable in a Phoenix router with per-route pricing. mpp has zero api_cache dependencies.
Three reference repos are cloned into refs/ (gitignored, auto-updated on session start via hook). Read these directly — do NOT WebFetch from GitHub.
refs/mpp-specs/ — IETF spec source (specs/, examples/)
refs/mppx/ — TypeScript SDK (primary reference). Key files in src/:
Challenge.ts, Credential.ts, Receipt.ts, Errors.ts,
Method.ts, PaymentRequest.ts
refs/mpp-rs/ — Rust SDK. Key files in src/: protocol/, client/, server/
Also available:
- IETF spec: https://paymentauth.org/
- Developer docs: https://mpp.dev/ (llms-full.txt for complete docs)
- SDK index: https://mpp.dev/sdk — lists four official SDKs (TypeScript
mppx, Pythonpympp, Rustmpp-rs, Gompp-go) plus community SDKs (Elixir/ZenHive, Go/cp0x-org) - Non-cloned SDKs (
pympp,mpp-go, communitycp0x-org/mppx) — fetch on demand viagh repo view/ MCP / WebFetch when cross-referencing - MCP server at
.mcp.json—mcp__mpp__*tools for cross-referencing SDK source code:search_source/read_source_file/get_file_tree— work for mppx, mpp-rs, pympp, tempolist_pages/search_docs— not functional (docs not indexed); use WebFetch for mpp.dev contentmpp-specssource — empty via MCP; use localrefs/mpp-specs/instead
The mpp.dev docs site (tempoxyz/mpp) lists SDKs at https://mpp.dev/sdk in two tables: Official (mppx, pympp, mpp-rs, mpp-go) and Community-Maintained (our Elixir mpp via ZenHive, plus Go mppx by cp0x-org). Community entries were added via upstream PR #502 on 2026-03-31. Our earlier PR #473 (richer per-SDK pages under /sdk/elixir) was closed in favor of the community-table approach. If upstream opens the door to per-SDK pages again, revive from the e-fu/mpp fork.
- Styler is the formatter plugin (runs automatically via
mix format) test/support/is compiled in test env (elixirc_paths)- Integration tests are mandatory. Every payment method feature that makes RPC or API calls MUST have integration tests against the real service (Moderato testnet, Stripe test API, etc.). Unit tests with stubs only prove internal consistency — they cannot catch wrong request shapes, unexpected responses, or protocol mismatches. The Task 13g
eth_callparams bug proved this: all stub tests passed, but Moderato rejected the request. Tagged:integration, run withmix test --include integration. - Spec source:
refs/mpp-specs/(local) or tempoxyz/mpp-specs - Reference impl:
refs/mppx/(local) or wevm/mppx (TypeScript) - Reference impl:
refs/mpp-rs/(local) or tempoxyz/mpp-rs (Rust)
Configured: 2026-03-25
Format: imperative-mood
<description>
Start with imperative verb: Add, Update, Fix, Remove, etc.