Skip to content

cp0x-org/mppx

mppx

mppx-go

Go SDK for the Machine Payment Protocol (MPP): the server returns 402 Payment Required with a challenge, and the client can automatically pay and retry the request.

Installation

go get github.com/cp0x-org/mppx

Requirement: Go 1.25+ (see go.mod).

What Is Included

  • github.com/cp0x-org/mppx - core protocol types:
    • Challenge, Credential, Receipt
    • header serialization/deserialization
    • PaymentError + RFC 9457 ProblemDetails
    • Expires.*, ComputeDigest, VerifyDigest
  • github.com/cp0x-org/mppx/server - server-side 402/200 flow (Handle, Compose, Middleware, HandlerFunc)
  • github.com/cp0x-org/mppx/client - automatic 402 handling and retry with Authorization: Payment ...
  • github.com/cp0x-org/mppx/middleware/gin - Gin middleware
  • github.com/cp0x-org/mppx/middleware/fiber - Fiber middleware
  • github.com/cp0x-org/mppx/tempo - Tempo payment method (charge and session)
  • github.com/cp0x-org/mppx/stripe - Stripe payment method (charge)

Quick Start

# PowerShell
$env:MPP_SECRET_KEY="demo-secret-key-change-in-production"
go run ./examples/basic/server

In another terminal:

go run ./examples/basic/client

Payment Flow

  1. Client sends a regular HTTP request.
  2. Server responds with 402, adds WWW-Authenticate: Payment ..., and returns application/problem+json.
  3. Client parses the challenge, creates a credential, and retries with Authorization: Payment ....
  4. Server verifies the credential and returns 200 + Payment-Receipt.

Errors and Status Codes

If a method returns *mppx.PaymentError from Verify(), the server serializes it as RFC 9457 application/problem+json (usually with a new challenge).

Main SDK errors:

Constructor HTTP Problem Type
ErrMalformedCredential 402 .../malformed-credential
ErrInvalidChallenge 402 .../invalid-challenge
ErrVerificationFailed 402 .../verification-failed
ErrPaymentRequired 402 .../payment-required
ErrPaymentExpired 402 .../payment-expired
ErrInvalidPayload 402 .../invalid-payload
ErrPaymentInsufficient 402 .../payment-insufficient
ErrInsufficientBalance 402 .../session/insufficient-balance
ErrInvalidSignature 402 .../session/invalid-signature
ErrBadRequest 400 .../bad-request
ErrMethodUnsupported 400 .../method-unsupported
ErrChannelNotFound 410 .../session/channel-not-found
ErrChannelClosed 410 .../session/channel-finalized

Useful helpers:

  • mppx.IsPaymentError(err) / mppx.AsPaymentError(err) for checking/unwrapping payment errors.
  • Any non-PaymentError returned from Verify() is automatically wrapped as ErrVerificationFailed(...).

Headers and Data Format

  • Server challenge: WWW-Authenticate: Payment id="...", realm="...", method="...", intent="...", request="..."
  • Client credential: Authorization: Payment <base64url-json>
  • Server receipt: Payment-Receipt: <base64url-json>
  • Error body: Content-Type: application/problem+json (RFC 9457)

Challenge fields:

  • realm, method, intent, request are required for payment routing.
  • expires is supported and validated via mppx.IsExpired.
  • description is set via server.WithDescription.
  • opaque is set via server.WithMeta.

Key Server API Elements

  • server.New(server.Config{SecretKey, Realm, DefaultExpiry})
  • srv.Handle(r, method, req, opts...)
  • srv.Compose(entries...) - multiple payment methods for one endpoint
  • srv.Middleware(...) / srv.HandlerFunc(...)
  • server.WithDescription(...), server.WithExpires(...), server.WithMeta(...)

Validations inside Handle:

  • challenge signature (SecretKey + HMAC),
  • method/intent/realm match,
  • key request field match: amount, currency, recipient,
  • expires check.

Key Client API Elements

  • client.New(client.Config{Methods, HTTPClient, OnChallenge})
  • c.Do(req), c.Get(ctx, url), c.Post(...)
  • client.GetReceipt(resp)

Do behavior:

  • validates URL (http/https),
  • on 402, parses challenge,
  • finds registered method by method/intent,
  • creates credential and retries the request.

