Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ export function RustSdkCard() {
);
}

export function ElixirSdkCard() {
return (
<Card
description="MPP SDK for Elixir — Plug middleware for Phoenix and Plug apps"
icon="simple-icons:elixir"
title="Elixir"
to="/sdk/elixir"
/>
);
}

export function MppxCreateReferenceCard({ to }: { to: string }) {
return (
<Card
Expand Down
89 changes: 89 additions & 0 deletions src/pages/sdk/elixir/core.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
imageDescription: "Challenge, Credential, and Receipt types for working with the MPP protocol in Elixir"
---

# Core types [Challenge, Credential, and Receipt modules]

These modules map directly to HTTP headers — `WWW-Authenticate`, `Authorization`, and `Payment-Receipt` — and can be used independently of the `MPP.Plug` middleware.

## Challenge

Create an HMAC-bound challenge and format it as a `WWW-Authenticate` header:

```elixir
alias MPP.{Challenge, Headers}

challenge = Challenge.create(
secret_key: "your-hmac-secret",
realm: "api.example.com",
method: "stripe",
intent: "charge",
request_b64: Base.url_encode64(Jason.encode!(%{amount: "1000", currency: "usd"}), padding: false)
)

header = Headers.format_www_authenticate(challenge)
# → "Payment id=\"abc...\", realm=\"api.example.com\", ..."
```

Verify a challenge ID on the retry request:

```elixir
{:ok, true} = Challenge.verify(challenge, "your-hmac-secret")
```

## Credential

Parse an `Authorization: Payment` header into a credential struct:

```elixir
alias MPP.{Credential, Headers}

{:ok, credential} = Headers.parse_authorization(auth_header)

# Access the echoed challenge fields
credential.challenge_id
credential.method
credential.intent

# Access the payment payload
credential.payload
```

## Receipt

Build a receipt and format it as a `Payment-Receipt` header:

```elixir
alias MPP.{Receipt, Headers}

receipt = %Receipt{
status: "success",
challenge_id: challenge.id,
method: "stripe",
reference: "pi_abc123"
}

header = Headers.format_receipt(receipt)
# → base64url-encoded JSON
```

## Headers

The `MPP.Headers` module handles all header parsing and formatting:

| Function | Description |
|----------|-------------|
| `format_www_authenticate/1` | Serialize a Challenge to a `WWW-Authenticate` header value |
| `format_receipt/1` | Serialize a Receipt to a `Payment-Receipt` header value |
| `parse_authorization/1` | Parse an `Authorization` header into a Credential |

## Type reference

| Module | Description |
|--------|-------------|
| `MPP.Challenge` | Server challenge with HMAC-SHA256 ID binding |
| `MPP.Credential` | Client credential parsed from `Authorization` header |
| `MPP.Receipt` | Server receipt for `Payment-Receipt` header |
| `MPP.Headers` | Header parsing and formatting utilities |
| `MPP.Intents.Charge` | Charge intent request schema (amount, currency, recipient) |
| `MPP.Errors` | RFC 9457 Problem Detail error types |
107 changes: 107 additions & 0 deletions src/pages/sdk/elixir/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: "Elixir SDK for MPP"
imageDescription: "The mpp Elixir library for adding payment-gated endpoints to Phoenix and Plug applications"
---

import * as SdkBadge from '../../../components/SdkBadge'

# Elixir SDK [Plug middleware for Phoenix and Plug apps]

## Overview

The `mpp` Elixir library provides Plug middleware that adds pay-per-call billing to any Phoenix or Plug application. Mount the plug on a route, set a price, and the middleware handles the full 402 challenge/credential/receipt flow.

<div className="flex gap-2">
<SdkBadge.GitHub repo="ZenHive/mpp" />
<SdkBadge.Maintainer name="ZenHive" href="https://github.com/ZenHive" />
</div>

## Install

Add `mpp` to your dependencies in `mix.exs`:

```elixir [mix.exs]
defp deps do
[
{:mpp, "~> 0.1"}
]
end
```

Then fetch dependencies:

```bash [install.sh]
$ mix deps.get
```

## Requirements

- Elixir 1.17+
- A Plug-compatible application (Phoenix, Bandit, Cowboy)

## Quick start

### Server

Mount `MPP.Plug` in a Phoenix router to gate endpoints behind payment:

```elixir [router.ex]
defmodule MyAppWeb.Router do
use MyAppWeb, :router

pipeline :paid do
plug MPP.Plug,
secret_key: "your-hmac-secret",
realm: "api.example.com",
method: MPP.Methods.Stripe,
method_config: %{stripe_secret_key: System.get_env("STRIPE_SECRET_KEY")},
amount: "1000",
currency: "usd"
end

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

The middleware handles the full flow automatically:

1. Request without `Authorization: Payment` header returns `402` with a `WWW-Authenticate` challenge
2. Client pays off-band (e.g. via Stripe), retries with `Authorization: Payment <credential>`
3. Valid credential passes through with a `Payment-Receipt` header attached
4. Invalid credential returns `402` with a fresh challenge and an RFC 9457 error body

### Per-route pricing

Each route can have its own price — just mount the plug with different options:

```elixir [router.ex]
scope "/api", MyAppWeb do
pipe_through :api

scope "/basic" do
plug MPP.Plug, amount: "100", currency: "usd",
secret_key: secret, realm: realm, method: MPP.Methods.Stripe
get "/data", DataController, :show
end

scope "/premium" do
plug MPP.Plug, amount: "5000", currency: "usd",
secret_key: secret, realm: realm, method: MPP.Methods.Stripe
get "/report", ReportController, :show
end
end
```

## Design

- **Stateless**: Challenge verification uses HMAC-SHA256 binding — no database or challenge store needed
- **Explicit configuration**: No `Application.get_env` or ENV fallbacks — pass credentials directly via plug opts
- **Pluggable methods**: Implement the `MPP.Method` behaviour to add new payment methods beyond Stripe

## Next steps

- [Core types](/sdk/elixir/core): Challenge, Credential, and Receipt modules
- [Server](/sdk/elixir/server): Plug middleware configuration and usage
146 changes: 146 additions & 0 deletions src/pages/sdk/elixir/server.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
imageDescription: "Protect Phoenix and Plug endpoints with payment requirements using MPP middleware"
---

# Server [Plug middleware for payment-gated endpoints]

Mount `MPP.Plug` in any Phoenix or Plug router to protect endpoints with payment. The plug handles the full 402 challenge/credential/receipt flow.

## Quick start

```elixir
plug MPP.Plug,
secret_key: "your-hmac-secret",
realm: "api.example.com",
method: MPP.Methods.Stripe,
method_config: %{stripe_secret_key: "sk_..."},
amount: "1000",
currency: "usd"
```

## Options

### secret_key (required)

- **Type:** `String.t()`

HMAC-SHA256 key for stateless challenge ID binding. The server recomputes the HMAC on verification — no challenge store needed.

### realm (required)

- **Type:** `String.t()`

Server protection space, included in `WWW-Authenticate` headers. Typically your API domain.

### method (required)

- **Type:** module implementing `MPP.Method`

Payment method module for verifying credentials. Built-in: `MPP.Methods.Stripe`.

### amount (required)

- **Type:** `String.t()`

Price in base units (e.g. `"1000"` for $10.00 USD with Stripe).

### currency (required)

- **Type:** `String.t()`

Currency code, normalized to lowercase (e.g. `"usd"`).

### method_config (optional)

- **Type:** `map()`

Server-only configuration passed to `MPP.Method.verify/2` via `charge.method_details`. Use this for secrets like API keys that the payment method needs but clients must not see. Never serialized into challenges.

```elixir
method_config: %{stripe_secret_key: System.get_env("STRIPE_SECRET_KEY")}
```

### recipient (optional)

- **Type:** `String.t()`

Payment recipient identifier included in the charge intent.

### description (optional)

- **Type:** `String.t()`

Human-readable description attached to the challenge.

### expires_in (optional)

- **Type:** `integer()`

Challenge TTL in seconds. Defaults to 300 (5 minutes).

### opaque (optional)

- **Type:** `String.t()`

Base64url-encoded server correlation data included in the challenge.

## Phoenix router example

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

pipeline :paid do
plug MPP.Plug,
secret_key: Application.compile_env!(:my_app, :mpp_secret_key),
realm: "api.example.com",
method: MPP.Methods.Stripe,
method_config: %{
stripe_secret_key: Application.compile_env!(:my_app, :stripe_secret_key)
},
amount: "1000",
currency: "usd"
end

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

## Accessing the receipt

After successful payment verification, the receipt is available in `conn.assigns`:

```elixir
def show(conn, _params) do
receipt = conn.assigns[:mpp_receipt]
json(conn, %{data: "paid content", reference: receipt.reference})
end
```

## Custom payment methods

Implement the `MPP.Method` behaviour to add new payment methods:

```elixir
defmodule MyApp.Payments.CustomMethod do
@behaviour MPP.Method

@impl true
def verify(credential, charge) do
# Verify the payment and return a receipt or error
{:ok, %MPP.Receipt{status: "success", reference: "ref_123", ...}}
end
end
```

Then use it in your plug configuration:

```elixir
plug MPP.Plug,
method: MyApp.Payments.CustomMethod,
# ...
```
7 changes: 4 additions & 3 deletions src/pages/sdk/index.mdx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
---
title: "SDKs and client libraries"
description: "Official MPP SDK implementations in TypeScript, Python, and Rust. Libraries for building MPP clients and servers with full protocol support."
imageDescription: "Official MPP SDK implementations in TypeScript, Python, and Rust for clients, servers, and agents"
description: "MPP SDK implementations in TypeScript, Python, Rust, and Elixir. Libraries for building MPP clients and servers with full protocol support."
imageDescription: "MPP SDK implementations in TypeScript, Python, Rust, and Elixir for clients, servers, and agents"
---

import { Cards } from 'vocs'
import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, WalletCliCard } from '../../components/cards'
import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, ElixirSdkCard, WalletCliCard } from '../../components/cards'
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WalletCliCard is imported here but never used in this MDX file. Please remove it from the import list to avoid unused-import lint/TS checks failing (or add the card to the <Cards> section if it was intended to be shown).

Suggested change
import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, ElixirSdkCard, WalletCliCard } from '../../components/cards'
import { TypeScriptSdkCard, PythonSdkCard, RustSdkCard, ElixirSdkCard } from '../../components/cards'

Copilot uses AI. Check for mistakes.

# SDKs [Official implementations in multiple languages]

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page heading describes the listed SDKs as “Official implementations”, but the newly added Elixir SDK is a community-maintained project (ZenHive/mpp) per the PR description/issue. Please adjust the heading (and/or add a clear “Community” label on the Elixir card) so readers aren’t misled about support/ownership.

Copilot uses AI. Check for mistakes.
<Cards>
<TypeScriptSdkCard />
<PythonSdkCard />
<RustSdkCard />
<ElixirSdkCard />
</Cards>
Loading
Loading