MPP
Charge agents and apps for API calls using the Machine Payments Protocol. For Bitcoin payments, use MPP's Lightning charge flow with your Phoenixd node as the invoice backend.
Start with a single paid endpoint, such as a query that costs 100 sats. Your application sets the price and serves the result; your node receives the Lightning payment.
Connect MPP to your node
Use @nodana/phoenixd-mpp-sdk as the adapter between MPP and your Phoenixd node. It supplies the Lightning payment method to mppx, which handles payment challenges, credentials and receipts. @nodana/phoenixd-ts connects the adapter to your node's API.
Install the adapter and its dependencies in your backend service:
npm install @nodana/phoenixd-mpp-sdk @nodana/phoenixd-ts mppxRun your payment handler in a Node.js 22.12 or later backend. Configure these secrets in your service:
PHOENIXD_URL: your node's HTTPS endpoint.PHOENIXD_PASSWORD: your node's restricted password (http-password-limited-access). The receiving adapter creates and decodes invoices, so it does not need spending permission. See Phoenixd API authentication.MPP_SECRET_KEY: a signing secret for your payment challenges. Generate one withopenssl rand -hex 32and keep the same value across server instances and restarts.
Gate a request with MPP
Pass a configured Phoenixd client to the adapter, then check payment before running your query:
import { Phoenixd } from "@nodana/phoenixd-ts";
import { Mppx, phoenixd } from "@nodana/phoenixd-mpp-sdk/server";
const node = new Phoenixd(process.env.PHOENIXD_URL!, process.env.PHOENIXD_PASSWORD!, {
timeoutMs: 15_000,
});
const payments = Mppx.create({
methods: [phoenixd.charge({ client: node })],
secretKey: process.env.MPP_SECRET_KEY!,
});
export async function handler(request: Request): Promise<Response> {
const payment = await payments.charge({
amount: "100",
description: "Paid API query",
})(request);
if (payment.status === 402) return payment.challenge;
const result = { answer: "Your paid query result" };
return payment.withReceipt(Response.json(result));
}Mount the handler at your paid endpoint. amount: "100" charges 100 sats. An unpaid request receives a payment challenge, then a paid request receives the result and a receipt. Replace the sample result with your query or tool call, keeping that work after the payment check.
Use Redis for your deployed service
The example uses in-memory payment state for development. For deployed services, pass a connected Redis client to redisStore. The adapter handles validation and rejects reused payments; you do not need to implement those checks.
Install the Redis client if your application does not already use it:
npm install redisimport { createClient } from "redis";
import { redisStore } from "@nodana/phoenixd-mpp-sdk/server";
const redis = createClient({ url: process.env.REDIS_URL! });
redis.on("error", () => console.error("Redis connection error"));
await redis.connect();
const payments = Mppx.create({
methods: [phoenixd.charge({ client: node, store: redisStore(redis) })],
secretKey: process.env.MPP_SECRET_KEY!,
});Reuse your application's existing connected Redis client if you have one. Use the same persistent Redis service and signing secret across server instances. Expired payment records are removed automatically.
Set the price and expiry
Choose a price for each endpoint. To give the client one minute to complete payment, set an expiry on the charge:
const payment = await payments.charge({
amount: "100",
expires: new Date(Date.now() + 60_000),
})(request);The adapter caps the challenge expiry to the invoice lifetime. Each accepted payment authorises one request; clients retry the same URL, method and body to receive the result.
Test the endpoint
Inspect your deployed endpoint without paying:
curl -i https://api.example.com/queryExpect HTTP 402 with an MPP payment challenge. Then use a Lightning-capable MPP client to make a paid request and check that it receives your result and a receipt. Test expired invoices and repeated credentials before making the endpoint available to customers.
If invoice creation fails, check the node URL, API password and node status. If payment succeeds but access fails, check that the retry uses the same request before expiry and that every server instance has the same signing secret and Redis service. Check outgoing payment history before paying again when the payment outcome is uncertain.