MPP lets any client — agents, apps, or humans — pay for any service in the same HTTP request. It standardizes HTTP 402 with an open IETF specification, so servers can charge and clients can pay without API keys, billing accounts, or checkout flows.
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/tempoxyz/mpp-go/pkg/server"
"github.com/tempoxyz/mpp-go/pkg/tempo"
charge "github.com/tempoxyz/mpp-go/pkg/tempo/server"
)
func main() {
method, _ := charge.MethodFromConfig(charge.Config{
// Development only; use a shared persistent Store in production.
Store: tempo.NewMemoryStore(),
RPCURL: "https://rpc.moderato.tempo.xyz",
Recipient: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
})
// Secret must be at least 32 bytes (generate with: openssl rand -base64 32).
payment, err := server.New(method, "api.example.com", "replace-me-with-a-32-byte-min-secret")
if err != nil {
log.Fatal(err)
}
handler := server.ChargeMiddleware(payment, server.ChargeParams{
Amount: "0.50",
Description: "Paid content",
})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"data": "paid content",
"payer": server.CredentialFromContext(r.Context()).Source,
})
}))
_ = http.ListenAndServe(":8080", handler)
}
If the endpoint already uses Authorization (API keys, Bearer tokens), create the server with server.WithRequiresAuth(true). Challenges then advertise header="Payment-Authorization", and clients send the Payment credential in that header instead of Authorization.
Local verifier construction requires an explicit Store, so an unconfigured
server cannot issue payable challenges. All
replicas must share persistent replay keys, with atomic PutIfAbsent, no eviction,
and fail-closed behavior on storage errors. tempo.NewMemoryStore() is an explicit
single-process development choice; it loses replay protection on restart. Existing
applications relying on the implicit memory store must configure storage. Relay
verification delegates replay protection to the relay.
Client
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/tempoxyz/mpp-go/pkg/client"
"github.com/tempoxyz/mpp-go/pkg/mpp"
charge "github.com/tempoxyz/mpp-go/pkg/tempo/client"
)
func main() {
method, _ := charge.New(charge.Config{
PrivateKey: os.Getenv("MPP_PRIVATE_KEY"),
RPCURL: "https://rpc.moderato.tempo.xyz",
})
c := client.New([]client.Method{method})
response, err := c.Get(context.Background(), "https://api.example.com/paid")
if err != nil {
panic(err)
}
defer response.Body.Close()
receipt, _ := mpp.ParseReceipt(response.Header.Get("Payment-Receipt"))
var body struct {
Data string `json:"data"`
Payer string `json:"payer"`
}
_ = json.NewDecoder(response.Body).Decode(&body)
fmt.Printf("paid request for %q from %s with receipt %s\n", body.Data, body.Payer, receipt.Reference)
}
Moderato charge flow that delegates credential validation and broadcast to Tempo API's MPP relay
Idempotent POST Charges
For paid POST routes or agent tool calls, bind each logical operation to a
stable ExternalID. The value is included in the MPP charge request and echoed
back in the Payment-Receipt, so servers can distinguish a safe retry from a
different paid operation. ExternalID is receipt metadata; it does not by
itself persist or reject duplicate operations. Your application must keep its
own idempotency record or response cache keyed by the client idempotency value
before running the paid side effect.
If your API already accepts an Idempotency-Key header, pass that value through
as ChargeParams.ExternalID when constructing the charge:
Use the same ExternalID for retries of the same operation. Do not generate a
new value for each retry, and avoid reusing one value across different paid
operations. On retry, check the stored idempotency result first and return the
cached response or receipt instead of executing the POST operation again.
Split Credential Lifecycle
Server intents can register separate advisory validation and settlement hooks:
The constructed intent satisfies the public server.BroadcastingIntent
interface as well as the backwards-compatible server.Intent interface. Custom
intents can implement server.ValidatingIntent or server.BroadcastingIntent
directly. Servers can run the phases independently:
ValidateCredential requires a server.ValidatingIntent and must not consume
replay state, sign fee-payer transactions, or broadcast.
BroadcastCredential re-validates before settlement. VerifyCredential
remains an alias for the mutating broadcast path, and legacy intents that only
implement Verify continue to work for paid routes and broadcasts.
The built-in Tempo charge intent implements the split lifecycle locally. Tempo
relay methods implement the same lifecycle by delegating validation to POST /v1/mpp/validate and finalization to POST /v1/mpp/broadcast.
Web Frameworks
The most common Go HTTP stacks today are net/http, Gin, Echo, Chi, and Fiber.
MPP already integrates directly with net/http, which also covers Chi because
Chi handlers and middleware use the standard http.Handler interfaces.
For the most common non-stdlib frameworks, mpp-go ships first-class adapters:
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this project by you, as defined in the Apache-2.0 license,
shall be dual licensed as above, without any additional terms or conditions.
Payment request safety
Hash-mode clients validate the credential header size before broadcasting. For custom clients,
serialize with mpp.FormatAuthorizationStrict(credential) and handle its error.
With ChargeParams{EnableChallengeNonce: true}, server-issued challenges have
random opaque _mpp_nonce metadata. Do not set
that reserved key. Bind authenticated application context with
ChargeParams{Meta: map[string]string{"account": authenticatedAccountID}} when
a payment must authorize a specific caller. Derive this ID from trusted
session authentication, never directly from request input. Nonces prevent
challenge collisions; they do not make leaked complete credentials safe.
Low-level challenge constructors remain deterministic, so custom issuance must
supply its own unique, authenticated context. Existing challenges remain valid
until their original expiry.
Nonce issuance is opt-in for upgrade safety. First deploy this release everywhere
with the default EnableChallengeNonce: false. Verification already accepts
nonces, while issuance stays compatible with old replicas. After all verifiers
are upgraded, set ChargeParams{EnableChallengeNonce: true} on each route (or
the equivalent VerifyParams field). The shared-challenge collision risk
remains until issuance is enabled. Old replicas reject nonced credentials,
potentially after a hash payment has settled; do not enable issuance while old
verifiers remain. Version-sticky retries are an alternative rollout strategy.
Package tempo contains the Tempo-specific charge method built on top of the generic pkg/client and pkg/server HTTP 402 flow, including request helpers, attribution utilities, and replay stores for single-process and Redis-backed deployments.
Package tempo contains the Tempo-specific charge method built on top of the generic pkg/client and pkg/server HTTP 402 flow, including request helpers, attribution utilities, and replay stores for single-process and Redis-backed deployments.