Packages
Elixir implementation of the Machine Payments Protocol (MPP) — HTTP 402 payment middleware for AI agents and machine-to-machine commerce. Supports Stripe, Tempo, and generic EVM payment methods with pluggable architecture.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/mpp/errors.ex
defmodule MPP.Errors do
@moduledoc """
RFC 9457 Problem Details for MPP payment errors.
Each error type maps to a problem URI under `https://paymentauth.org/problems/`,
an HTTP status code, and a human-readable title. Use `to_map/1` to render an
error as a JSON-compatible map for HTTP response bodies.
## Problem Types
* `:payment_required` — no payment credential provided (402)
* `:payment_insufficient` — payment amount too low (402)
* `:payment_expired` — challenge has expired (402)
* `:verification_failed` — payment proof is invalid (402)
* `:method_unsupported` — payment method not accepted (400)
* `:malformed_credential` — credential cannot be parsed (402)
* `:invalid_challenge` — challenge ID doesn't match or is unknown (402)
* `:invalid_payload` — credential payload doesn't match schema (402)
* `:bad_request` — malformed request (400)
"""
use Descripex, namespace: "/protocol"
@base_uri "https://paymentauth.org/problems/"
@problem_types %{
payment_required: %{suffix: "payment-required", title: "Payment Required", status: 402},
payment_insufficient: %{suffix: "payment-insufficient", title: "Payment Insufficient", status: 402},
payment_expired: %{suffix: "payment-expired", title: "Payment Expired", status: 402},
verification_failed: %{suffix: "verification-failed", title: "Verification Failed", status: 402},
method_unsupported: %{suffix: "method-unsupported", title: "Method Unsupported", status: 400},
malformed_credential: %{suffix: "malformed-credential", title: "Malformed Credential", status: 402},
invalid_challenge: %{suffix: "invalid-challenge", title: "Invalid Challenge", status: 402},
invalid_payload: %{suffix: "invalid-payload", title: "Invalid Payload", status: 402},
bad_request: %{suffix: "bad-request", title: "Bad Request", status: 400}
}
@type problem_type ::
:payment_required
| :payment_insufficient
| :payment_expired
| :verification_failed
| :method_unsupported
| :malformed_credential
| :invalid_challenge
| :invalid_payload
| :bad_request
@type t :: %__MODULE__{
type: String.t(),
title: String.t(),
status: pos_integer(),
detail: String.t()
}
@enforce_keys [:type, :title, :status, :detail]
defstruct [:type, :title, :status, :detail]
api(:new, "Create an RFC 9457 Problem Detail error for the given problem type.",
params: [
type: [kind: :value, description: "Problem type atom (e.g., `:payment_required`, `:verification_failed`)"],
detail: [kind: :value, description: "Human-readable error detail string"]
],
returns: %{type: :struct, description: "Error struct with `type` URI, `title`, `status`, and `detail`"},
composes_with: [:to_map, :to_json]
)
@spec new(problem_type(), String.t()) :: t()
def new(type, detail) when is_atom(type) and is_binary(detail) do
case Map.fetch(@problem_types, type) do
{:ok, problem} ->
%__MODULE__{
type: @base_uri <> problem.suffix,
title: problem.title,
status: problem.status,
detail: detail
}
:error ->
raise ArgumentError, "unknown problem type: #{inspect(type)}"
end
end
api(:to_json, "Render the error as an RFC 9457 Problem Details JSON string.",
params: [
error: [kind: :value, description: "Error struct to serialize"]
],
returns: %{type: :string, description: "JSON string with `type`, `title`, `status`, `detail` keys"},
composes_with: [:new, :to_map]
)
@spec to_json(t()) :: String.t()
def to_json(%__MODULE__{} = error) do
error
|> to_map()
|> Jason.encode!()
end
api(:to_map, "Render the error as an RFC 9457 Problem Details map with string keys.",
params: [
error: [kind: :value, description: "Error struct to render"]
],
returns: %{type: :map, description: ~s(Map with `"type"`, `"title"`, `"status"`, `"detail"` keys)},
composes_with: [:new, :to_json]
)
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = error) do
%{
"type" => error.type,
"title" => error.title,
"status" => error.status,
"detail" => error.detail
}
end
api(:types, "Return the list of known problem type atoms.")
@spec types :: [problem_type()]
def types, do: Map.keys(@problem_types)
end