MPP
Pay per request from a Solana wallet — open a USDC session once, meter with off-chain vouchers, get the unused deposit back.
MPP (Machine Payments Protocol) pays for inference from a Solana wallet — no plan, no sign-up,
no vk_ key, and no per-request minimum. You escrow a USDC deposit once, every request is
metered with an off-chain signed voucher, and closing the session settles what you actually
spent and refunds the rest. For where MPP sits next to the other payment methods, see
Pricing; for the per-request wallet flow on Base, see
x402.
Solana mainnet only
MPP sessions settle in USDC on Solana mainnet
(EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v), escrowed in the Solana Foundation
payment-channels program. No other
chain or mint is accepted. Deposits are capped at 100 USDC per session.
Why a session
Every x402 request settles on-chain, so the facilitator's $0.01 minimum applies to each call. An MPP session moves that cost to the edges: one transaction to open, one to close, and every request in between is a free off-chain signature. That makes it the right rail for micro-requests and chatty agents — a thousand $0.0001 calls cost $0.10 in usage, not $10 in minimums.
The escrow is trustless: funds live in the on-chain program, the gateway can only claim amounts you signed vouchers for, and the unused remainder returns to your wallet at close. If the gateway ever goes unresponsive, you can force-close on-chain and reclaim the deposit yourself.
Pay from a Solana wallet
The @solana/mpp client wraps fetch: on the first 402 it opens the session (one wallet
signature — the gateway co-signs as fee payer, so the wallet needs USDC only, no SOL), then
signs vouchers silently with an ephemeral session key. Two integration shapes follow — the
session client directly, or dropped into the OpenAI SDK.
Requires @solana/mpp 0.7.0+
The 0.6.0 build on npm predates the deployed program's current account layout — session
opens built with it fail on-chain. Use 0.7.0 or newer (in the
mpp-sdk repository ahead of the npm publish).
After every metered response the gateway returns a Payment-Metering header (base64url JSON)
naming the exact cumulative amount to sign next. Feed it back with recordCumulative + flush
and the client signs and commits the voucher off-chain, no wallet prompt. The gateway serves at
most one request beyond your last voucher — the next call answers 402 until that commit lands,
so keep the record-and-flush loop next to your calls.
import { createSessionFetch, createPaymentChannelSessionOpener } from "@solana/mpp/client";
import { createKeyPairSignerFromBytes, getBase58Encoder } from "@solana/kit";
const payer = await createKeyPairSignerFromBytes(
new Uint8Array(getBase58Encoder().encode(process.env.PAYER_SECRET_KEY!)),
);
const session = createSessionFetch({
opener: createPaymentChannelSessionOpener({
signer: payer,
deposit: 1_000_000n, // 1 USDC escrowed up front; the unused part refunds at close
rpcUrl: "https://api.mainnet-beta.solana.com",
}),
// The gateway answers anonymous requests with 401 — signal the Payment scheme
// so the first attempt receives the 402 session challenge instead.
prepareRequest: ({ input, init }) => {
const headers = new Headers(init?.headers);
if (!headers.has("authorization")) headers.set("authorization", "Payment e30");
return { input, init: { ...init, headers } };
},
});
const res = await session.fetchWithSession("https://gateway.venna.net/v1/chat/completions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "venna-fast", messages: [{ role: "user", content: "ping" }] }),
});
const directive = JSON.parse(
Buffer.from(res.headers.get("Payment-Metering")!, "base64url").toString("utf8"),
);
session.recordCumulative(BigInt(directive.requiredCumulative));
await session.flush();Unlike x402 — which rides in PAYMENT-SIGNATURE and leaves
Authorization free — MPP's credential is the Authorization header, the same slot the SDK
fills with its key. So wrap fetch to clear the SDK's key, let the session own Authorization,
and feed the metering directive back after each response:
import OpenAI from "openai";
import { createSessionFetch, createPaymentChannelSessionOpener } from "@solana/mpp/client";
import { createKeyPairSignerFromBytes, getBase58Encoder } from "@solana/kit";
const payer = await createKeyPairSignerFromBytes(
new Uint8Array(getBase58Encoder().encode(process.env.PAYER_SECRET_KEY!)),
);
const session = createSessionFetch({
opener: createPaymentChannelSessionOpener({
signer: payer,
deposit: 1_000_000n,
rpcUrl: "https://api.mainnet-beta.solana.com",
}),
});
// Drop the SDK's Bearer key, hint the Payment scheme so the first attempt gets
// the 402 session challenge, then fund the next call from the metering header.
const meteredFetch: typeof fetch = async (input, init) => {
const headers = new Headers(init?.headers);
headers.set("authorization", "Payment e30"); // session overwrites this on the paid retry
const res = await session.fetchWithSession(input, { ...init, headers });
const directive = res.headers.get("Payment-Metering");
if (directive) {
const { requiredCumulative } = JSON.parse(Buffer.from(directive, "base64url").toString("utf8"));
session.recordCumulative(BigInt(requiredCumulative));
await session.flush();
}
return res;
};
const client = new OpenAI({
baseURL: "https://gateway.venna.net/v1",
apiKey: "mpp", // placeholder; the signed voucher is the auth, not a key
fetch: meteredFetch,
});
const completion = await client.chat.completions.create({
model: "venna-fast",
messages: [{ role: "user", content: "Paid straight from a Solana wallet." }],
});The session lifecycle
Open — one signature, one deposit
The first request returns 402 with a WWW-Authenticate: Payment challenge naming the program,
the USDC mint, and the deposit cap. The opener builds the escrow transaction, your wallet signs
it as payer, and the gateway co-signs as fee payer and broadcasts — the deposit is on-chain
before anything is metered.
Meter — free off-chain vouchers
Every request reserves its estimated cost against the deposit, runs, and is recorded at the metered actual. Vouchers are Ed25519 signatures from an ephemeral session key over a running total — nothing touches the chain, nothing pops a wallet prompt, and there is no per-request minimum.
Close — settle the total, refund the rest
Closing the session (or ~1 hour of inactivity — the gateway closes idle sessions so deposits
are never stranded) submits one transaction: the gateway claims exactly the vouchered total,
and deposit − spent returns to your wallet. If the gateway is unresponsive, requestClose
on-chain starts a 15-minute grace window after which anyone can finalize the refund — this
escape path is the one action that needs a little SOL for its fee.
Settlement guarantees
- Escrowed, not custodial — funds sit in the on-chain program; the gateway can only claim amounts covered by vouchers you signed.
- Metered, no minimum — each request charges the metered actual; sub-cent requests cost sub-cents.
- Refund by construction — close (cooperative or forced) always returns
deposit − spent. - Bounded exposure — at most one request is ever served ahead of your vouchers.
MPP or x402?
| MPP session (Solana) | x402 (Base) | |
|---|---|---|
| Settles | USDC on Solana mainnet | USDC on Base |
| On-chain cost | 2 transactions per session | 1 settlement per request |
| Per-request minimum | none | $0.01 |
| Best for | micro-requests, agents, sustained traffic | one-shot calls worth ≥ $0.01 |
| Upfront | escrow a deposit (refunded) | none — sign per request |
Next steps
- x402 — the per-request wallet flow on Base
- Pricing — how every payment method compares
- Subscription plans — prepaid capacity without a wallet in the loop