Middleware (Gin/Fiber)

Detailed documentation:

In short:

  • Payment(...) - one payment method per route.
  • Compose(...) - multiple payment methods.
  • GetReceipt(c) - extracts mppx.Receipt in the handler.

Repository Examples

  • go run ./examples/basic/server - basic net/http + Gin
  • go run ./examples/fiber/server - Fiber middleware
  • go run ./examples/http-middleware/server - HTTP middleware patterns
  • go run ./examples/multi-method/server - multi-method (server.Compose)
  • go run ./examples/session/server - session flow (tempo/session-like)
  • go run ./examples/stripe/server - Stripe flow (STRIPE_SECRET_KEY=sk_test_...)
  • go run ./examples/tempo/charge/server - Tempo one-time charge (TIP-20 transfer)
  • go run ./examples/tempo/session/server - Tempo payment channel (open/voucher/close)

Clients are in matching examples/*/client folders.

How to run the Tempo charge example (server + client), configure .env files, and see real output: docs/tempo-charge-example.md.

Minimal Example: Paid Server Endpoint

package main

import (
	"context"
	"encoding/json"
	"net/http"
	"os"

	mppx "github.com/cp0x-org/mppx"
	"github.com/cp0x-org/mppx/server"
)

type DemoMethod struct{}

func (m *DemoMethod) Name() string   { return "demo" }
func (m *DemoMethod) Intent() string { return "charge" }
func (m *DemoMethod) DefaultRequest() map[string]any {
	return map[string]any{"currency": "USDC", "recipient": "0xabc"}
}
func (m *DemoMethod) Verify(_ context.Context, cred mppx.Credential, _ map[string]any) (mppx.Receipt, error) {
	payload, ok := cred.Payload.(map[string]any)
	if !ok {
		return mppx.Receipt{}, mppx.ErrInvalidPayload("expected object payload")
	}
	txHash, _ := payload["txHash"].(string)
	if txHash == "" {
		return mppx.Receipt{}, mppx.ErrInvalidPayload("missing txHash")
	}
	return mppx.NewReceipt(mppx.ReceiptParams{Method: "demo", Reference: txHash}), nil
}

func main() {
	srv := server.New(server.Config{SecretKey: os.Getenv("MPP_SECRET_KEY"), Realm: "localhost"})
	method := &DemoMethod{}

	http.HandleFunc("/premium", func(w http.ResponseWriter, r *http.Request) {
		result := srv.Handle(r, method, map[string]any{"amount": "1000000"}, server.WithDescription("Premium API"))
		if result.Status == http.StatusPaymentRequired {
			result.Write(w) // 402 + WWW-Authenticate + Problem Details
			return
		}
		result.SetReceiptHeader(w) // Payment-Receipt
		_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
	})

	_ = http.ListenAndServe(":8080", nil)
}

Minimal Example: Client Request and Handling

package main

import (
	"context"
	"net/http"

	mppx "github.com/cp0x-org/mppx"
	"github.com/cp0x-org/mppx/client"
)

type DemoClientMethod struct{}

func (m *DemoClientMethod) Name() string   { return "demo" }
func (m *DemoClientMethod) Intent() string { return "charge" }
func (m *DemoClientMethod) CreateCredential(_ context.Context, ch mppx.Challenge) (string, error) {
	cred := mppx.Credential{
		Challenge: ch,
		Payload:   map[string]any{"txHash": "0xdeadbeef"},
		Source:    "did:pkh:eip155:1:0xDemo",
	}
	return mppx.SerializeCredential(cred), nil
}

func main() {
	c := client.New(client.Config{
		Methods:    []mppx.ClientMethod{&DemoClientMethod{}},
		HTTPClient: http.DefaultClient,
	})

	resp, err := c.Get(context.Background(), "http://localhost:8080/premium")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	receipt, err := client.GetReceipt(resp) // Payment-Receipt -> mppx.Receipt
	if err != nil {
		panic(err)
	}
	_ = receipt // use receipt.Method / receipt.Reference / receipt.Timestamp
}

Development

go build ./...
go test ./...

Contributing

About

Golang Interface for Machine Payments Protocol by cp0x

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages