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, and NEAR Intents payment methods with pluggable architecture.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
mpp CHANGELOG.md
Raw

CHANGELOG.md

# Changelog
All notable changes to this project will be documented in this file.
Per-task history (acceptance criteria, scoring, decision notes) lives in `roadmap/tasks.toml` — query with `rmap show <id>` or read `roadmap/data.json`. This file is curated release notes only.
---
## [Unreleased]
## [0.11.0] — 2026-07-31
### Changed — CI runs `mix ci`, and the security audit can no longer pass vacuously
`.github/workflows/ci.yml` invokes the `mix ci` (= `mix precommit.full`) alias
instead of a hand-maintained step list, so CI and local dev share one gate
definition. Three properties of that gate needed shoring up before the shared
definition was trustworthy:
- **`deps.audit` runs last.** mix_audit signals a finding with `System.stop(1)`,
which is asynchronous — it returns, the alias proceeds, and the concurrent VM
shutdown truncates the next step. Exit status was always non-zero, but the log
showed an aborted dialyzer instead of the vulnerability report.
- **`deps.audit.gated` proves the mirror was populated.** `MixAudit.Repo` discards
its clone/pull exit status, so a failed sync yields zero advisories and a
passing report. The freshness prover catches that on the developer host but is
a host script that skips on CI — precisely where a rate-limited clone would
have produced a green "No vulnerabilities found." over nothing. The gate now
also rejects a `MIX_AUDIT_ADVISORY_PATH` that diverges from `MixAudit.Repo`'s
hardcoded path (the prover honours the variable; the audit does not), and fails
if `cowboy` enters `mix.lock` while `.mix_audit_ignore` still ignores
GHSA-w4f7-4cxr-rv3c — `--ignore-file` takes advisory IDs only, never a package
scope, and that advisory is genuine for cowboy `< 2.16.0`.
- **`reach.check` is pinned to `--path lib`.** Reach auto-discovers roots via
`*/lib` + `*/src` wildcards, which picks up gitignored sibling checkouts
(`mpp-docs-fork/src`) that don't exist on a runner. With `smells: [strict: true]`
now gating, an unpinned scope would have graded different file sets locally and
in CI.
Host-script skips also print via `IO.puts` rather than `Mix.shell().info/1`,
which `dialyzer.json --quiet` had silenced for the remainder of the run, and the
"is this runnable" guard is an executable-bit check rather than `File.exists?/1`
(true for directories and non-executable files, which raised an opaque
`ErlangError :enoent`).
### Changed — `{:descripex, "~> 0.11"}``{:descripex, "~> 0.12.0"}`
descripex 0.12.0 changed `short_name` in `describe/1` output from an atom to a
string — a consumer-visible contract change shipped at a *minor* bump, which the
old two-segment `~> 0.11` (`>= 0.11.0 and < 1.0.0`) would have absorbed on any
fresh resolution. The requirement is now three-segment (`< 0.13.0`); a 0.x
package that breaks on minor earns the tighter form, and the cap gets raised
deliberately after reading its release notes.
mpp does not read `short_name` — nothing in `lib/` or `test/` references it, and
the full suite (1061 tests) is green against descripex 0.12.0 with no code
change. The break is in the *bound*, not the behaviour. This release was already
a minor for other reasons, so the narrowing costs no extra version step.
### Changed — self-caps removed; `styler` 1.11.0 → 1.12.2
`{:styler, "~> 1.11.0"}` and `{:ex_ast, "~> 0.12.0"}` were three-segment
self-caps blocking their own minor lines for no reason beyond how the bounds
were written. Both dev/test only, so no effect on the published package. styler
now resolves 1.12.2; ex_ast resolves 0.12.10 because `reach 2.8.2` declares
`ex_ast ~> 0.12.0`. That transitive requirement is a choice rather than a wall —
`override: true` gets past it, as cartouche does — but the override is
unmeasured here: ex_ast 0.13.0 changed pattern-matching semantics (map patterns
became subset matching) and reach's smell checks are built on those patterns, so
it could quietly report fewer findings. `req` resolves to 0.7.2.
styler 1.12 rewrote 19 lines of time arithmetic, `DateTime.add(dt, n, :second)`
`DateTime.shift(dt, second: n)` (normalised to `minute:`/`hour:` where the
literal allowed). These are **not** unconditionally equivalent — `shift/2` is
calendar-aware and re-resolves the time zone, so on a DST zone it can differ
from absolute-time addition. Every call site in `MPP.Expires` and `MPP.Plug`
starts from `DateTime.utc_now/0`, and UTC has no DST, so the rewrite is safe
here. Worth stating explicitly because the *tests* were rewritten the same way,
which means a green suite alone would not have proven it.
### Changed — dependency floors raised so the req 0.7 lift actually lands
- `{:onchain, "~> 0.10"}``{:onchain, "~> 0.11"}` and
`{:onchain_tempo, "~> 0.7"}``{:onchain_tempo, "~> 0.8"}`. Those are the
releases that carry `cartouche ~> 0.6`, which is what lifts cartouche's
transitive `req < 0.7` cap. The previous two-segment bounds already
*permitted* the new versions but did not *require* them — and a lockfile entry
that still satisfies its bound is never re-resolved, so a consumer sitting on
onchain 0.10.0 would have gone on resolving cartouche 0.5.x, and therefore
req 0.6.x, through any number of `mix deps.get` runs. Raising the floors
invalidates the stale entries so the upgrade happens without anyone having to
know to run `mix deps.update`.
- `{:descripex, "~> 0.9"}``{:descripex, "~> 0.11"}`, matching what cartouche
0.6 already forces.
Resolves to onchain 0.11.0, onchain_tempo 0.8.0, cartouche 0.6.0,
descripex 0.11.0, req 0.7.1. Compiles `--warnings-as-errors` clean; 1061 offline
tests pass (2 doctests, 13 properties).
**Session intent schema.** Added `MPP.Intents.Session` — pay-as-you-go / metered session request schema parallel to `MPP.Intents.Charge`, with `new/1` validation, camelCase `to_request/1` / `from_request/1` matching mpp-rs `SessionRequest` (`unitType`, `suggestedDeposit`, `methodDetails`; transient `decimals` / `external_id` stripped from wire — mpp-rs session has no `externalId`). `MPP.Method` callbacks now accept `MPP.Method.intent()` (`Charge.t() | Session.t()`).
**MCP server transport adapter (Task 32b).** `MPP.Mcp` is now a mountable server-side transport, not just constants and helpers: `MPP.Mcp.init/1` accepts the same endpoint options as `MPP.Plug`, and `MPP.Mcp.call/3` runs a JSON-RPC request through payment verification before invoking the handler — reading the credential from `params._meta["org.paymentauth/credential"]`, emitting `-32042` payment-required / `-32602` malformed-credential / `-32043` verification-failed errors with challenges and RFC 9457 problem details (mppx `mcpErrorCode` parity), and attaching the receipt (+ `challengeId`) to `result._meta` on success. Challenge generation is shared with `MPP.Plug`, so both transports emit byte-identical challenges from the same config.
**Credential replay dedup shared across transports (`MPP.Replay`).** The plug-level credential single-use dedup (key `mpp:credential:<challenge-id>:<payload-hash>`, Tempo carve-out, atomic `check_and_mark/2` only — GHSA-w8j7-7qc3-5f24) moved to an internal `MPP.Replay` module used by both `MPP.Plug` and the MCP transport, so a verified MCP credential cannot be replayed across JSON-RPC requests for store-backed methods.
**Stripe Connect settlement options (Task 52).** `MPP.Methods.Stripe` accepts an optional server-only `"connect"` map in `method_config` — destination charges (`transfer_data` destination/amount), direct charges (`stripe_account``Stripe-Account` header), application-fee splits (`application_fee_amount`), plus `on_behalf_of` and `transfer_group` — validated against the charge amount before PaymentIntent creation and never serialized into the public 402 challenge, matching mppx (`validateConnectSettlement` / `createWithSecretKey`).
**Audit hardening on the above (dual-reviewer pass).** Stripe PaymentIntent requests now pin `Stripe-Version: 2026-02-25.preview` (required for SPT private preview; mppx `stripePreviewVersion` parity) and reject a non-string Connect `transfer_group` cleanly instead of raising at request time. The MCP transport treats non-map JSON-RPC `params` (arrays, explicit null) as carrying no credential instead of crashing; its client helpers `payment_required?/1` and `extract_challenges/1` now also accept the full JSON-RPC response envelope (mppx `paymentRequiredData` parity); and a replayed MCP credential emits the same `[:mpp, :verify, :start]`/`:fail` telemetry as the HTTP path. `MPP.Replay` rejects a credential whose payload the JCS subset cannot canonicalize (e.g. floats) as `malformed-credential` instead of leaking a `FunctionClauseError` — also fixing the pre-existing crash on the HTTP path for store-backed methods.
## [0.10.0] - 2026-07-08
**Internal refactor — shared-helper de-duplication and `MPP.Headers` split (no runtime behavior change; the Accept-Payment API move below is the one breaking surface).** Extracted copy-pasted internal helpers into three small modules: `MPP.Hex` (`strip_0x/1`, `hex_string?/1`), `MPP.Methods.Shared` (`require_config/3`, `check_receipt_status/1`, `parse_charge_amount/1`), and `MPP.Codec` (`decode_base64_json/1`), removing the duplication across the payment-method and wire-format modules. The 900-line `MPP.Headers` was split: the `Accept-Payment` parse/format/rank algorithm moved to its own `MPP.AcceptPayment` module, and the byte-level multi-scheme boundary state machine moved to an internal `MPP.Headers.SchemeSplitter`, leaving `MPP.Headers` focused on the challenge/credential/receipt wire format.
**Breaking (moved API) — `Accept-Payment` functions relocated to `MPP.AcceptPayment`.** The four public functions moved out of `MPP.Headers` and were renamed to drop the now-redundant suffix: `MPP.Headers.parse_accept_payment/1``MPP.AcceptPayment.parse/1`, `apply_accept_payment_header/3``apply_header/3`, `format_accept_payment/1``format/1`, `rank_by_accept_payment/2,3``rank/2,3`. Behavior is identical; the progressive-discovery key moves from `MPP.describe(:headers)` to `MPP.describe(:accept_payment)`. Update any direct callers to the new module.
**Parse-time input validation for challenges (reference-SDK parity).** `MPP.Headers.parse_challenge/1` and the echoed-challenge decode paths (`MPP.Credential.decode/1`, `MPP.Mcp`) now validate challenge field shapes at parse time instead of deferring to a downstream mismatch, via a shared `MPP.Challenge.validate_fields/1`: `id` must be non-empty, `method` must match the spec ABNF `payment-method-id = 1*LOWERALPHA` (lowercase ASCII letters only — following mpp-rs; mppx's looser `[a-z0-9:_-]` regex diverges from the spec), `request` must base64url-decode to a JSON object, and `digest` (when present) must start with `sha-256=`. The header/credential paths return distinct error atoms (`:empty_id`, `:invalid_method`, `:invalid_request`, `:invalid_digest`); the MCP transport keeps its coarse `:invalid_challenge`. `MPP.Verifier` now reports a malformed (non-ISO-8601) `expires` as a `credential_mismatch` distinct from an actual `payment_expired`, and `MPP.JCS.canonicalize/1` raises `ArgumentError` on a non-string map key per its RFC 8785 contract. Raw wire bytes are never altered — validation only inspects shape, so HMAC binding is unaffected.
Tempo dedup stores now support per-store key prefixes for shared-cache tenancy. Pass `key_prefix: "tenant:"` in `{MPP.Tempo.ConCacheStore, opts}` to namespace backing keys while preserving the no-prefix default.
**Audit hardening on the above (dual-reviewer pass).** `MPP.Credential.decode/1` now also rejects a non-string optional field (`description`/`digest`/`expires`/`opaque` in the echoed challenge, or the top-level `source`) with `:invalid_optional_field` — previously such a credential decoded successfully and crashed the verifier downstream (HMAC input join / ISO-8601 parse) on attacker-supplied wire bytes; mppx (`z.optional(z.string())`) and mpp-rs (serde `Option<String>`) both reject these shapes at deserialization. `MPP.Mcp`'s JCS pre-check now rejects non-string map keys in a native `request` so they surface as `:invalid_challenge` instead of leaking the new `MPP.JCS.canonicalize/1` raise. `MPP.Plug.init/1` raises at boot when a configured `method_name/0` falls outside the spec ABNF `1*LOWERALPHA` — such a config would emit challenges that no compliant client (including this library's own parse paths) can parse.
## [0.9.0] - 2026-07-08
**Security (fee-payer intrinsic-gas hardening — closes an open item in the published gas-drain family).** `MPP.Methods.Tempo.FeePayerPolicy` now rejects two non-canonical shapes in the client-signed `0x76` envelope before the server co-signs, matching mppx #602 (`fix-fee-payer-intrinsic-gas`): (1) any call carrying a nonzero native `value`, and (2) non-canonical calldata for the recognized TIP-20 / stablecoin-DEX calls (`transfer`, `approve`, `transferWithMemo`, `swapExactAmountOut`) — trailing padding or non-canonical high-order bytes that raise the transaction's *intrinsic* gas (16 gas per byte, paid by the sponsor) without changing the decoded payment intent. Both are siblings of the already-published gas-draining advisories (GHSA-vv77-66rf-pm86, GHSA-vj8p-hp9x-gh47, GHSA-qpxh-ff8m-c62v) that the existing price/access-list ceilings did not cover, and which pre-broadcast `eth_simulateV1` cannot catch (padded calldata still executes successfully, it just costs more). Safe-by-default: honest clients emit canonical, zero-value calls, so existing `fee_payer: true` deployments are protected without config changes. Unrecognized selectors are left to the separate call-scope gate. mpp-rs does not implement this check (its fee-payer path uses a different inline-envelope architecture).
## [0.8.0] - 2026-07-08
**Security (presenter-identity binding — closes the GHSA-34g7-vx6g-82mq residual).** New opt-in Tempo `method_config` key `"require_presenter_binding"`: when `true`, `type="hash"` and `type="transaction"` credentials must carry a `"presenterSignature"` — an EIP-712 signature over the same `Proof` typed data the `type="proof"` path uses (MPP domain v3, `{account, challengeId, realm}`) — produced by the transfer sender's wallet or one of its authorized access keys. This closes the advisory's documented residual: dedup on the hash/transaction paths is keyed on the tx hash alone, so a third party who observed a settled transfer could race its hash against their own fresh challenge; with binding enabled the presenter must prove control of the sender address (hash path: the credential's `source` DID is required and must match the matched transfer's `from`; transaction path: the account is the sender recovered from the signed 0x76 transaction, and a `source`, when present, must agree). The requirement is advertised to clients as `"presenterBinding": true` in the 402 challenge's method details, and a supplied `presenterSignature` is verified even when the flag is off. Off by default for interoperability: neither mpp-rs nor mppx binds the presenter on the hash path (both default the expected sender to the receipt's `from` — mpp-rs `verify_hash`, mppx `Charge.ts` hash branch), so this is a deliberate hardening extension beyond the reference SDKs.
## [0.7.0] - 2026-07-08
**Security (Accept-Payment DoS cap — mpp-rs #299 parity).** `MPP.Headers.parse_accept_payment/1` and `apply_accept_payment_header/3` now ignore an `Accept-Payment` header larger than 16 KiB (`@max_token_len`) before splitting, extending the credential/receipt/challenge-request token cap to the fourth client-supplied parse surface. Oversized headers are treated as malformed (spec MAY-ignore): parsing returns `[]` and offer ranking is a no-op.
**Security (replay protection on by default — issue #7).** Dedup stores are now **on by default** across all three layers (`MPP.Plug` credential store, `MPP.Methods.Tempo`, `MPP.Methods.EVM`). When no `:store` / `"store"` is configured, the app-started `MPP.Tempo.ConCacheStore` is used automatically, so a fresh install rejects replays out of the box instead of silently allowing them — matching the reference SDKs (mpp-rs `store: Some(MemoryStore::new())` in `server/tempo.rs`; mppx `Store.from(store ?? Store.memory())` in `tempo/server/Charge.ts`). The `:mpp` application is now started (via `mod: {MPP.Application, []}` in `mix.exs`) to supervise that default store.
**Security (atomic dedup contract required — GHSA-w8j7-7qc3-5f24 residual).** A configured store MUST now implement the atomic `check_and_mark/2`; `check_and_mark/2` is a required `MPP.Tempo.Store` callback and a store lacking it is rejected at `Plug.init` / `validate_config!` with an `ArgumentError`. The previous non-atomic `get/1`+`put/2` fallback — which left a TOCTOU replay window for custom get/put-only stores — has been removed. The built-in `ConCacheStore` (atomic via ConCache row isolation) and both reference SDKs (mpp-rs `put_if_absent` fails closed on `AtomicUnsupported`; mppx atomic `update`) already meet this contract.
**Breaking changes (0.7.0):**
- Replay protection defaults to ON. Deployments that relied on the previous stateless (no-dedup) default must set `store: false` (Plug opt) / `"store" => false` (method_config) to explicitly opt out. `store: nil`/absent now means "use the default store," not "no dedup."
- Custom stores implementing only `get/1` + `put/2` are rejected at init — add an atomic `check_and_mark/2`, or use `store: false` to disable dedup.
- The `:mpp` application now starts a supervised process (the default `ConCacheStore`). A static Tempo `"memo"` combined with `store: false` is rejected (a static memo still requires single-use enforcement).
## [0.6.4] - 2026-07-07
**Security (Tempo static-memo hardening — GHSA-34g7-vx6g-82mq).** A static `"memo"` in the Tempo `method_config` now requires a dedup `"store"` to be configured — `MPP.Plug` raises `ArgumentError` at init otherwise. A static memo pins attribution independently of the per-challenge nonce, so the dedup store provides the single-use guarantee for that configuration, matching the reference Rust SDK's store-on-by-default backstop. Routes using the default per-challenge attribution (no static memo) are unaffected. Configure `MPP.Tempo.ConCacheStore` (or `{MPP.Tempo.ConCacheStore, opts}`) in your supervision tree, or omit the static memo.
## [0.6.3] - 2026-07-06
**Security (EVM payment-proof single-use — GHSA-vp5h-xh25-44wf).** `MPP.Methods.EVM` gains an optional `"store"` config (an `MPP.Tempo.Store` module, or `{MPP.Tempo.ConCacheStore, opts}`) that makes each on-chain transaction hash single-use, closing a payment-proof replay gap: the method previously matched a settled transfer only by `token`/`to`/`amount` with no single-use binding, and the generic `MPP.Plug` store keys on the per-402 `challenge.id`, so one settled transaction could satisfy repeated charges on a static-price route. The tx hash is now checked before on-chain verification and atomically committed (`check_and_mark/2`, with a non-atomic `put/2` fallback) after — keyed on the canonical (lowercased) hash rather than the challenge id, mirroring the Tempo `type="hash"` path. Store misconfiguration is rejected at init via `validate_config!`. Configure a store with a TTL ≥ your challenge expiry; residual per-challenge on-chain attribution is provided by the EIP-3009 authorization path (roadmap).
Dependency updates. Transitive: `plug 1.20.1 → 1.20.2`, `cowlib 2.17.1 → 2.18.0`, `hpax 1.0.3 → 1.0.4`, `makeup 1.2.1 → 1.2.2`; dev-only `ex_ast 0.12.5 → 0.12.7`.
## [0.6.2] - 2026-06-30
Protocol utilities. Added `MPP.Expires` for ISO 8601 challenge-expiration helpers and `MPP.DID.evm_did/2` for `did:pkh:eip155` credential-source identifiers.
Security. `MPP.Plug` now accepts an optional shared replay store for generic credential deduplication across non-Tempo methods, using the existing `MPP.Tempo.Store` interface and atomic `check_and_mark/2` when available.
**Security (Tempo + Stripe hardening — Task 46).** Tempo gains EIP-712 proof v3 credentials (`MPP.Methods.Tempo.Proof`, `type="proof"`) with wallet-bound signatures, optional store dedup, and zero-amount enforcement. Fee-payer co-signing now rejects fee tokens outside a per-chain allowlist (`FeePayerPolicy.default_allowed_fee_tokens/1`, overridable via `fee_payer_allowed_fee_tokens`). `MPP.DID.parse_evm_did/1` validates hash-credential `did:pkh` sources. `MPP.Methods.Tempo.SessionReceipt` adds optional `externalId` (PaymentWitness parity). Stripe rejects credential `externalId` values that disagree with the route request (mppx #537).
**Security (Tempo proof access keys — Task 69).** Zero-amount Tempo proof verification now accepts active delegated access-key signatures through the Tempo AccountKeychain path when the direct signer is not the root wallet, and rejects stale or revoked access keys.
**Security (Tempo hosted fee payer — Task 68).** Tempo charge verification can delegate fee-payer co-signing to a hosted `eth_fillTransaction` endpoint via `fee_payer_url`, matching mppx `fillHostedFeePayerTransaction`. Returned `feeToken` values are checked against the same sponsor allowlist as local co-signing before broadcast.
## [0.6.1] - 2026-06-29
**Security (Tempo hash-credential dedup).** The `type="hash"` credential path now commits its dedup mark through the store's atomic `check_and_mark/2` — the same primitive the `type="transaction"` path already uses — matching the reference SDKs (mpp-rs `Store::put_if_absent`, mppx atomic `markHashUsed`). The mark still happens only after successful on-chain verification, so a transient receipt-RPC failure does not burn a legitimate hash; stores that do not implement `check_and_mark/2` keep the documented best-effort fallback.
**Security hardening parity.** Verifier/Plug now issue expiring challenges by default, fail closed on missing/invalid expiration, require echoed `digest`/`opaque` values to match endpoint configuration, and MCP clients recognize payment-required result metadata via `org.paymentauth/payment-required`. Tempo validates declared payer source against the matched transfer sender and chain, verifies challenge-bound attribution metadata on unconfigured memo transfers, and provider/RPC/store failure details are sanitized before becoming public 402 responses. **Breaking:** credentials for newly-issued challenges must echo valid expiration data, and Tempo no-static-memo routes now require challenge-bound attribution metadata.
Development tooling. The dev baseline now pins Elixir `1.20.2-otp-29`, adds `ex_slop` and `reach` to `mix precommit.full`, refreshes the generated agent instructions, and includes MCP config for Cursor/Codex/Grok agents. Tempo's unsupported-`eth_simulateV1` degraded-mode log is now a warning so missing node support is visible during operations.
## [0.6.0] - 2026-06-24
**CI / security scaffolding.** The repo gains GitHub Actions CI — previously it had none. A base **CI** workflow gates every push/PR to `development`/`main` with the full check stack (format, `--warnings-as-errors` compile, Credo strict, Doctor, Sobelow, tests at a 95% coverage floor, Dialyzer), mirroring `mix precommit.full`; an **Integration** workflow runs the credential-gated `:integration` suite nightly (and on PR / manual dispatch), flunking loudly when secrets are absent rather than reporting a green 0-test run. A **Code Scanning** workflow uploads Sobelow findings to the Security tab as SARIF (CodeQL has no Elixir support), plus a Dependabot config (weekly Hex + Actions updates) and an expanded `SECURITY.md` scope. Elixir/OTP are pinned via `.tool-versions` so CI never drifts from local `mix format` output.
**Security (Tempo fee-payer gas draining).** When the server acts as fee payer it now validates the client-signed `0x76` envelope's gas economics **before** co-signing, closing two reported gas-draining vectors (GHSA-vv77-66rf-pm86, GHSA-qpxh-ff8m-c62v): unbounded `max_fee_per_gas`/`max_priority_fee_per_gas` and access-list padding. A new `MPP.Methods.Tempo.FeePayerPolicy` bounds `gas_limit`, `max_fee_per_gas`, `max_priority_fee_per_gas`, the `gas × max_fee_per_gas` total-fee budget, and rejects non-empty access lists. Cross-checking against the mppx and mpp-rs reference SDKs surfaced a further defense both enforce that we were missing — folded in here: the sponsored transaction must use the expiring nonce key and declare a `valid_before` that is in the future and within `max_validity_window_seconds` (default 15 min), so a client cannot hold a co-signed sponsorship broadcastable far into the future. All ceilings (gas economics + validity window) default to the reference-SDK values (per-chain; Moderato gets a higher priority-fee ceiling) and are overridable via `method_config["fee_payer_policy"]`. Safe-by-default — existing `fee_payer: true` deployments are protected without config changes. Complementing this static policy, the server now **pre-simulates the full co-signed sponsored transaction** via `eth_simulateV1` before broadcasting — on both the synchronous and optimistic paths: a transaction that would revert on-chain is rejected before the sponsor commits any gas, a node without `eth_simulateV1` (JSON-RPC -32601) degrades gracefully, and any other RPC error fails closed. This covers the residual class that static gas bounds cannot — a `gas_limit` set too low to complete execution. The `onchain_tempo` floor is raised to `~> 0.7` (lock 0.7.0) for the sender-recovery + `Onchain.Tempo.RPC.simulate/3` primitives `MPP.Methods.Tempo` calls directly; the reference SDKs name the method `tempo_simulateV1`, but Tempo deploys the AA-aware EVM-standard `eth_simulateV1` (both mainnet and Moderato return -32601 for the former).
Dependency updates. Runtime: `req ~> 0.5.17``~> 0.6.1` (cascades transitive `finch ~> 0.17``~> 0.21/0.22`; MPP uses only the stable `Req.request/2` + `Req.Response`/`Req.Request` surface, no code changes). Dev/test JS tooling moved as a set now that quickbeam relaxed its constraints: `quickbeam 0.10.5``0.10.15`, `oxc ~> 0.13.0``~> 0.15.1`, `npm ~> 0.6.1``~> 0.7.4`; `ex_unit_json ~> 0.4.3``~> 0.5.0`; dev-only `bandit ~> 1.11.1``~> 1.12.0`.
On-chain stack advanced to the onchain-0.10 line: `descripex ~> 0.7.0``~> 0.9`, `onchain ~> 0.7.0``~> 0.10`, `onchain_tempo ~> 0.2.2``~> 0.7` (the `onchain` bump is what moves `MPP.Methods.EVM` onto the `Onchain.RPC` surface). This required moving the whole ZenHive on-chain family together — descripex 0.8/0.9 add spec-derived JSON Schema and are additive, but descripex **0.9.1** was cut alongside to fix a `safe_convert` crash (it only rescued `ArgumentError`, so real-world specs like Cartouche's `%{required(non_neg_integer()) => <<_::256>>}` aborted the manifest/`describe` build with an uncaught `CaseClauseError`). The lock resolves the family at `descripex 0.11.0`, `cartouche 0.5.0`, `onchain 0.10.0`, `onchain_tempo 0.7.0` (cartouche 0.5 carries the `descripex ~> 0.11` floor). Compile clean under `--warnings-as-errors`; offline tests green against the full updated chain, integration suite green.
## [0.5.1] - 2026-06-09
Dependency updates. Bumped the onchain stack to the 0.7.0 line: `onchain ~> 0.5.4``~> 0.7.0`, `onchain_tempo ~> 0.2.1``~> 0.2.2`, `descripex ~> 0.6.0``~> 0.7.0`. onchain 0.7.0 cascades a major `decimal` `2.4.1``3.1.1` jump (transitive only — MPP has no direct `Decimal` use) and pulls `cartouche 0.2.2`. Dev-tool `doctor` advanced `~> 0.22``~> 0.23` (0.23 requires `decimal ~> 3.1`, unblocked by the jump). No library code changes — compile clean under `--warnings-as-errors`, 601 offline tests green.
## [0.5.0] - 2026-05-15
Tempo session-receipt support and a client-side HTTP transport. Tempo integration tests now use `Onchain.Tempo.Faucet` for per-test fresh wallets instead of hardcoded keys, removing nonce coupling and unblocking a future move to async tests. `MPP.Client.Transport` behaviour with an HTTP implementation lands as the client-side counterpart to server-side `MPP.Method`, exposing a `select_challenge/2` helper that Task 33c (Req plugin) and Task 33d (MCP) will reuse. Dependency floors tightened: `onchain ~> 0.5`, `onchain_tempo ~> 0.2`. A second cross-SDK gap pass added Tasks 36–42 (Stellar Charge, Accept-Payment, EVM `credentialTypes`/Permit2/EIP-3009, Tempo SessionReceipt, OpenAPI discovery) to the roadmap.
## [0.4.0] - 2026-04-18
The first cross-SDK gap pass with substance. `MPP.Verifier` extracts the verification pipeline (HMAC, realm, expiry, request match, method.verify) out of `MPP.Plug` into a transport-neutral module, so MCP and future transports share the same correctness gate. `MPP.JCS` implements the RFC 8785 subset MPP needs for cross-SDK HMAC interop. The MCP transport (`MPP.Mcp`) lands with -32042/-32043 error codes and the `org.paymentauth/credential` + `org.paymentauth/receipt` `_meta` keys. Client-side gets `MPP.Client.PaymentProvider` behaviour and `MultiProvider` first-match dispatch. Protocol utilities fill in: `MPP.Headers.parse_challenges/1` (multi-challenge `WWW-Authenticate` parsing), `MPP.BodyDigest`, `MPP.Amount` (parse_units / with_base_units / parse_dollar_amount). Eight new RFC 9457 session error types added under `paymentauth.org/problems/session/`. EVM integration tests against Sepolia validate real RPC round-trips for both ERC-20 and native-ETH paths.
**Breaking:** `MPP.Amount.parse_dollar_amount/2` now requires callers to supply `decimals` explicitly (previously a single-arg form inferred it from currency). Neither mppx nor mpp-rs maintain a currency-to-decimals table; implicit inference was a correctness risk.
**Scope decision:** proxy/gateway functionality scoped out to a separate `mpp_proxy` package. The `mpp` library focuses on protocol correctness; the BEAM-native payment gateway is a separate product surface.
Dependency bumps: quickbeam 0.8.1 → 0.10.0, oxc 0.5.4 → 0.7.2, ex_dna 1.2.2 → 1.3.0, credo back to hex `~> 1.7` (upstream shipped the Elixir 1.20-rc multi-line sigil fix).
## [0.3.0] - 2026-04-03
Generic EVM payment method and the `onchain_tempo` extraction. `MPP.Methods.EVM` verifies hash-only credentials on any EVM chain (Ethereum, Base, Polygon, Arbitrum, …) — ERC-20 transfers via Transfer event log parsing, native ETH via tx value/recipient matching. Tempo chain primitives moved out of MPP into the standalone `onchain_tempo` Hex package; MPP delegates chain ops and keeps only the payment-protocol surface (dedup store, eth_call simulation, RFC 9457 error wrapping). `mix mpp.demo` ships an interactive Bandit server on port 4402 with a toy `demo-token` method and a pre-computed valid credential in the startup banner — zero-friction first experience. Read-only live-protocol integration tests against `mpp.dev/api/ping/paid` validate parser compatibility with the reference server. `MPP.Tempo.Store` moduledoc gained deployment-topology guidance (`ConCacheStore` for single-node / sticky routing; shared backend otherwise, with `fly-replay` as a worked example).
## [0.2.0] - 2026-03-28
The Tempo payment method (`MPP.Methods.Tempo`) — on-chain TIP-20 transfer verification with both `type="hash"` (client-broadcast) and `type="transaction"` (server-broadcast) credential paths. `transferWithMemo(address,uint256,bytes32)` matching enforced when `methodDetails.memo` is configured. Server-side fee sponsorship via 0x78 domain signing with whitelisted call-scope validation matching mppx's four allowed selector patterns. Optimistic broadcast mode (`wait_for_confirmation: false`) simulates the payment call via `eth_call`, broadcasts asynchronously, and returns an optimistic receipt without waiting for inclusion. `MPP.Tempo.Store` behaviour adds pluggable replay protection with `get`/`put` + optional atomic `check_and_mark/2`; built-in `MPP.Tempo.ConCacheStore` provides ETS-backed TTL dedup. `MPP.Plug`'s `:methods` option enables multi-method 402 endpoints with per-method pricing. All public functions gain Descripex `api()` annotations with `MPP.describe/0-2` progressive discovery and `mix mpp.manifest` for JSON API contract export. Bug fixes covered the TransferWithMemo `memo` indexed-topic discovery (via real Moderato receipts), v-value normalization for ox/tempo SDK interop, hash-path dedup ordering to avoid burning hashes on transient RPC failures, and crash protection in post-broadcast store paths.
## [0.1.0] - 2026-03-25
First public release. Core protocol — HMAC-SHA256-bound Challenge, Credential, Receipt, Headers (WWW-Authenticate / Authorization / Payment-Receipt), and RFC 9457 problem types under `paymentauth.org/problems/`. `MPP.Methods.Stripe` verifies PaymentIntents via SPT with idempotency, analytics metadata, and `Req.Test` mockability. `MPP.Plug` mounts in any Phoenix or Plug router with per-route pricing and explicit credential configuration — no `Application.get_env`, no ENV fallback.