vennaVenna

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)
SettlesUSDC on Solana mainnetUSDC on Base
On-chain cost2 transactions per session1 settlement per request
Per-request minimumnone$0.01
Best formicro-requests, agents, sustained trafficone-shot calls worth ≥ $0.01
Upfrontescrow 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

On this page