The Go MPP SDK handles the full Solana HTTP 402 payment flow with zero external dependencies. Pure stdlib: ed25519 signing, base58, JSON-RPC, and compact-u16 transaction encoding. One import, one function call on each side.
Go and HTTP 402
Go is the language of infrastructure: APIs, microservices, proxies. If you're building something that receives a lot of requests and needs to monetise them without billing overhead, Go is a natural fit.
The Go MPP SDK (github.com/mpptestkit/mpp-test-sdk-go) adds Solana-backed HTTP 402 support to any net/http handler with a single middleware call. No wallet libraries. No Solana SDK. No cgo. Pure Go standard library from crypto to JSON-RPC.
Installation
go get github.com/mpptestkit/mpp-test-sdk-go
Server
package main
import (
"encoding/json"
"fmt"
"net/http"
mpp "github.com/mpptestkit/mpp-test-sdk-go"
)
func main() {
server, err := mpp.CreateTestServer(&mpp.TestServerConfig{
Network: mpp.NetworkDevnet,
})
if err != nil {
panic(err)
}
fmt.Println("Recipient:", server.RecipientAddress)
mux := http.NewServeMux()
mux.Handle("/api/data", server.Charge(mpp.ChargeOptions{Amount: "0.001"})(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"result": "here is your data"})
}),
))
http.ListenAndServe(":3001", mux)
}
server.Charge(...) returns an http.Handler middleware that fits any net/http mux, gorilla/mux, chi, or echo router.
Client
package main
import (
"context"
"encoding/json"
"fmt"
mpp "github.com/mpptestkit/mpp-test-sdk-go"
)
func main() {
ctx := context.Background()
client, err := mpp.CreateTestClient(ctx, &mpp.TestClientConfig{
Network: mpp.NetworkDevnet,
OnStep: func(step mpp.PaymentStep) {
fmt.Printf("[%s] %s\n", step.Type, step.Message)
},
})
if err != nil {
panic(err)
}
fmt.Println("Wallet:", client.Address)
resp, err := client.Fetch(ctx, "http://localhost:3001/api/data", nil)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result) // map[result:here is your data]
}
What happens under the hood
The SDK builds a Solana legacy transaction entirely from stdlib primitives:
compact-u16(1) // signature count
[64]byte // ed25519 signature over message
Message:
[3]byte // header: required_sigs=1, ro_signed=0, ro_unsigned=1
compact-u16(3) // account key count
[32*3]byte // [from, to, system_program]
[32]byte // recent blockhash
compact-u16(1) // instruction count
Instruction:
u8(2) // program_id_index = system program
compact-u16(2) // account indices [0=from, 1=to]
compact-u16(12) // data length
[4]byte LE // SystemInstruction::Transfer = 2
[8]byte LE // lamports (u64)
No Solana Go SDK. No protobuf. Just crypto/ed25519, encoding/binary, and math/big for base58.
Lifecycle steps
client, err := mpp.CreateTestClient(ctx, &mpp.TestClientConfig{
Network: mpp.NetworkDevnet,
OnStep: func(step mpp.PaymentStep) {
switch step.Type {
case mpp.StepWalletCreated:
fmt.Println("Wallet:", step.Data["address"])
case mpp.StepFunded:
fmt.Println("Funded via devnet airdrop")
case mpp.StepPayment:
fmt.Println("Paying", step.Data["amount"], "SOL")
case mpp.StepSuccess:
fmt.Println("Served, status:", step.Data["status"])
}
},
})
Steps: wallet-created, funded, request, payment, retry, success, error.
Airdrop with exponential back-off
On devnet and testnet the SDK retries airdrop failures automatically with 1s → 2s → 4s back-off before returning MppFaucetError:
// Returns *MppFaucetError after 3 failed attempts
client, err := mpp.CreateTestClient(ctx, nil)
if err != nil {
var faucetErr *mpp.MppFaucetError
if errors.As(err, &faucetErr) {
fmt.Println("Airdrop failed for:", faucetErr.Address)
}
}
Mainnet
client, err := mpp.CreateTestClient(ctx, &mpp.TestClientConfig{
Network: mpp.NetworkMainnet,
SecretKey: myKeypairBytes, // 32-byte seed or 64-byte full private key
})
Drop-in http.Get replacement
resp, err := mpp.MppFetch(ctx, "http://localhost:3001/api/data", nil)
Lazily creates a shared devnet client. Call mpp.ResetMppFetch() to force a fresh wallet.
Error types
var (
faucetErr *mpp.MppFaucetError
payErr *mpp.MppPaymentError
timeoutErr *mpp.MppTimeoutError
netErr *mpp.MppNetworkError
)
switch {
case errors.As(err, &faucetErr): fmt.Println("Airdrop failed:", faucetErr.Address)
case errors.As(err, &payErr): fmt.Println("Payment failed, status:", payErr.Status)
case errors.As(err, &timeoutErr): fmt.Println("Timed out after:", timeoutErr.TimeoutMs, "ms")
case errors.As(err, &netErr): fmt.Println("Network error:", netErr.Network)
}
Integration test example
func TestChargesPerRequest(t *testing.T) {
ctx := context.Background()
server, _ := mpp.CreateTestServer(&mpp.TestServerConfig{Network: mpp.NetworkDevnet})
mux := http.NewServeMux()
mux.Handle("/data", server.Charge(mpp.ChargeOptions{Amount: "0.001"})(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}),
))
ts := httptest.NewServer(mux)
defer ts.Close()
client, _ := mpp.CreateTestClient(ctx, &mpp.TestClientConfig{Network: mpp.NetworkDevnet})
resp, err := client.Fetch(ctx, ts.URL+"/data", nil)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("want 200, got %d", resp.StatusCode)
}
}
Links
- GitHub: sdk-go
- mpptestkit.com — live playground
- GitHub: mpptestkit — source and issues
Top comments (0)