Help us improve
Share bugs, ideas, or general feedback.
From stellar-dev
Implements x402 (paid APIs via OZ Channels) and MPP (Machine Payments Protocol) on Stellar for AI-agent and machine-to-machine payments.
npx claudepluginhub stellar/stellar-dev-skill --plugin stellar-devHow this skill is triggered — by the user, by Claude, or both
Slash command
/stellar-dev:agentic-payments [payment task][payment task]The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Two complementary protocols for AI-agent and machine-to-machine payments on Stellar. Pick based on who depends on whom and how often the agent pays.
Dispatches payments across x402 exact/aggr_deferred schemes, MPP charge/session intents, and a2a-pay flows. Detects HTTP 402 responses or paymentIds, loads scheme references, and generates auth headers or tx-hashes.
Enables AI agents to make policy-gated x402 payments with per-task budgets, spending controls, and non-custodial wallets. Supports Base and X Layer.
Pays HTTP 402 challenges for machine-payable APIs using Tempo CLI requests and Uniswap swaps to fund wallet from any EVM token.
Share bugs, ideas, or general feedback.
Two complementary protocols for AI-agent and machine-to-machine payments on Stellar. Pick based on who depends on whom and how often the agent pays.
| x402 | MPP Charge | MPP Channel | |
|---|---|---|---|
| Per-request on-chain tx? | Yes (via facilitator) | Yes (Soroban SAC) | No (off-chain commits) |
| Needs facilitator? | Yes (OZ Channels) | No | No |
| Client needs XLM? | No (fees sponsored) | Optional (feePayer) | Yes |
| Setup complexity | Low | Low | Medium (deploy contract first) |
| Best for | Quickest setup, fee-free clients | No third-party dep | High-frequency agents |
All protocols use USDC (SEP-41 SAC) by default; stellar:testnet / stellar:pubnet CAIP-2 network IDs.
../soroban/SKILL.md../assets/SKILL.md../dapp/SKILL.md../data/SKILL.md../standards/SKILL.mdx402 is the right choice when:
Trade-off: you depend on OZ Channels (or a self-hosted relayer) for verification and settlement. If you need zero third-party dependency, use MPP Charge (Part 2 below) instead.
Client → GET /resource → Server
Client ← 402 Payment Required (payment requirements) ← Server
Client builds Soroban SAC USDC transfer
Client signs auth entries only (not the full tx envelope)
Client → GET /resource + X-PAYMENT header → Server
Server → OZ Channels /verify + /settle → Stellar (~5s)
Client ← 200 OK + resource
The key Stellar difference: clients sign auth entries, not full transaction envelopes. The facilitator assembles the transaction, pays fees, and submits. Clients need zero XLM.
npm install @x402/express @x402/core @x402/stellar express dotenv
npm pkg set type=module
// server.js
import express from "express";
import { paymentMiddlewareFromConfig } from "@x402/express";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { ExactStellarScheme } from "@x402/stellar/exact/server";
const app = express();
const facilitator = new HTTPFacilitatorClient({
url: process.env.FACILITATOR_URL ?? "https://channels.openzeppelin.com/x402/testnet",
// omit createAuthHeaders on testnet if you don't have an API key yet
createAuthHeaders: process.env.OZ_API_KEY
? async () => {
const h = { Authorization: `Bearer ${process.env.OZ_API_KEY}` };
return { verify: h, settle: h, supported: h };
}
: undefined,
});
app.use(
paymentMiddlewareFromConfig(
{
"GET /weather": {
description: "Current weather data",
// human-readable price string — auto-converts to USDC base units
price: "$0.001",
network: "stellar:testnet",
payTo: process.env.STELLAR_RECIPIENT, // your G... address
},
},
{ facilitator, schemes: [ExactStellarScheme] }
)
);
app.get("/weather", (_req, res) => {
res.json({ city: "San Francisco", temp: 18, conditions: "Foggy" });
});
app.listen(3001, () => console.log("x402 server on http://localhost:3001"));
Env vars:
STELLAR_RECIPIENT — your G... address (receives USDC)OZ_API_KEY — OZ Channels API key (optional on testnet, required on mainnet)FACILITATOR_URL — defaults to testnet URL abovePrice format options:
"$0.001" — human-readable, auto-converts to 7-decimal USDC units{ amount: "1000", asset: "ASSET_SAC_CONTRACT_ID" } — explicit base units for non-USDC assetsnpm install @x402/fetch @x402/stellar @stellar/stellar-sdk dotenv
npm pkg set type=module
// client.js
import { x402HTTPClient } from "@x402/fetch";
import { createEd25519Signer, getNetworkPassphrase } from "@x402/stellar";
import { ExactStellarScheme } from "@x402/stellar/exact/client";
import * as StellarSdk from "@stellar/stellar-sdk";
const keypair = StellarSdk.Keypair.fromSecret(process.env.STELLAR_SECRET_KEY);
const network = "stellar:testnet";
// createEd25519Signer wraps the keypair for auth-entry signing
const signer = createEd25519Signer(keypair, getNetworkPassphrase(network));
// x402HTTPClient wraps fetch — handles 402 negotiation transparently
const client = x402HTTPClient({ signer, schemes: [ExactStellarScheme] });
const res = await client.fetch("http://localhost:3001/weather");
console.log(await res.json());
// Paid automatically: 402 negotiation + auth-entry signing happens under the hood
Env vars:
STELLAR_SECRET_KEY — your S... secret key (needs USDC trustline + balance)Generate a keypair
node -e "const { Keypair } = require('@stellar/stellar-sdk'); const kp = Keypair.random(); console.log('Public:', kp.publicKey()); console.log('Secret:', kp.secret());"
Fund with testnet XLM
curl "https://friendbot.stellar.org?addr=YOUR_PUBLIC_KEY"
Add USDC trustline — open Stellar Lab, or via SDK:
import * as StellarSdk from "@stellar/stellar-sdk";
const server = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");
const keypair = process.env.STELLAR_SECRET_KEY
? StellarSdk.Keypair.fromSecret(process.env.STELLAR_SECRET_KEY)
: StellarSdk.Keypair.random();
const account = await server.loadAccount(keypair.publicKey());
const tx = new StellarSdk.TransactionBuilder(account, {
fee: "100",
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.changeTrust({
asset: new StellarSdk.Asset("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"),
}),
)
.setTimeout(30)
.build();
tx.sign(keypair);
await server.submitTransaction(tx);
Get testnet USDC — use the Circle testnet faucet (select Stellar testnet)
Get an OZ Channels testnet API key (optional for testnet, required for mainnet):
| Config | Value |
|---|---|
| Network ID | stellar:pubnet |
| RPC URL | Provider-specific endpoint (see Stellar RPC providers directory) |
| Facilitator URL | https://channels.openzeppelin.com/x402 |
| USDC SAC | CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75 |
| Funding | Real USDC on mainnet (CEX, DEX, or bridge) |
Always test on testnet first. Switch by changing network and FACILITATOR_URL.
Auth entry signing — On Stellar, x402 clients sign Soroban authorization entries, not full transaction envelopes. The facilitator assembles the complete transaction. This is lighter than EVM/Solana signing, and means clients never need to manage sequence numbers or pay fees.
Fee sponsorship — OZ Channels pays all Stellar network fees (~$0.00001/tx). Clients need a funded wallet with USDC but zero XLM.
exact-v2 scheme — The Stellar x402 scheme version. Server advertises scheme: "exact" + x402Version: 2. Don't mix v1 and v2 packages.
SAC (Stellar Asset Contract) — USDC on Stellar is a classic asset wrapped in a Soroban contract. x402 payments invoke transfer on the SAC. Any SEP-41 token works; USDC is the default.
Ledger expiration — Auth entries include a max_ledger bound. Use latestLedger + 12 (~1 minute at 5s/ledger). Expired entries fail at settlement.
CAIP-2 network IDs — stellar:testnet and stellar:pubnet. These are the exact strings the protocol expects.
Auth entry expired on settle
isValid: false, error mentions ledger expirationlatestLedger + 12 (or higher) as expiration; don't cache auth entries across requestsWrong USDC decimal precision
$0.001 = 10000 in base units.V1/V2 package mismatch
@x402/* packages at the same major version. V2 is multi-chain; don't import V1 @x402/core alongside V2 @x402/stellar.Missing USDC trustline
op_no_trust error during settlementchangeTrust operation before attempting any x402 payment (see testnet runbook above)OZ Channels 401 on mainnet
Authorization: Bearer header — generate one at channels.openzeppelin.com/genMPP is the right choice when:
Two modes:
| Mode | On-chain txs | Best for |
|---|---|---|
| Charge | One per request | Per-request payments, no pre-funding required |
| Channel | One deposit + one close | High-frequency agents (100s of requests/session) |
If you need zero-XLM clients or the simplest possible setup, use x402 (Part 1 above) instead.
Each request triggers a Soroban SAC token transfer settled on-chain. No facilitator. Server can optionally sponsor fees so clients don't need XLM.
npm install express @stellar/mpp mppx @stellar/stellar-sdk dotenv
npm pkg set type=module
Server:
// charge-server.js
import express from "express";
import { Mppx } from "mppx";
import * as stellar from "@stellar/mpp/charge/server";
import * as StellarSdk from "@stellar/stellar-sdk";
const USDC_SAC_TESTNET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const RECIPIENT = process.env.STELLAR_RECIPIENT; // G... address
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY, // shared secret for credential verification
methods: [
stellar.charge({
recipient: RECIPIENT,
currency: USDC_SAC_TESTNET,
network: "stellar:testnet",
// optional: server pays network fees so clients don't need XLM
feePayer: process.env.FEE_PAYER_SECRET
? { envelopeSigner: StellarSdk.Keypair.fromSecret(process.env.FEE_PAYER_SECRET) }
: undefined,
}),
],
});
const app = express();
app.use(express.json());
// mppx middleware: returns 402 with challenge, then validates payment on retry
app.use(mppx.middleware());
app.get("/data", (req, res) => {
res.json({ result: "paid content", price: "$0.001 USDC" });
});
app.listen(3002, () => console.log("MPP charge server on http://localhost:3002"));
Client:
// charge-client.js
import { Mppx } from "mppx";
import * as stellar from "@stellar/mpp/charge/client";
import * as StellarSdk from "@stellar/stellar-sdk";
const keypair = StellarSdk.Keypair.fromSecret(process.env.STELLAR_SECRET_KEY);
const mppx = Mppx.create({
methods: [
stellar.charge({
keypair,
mode: "pull", // server assembles and broadcasts the transaction
onProgress(event) {
// event.type: "challenge" | "signed" | "settled"
if (event.type === "settled") console.log("Settled:", event.txHash);
},
}),
],
});
// mppx wraps fetch — 402 handling is transparent
const res = await mppx.fetch("http://localhost:3002/data");
console.log(await res.json());
Env vars (server): STELLAR_RECIPIENT, MPP_SECRET_KEY, FEE_PAYER_SECRET (optional)
Env vars (client): STELLAR_SECRET_KEY
mode: "pull" vs "push":
"pull" — client signs auth entries, server assembles + broadcasts (default; use with feePayer)"push" — client builds and broadcasts the transaction directly (client must have XLM for fees)The client deploys a one-way payment channel contract, deposits USDC once, then signs cumulative commitments off-chain for each request. No transaction per request — only two on-chain txs total (deposit + close). Ideal for AI agents making hundreds of calls in a session.
1. Deploy channel contract (one-time) → C... contract address
2. Client deposits USDC into channel → on-chain tx
3. Per request: client signs commitment → off-chain (just a signature)
Amount is cumulative: each sig covers all previous payments + this one
4. Server closes channel when done → on-chain tx, settles total
C... contract address// channel-server.js
import express from "express";
import { Mppx, Store } from "mppx";
import * as stellar from "@stellar/mpp/channel/server";
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY,
methods: [
stellar.channel({
channel: process.env.CHANNEL_CONTRACT, // C... contract address
commitmentKey: process.env.COMMITMENT_PUBKEY, // 64-char hex ed25519 public key
store: Store.memory(), // dev only — use persistent store in production
network: "stellar:testnet",
}),
],
});
const app = express();
app.use(express.json());
app.use(mppx.middleware());
app.get("/data", (req, res) => {
res.json({ result: "paid content" });
});
app.listen(3003);
// channel-client.js
import { Mppx } from "mppx";
import * as stellar from "@stellar/mpp/channel/client";
import * as StellarSdk from "@stellar/stellar-sdk";
// commitment key must be a raw ed25519 seed — NOT a standard Stellar secret key
const commitmentKey = StellarSdk.Keypair.fromRawEd25519Seed(
Buffer.from(process.env.COMMITMENT_SECRET, "hex") // 64-char hex secret
);
const mppx = Mppx.create({
methods: [
stellar.channel({
commitmentKey,
onProgress(event) {
// event.type: "challenge" | "signed"
},
}),
],
});
// Make many requests — each signs a cumulative off-chain commitment
for (let i = 0; i < 100; i++) {
const res = await mppx.fetch("http://localhost:3003/data");
console.log(i, await res.json());
}
import { close } from "@stellar/mpp/channel/server";
import * as StellarSdk from "@stellar/stellar-sdk";
const txHash = await close({
channel: process.env.CHANNEL_CONTRACT,
amount: lastCumulativeAmount, // bigint, total USDC owed in base units
signature: lastCommitmentSignature, // hex string from final commitment
feePayer: { envelopeSigner: StellarSdk.Keypair.fromSecret(process.env.FEE_PAYER_SECRET) },
network: "stellar:testnet",
});
// Single on-chain transaction settles the full session
console.log("Channel closed:", txHash);
Env vars (server): CHANNEL_CONTRACT, COMMITMENT_PUBKEY, MPP_SECRET_KEY, FEE_PAYER_SECRET
Env vars (client): COMMITMENT_SECRET
npm install @stellar/mpp mppx @stellar/stellar-sdk
| Import path | Recommended import pattern |
|---|---|
@stellar/mpp/charge/server | import * as stellar from "@stellar/mpp/charge/server" — use stellar.charge(...) |
@stellar/mpp/charge/client | import * as stellar from "@stellar/mpp/charge/client" — use stellar.charge(...) |
@stellar/mpp/channel/server | import * as stellar from "@stellar/mpp/channel/server" — use stellar.channel(...), stellar.close(...), stellar.getChannelState(...), stellar.watchChannel(...) |
@stellar/mpp/channel/client | import * as stellar from "@stellar/mpp/channel/client" — use stellar.channel(...) |
@stellar/mpp/channel | Zod schema definitions for channel types |
mppx | import { Mppx, Store } from "mppx" |
Steps shared with all protocols:
Channel mode only: 4. Deploy the one-way-channel contract (see stellar-mpp-sdk for deploy script) 5. Generate a 64-char hex ed25519 seed for the commitment key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Channel: wrong commitment key format
Keypair.fromRawEd25519Seed throws or signatures fail to verifyS... secret key. Generate with crypto.randomBytes(32).toString('hex').Channel: non-cumulative amounts
amount must be the running total of all payments so far, not just the price of the current request. The server tracks the highest-seen commitment.Channel: deposit TTL expired
close() fails or channel appears drainedbumpContractInstance. Don't leave channels open indefinitely.Charge: client has no XLM for fees
op_insufficient_balance or fee errors on client-submitted transactionsmode: "pull" on the client and configure feePayer on the server so the server pays fees. The client only signs auth entries.Store.memory() in production
Store.memory() with a persistent store (database-backed) before going to production.