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.
go get github.com/cp0x-org/mppxRequirement: Go 1.25+ (see go.mod).
github.com/cp0x-org/mppx- core protocol types:Challenge,Credential,Receipt- header serialization/deserialization
PaymentError+ RFC 9457ProblemDetailsExpires.*,ComputeDigest,VerifyDigest
github.com/cp0x-org/mppx/server- server-side 402/200 flow (Handle,Compose,Middleware,HandlerFunc)github.com/cp0x-org/mppx/client- automatic402handling and retry withAuthorization: Payment ...github.com/cp0x-org/mppx/middleware/gin- Gin middlewaregithub.com/cp0x-org/mppx/middleware/fiber- Fiber middlewaregithub.com/cp0x-org/mppx/tempo- Tempo payment method (chargeandsession)github.com/cp0x-org/mppx/stripe- Stripe payment method (charge)
# PowerShell
$env:MPP_SECRET_KEY="demo-secret-key-change-in-production"
go run ./examples/basic/serverIn another terminal:
go run ./examples/basic/client- Client sends a regular HTTP request.
- Server responds with
402, addsWWW-Authenticate: Payment ..., and returnsapplication/problem+json. - Client parses the challenge, creates a credential, and retries with
Authorization: Payment .... - Server verifies the credential and returns
200+Payment-Receipt.
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-
PaymentErrorreturned fromVerify()is automatically wrapped asErrVerificationFailed(...).
- 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,requestare required for payment routing.expiresis supported and validated viamppx.IsExpired.descriptionis set viaserver.WithDescription.opaqueis set viaserver.WithMeta.
server.New(server.Config{SecretKey, Realm, DefaultExpiry})srv.Handle(r, method, req, opts...)srv.Compose(entries...)- multiple payment methods for one endpointsrv.Middleware(...)/srv.HandlerFunc(...)server.WithDescription(...),server.WithExpires(...),server.WithMeta(...)
Validations inside Handle:
- challenge signature (
SecretKey+ HMAC), method/intent/realmmatch,- key request field match:
amount,currency,recipient, expirescheck.
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.
Detailed documentation:
In short:
Payment(...)- one payment method per route.Compose(...)- multiple payment methods.GetReceipt(c)- extractsmppx.Receiptin the handler.
go run ./examples/basic/server- basicnet/http+ Gingo run ./examples/fiber/server- Fiber middlewarego run ./examples/http-middleware/server- HTTP middleware patternsgo 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.
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)
}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
}go build ./...
go test ./...