Gate any API route behind a Lightning payment. No accounts, no subscriptions — clients pay a Lightning invoice and get immediate access.
This is an L402-compatible implementation (bLIP-26). Any client that speaks the L402 protocol (HTTP 402 + L402 auth scheme) can interact with your API out of the box. The legacy LSAT scheme is also accepted for backwards compatibility.
How it works
Client requests a protected endpoint without credentials
Server returns 402 with a Lightning invoice and a signed token
Client pays the invoice and receives a preimage (proof of payment)
Client retries with Authorization: L402 <token>:<preimage>
Server verifies the token, expiry, and preimage — then forwards to the handler
Setup
Option A: Use an AI coding assistant (recommended)Install the MCP server and let your AI agent handle setup. When the agent asks for your email, use a real email address so you can log in to your dashboard later.Claude Code:claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/
After signup, it’s highly recommended to log in at moneydevkit.com and switch to the authenticated MCP server (see the “Existing Account” tab). This connects your agent to your account so it can manage apps, view payments, and access your dashboard. Option B: Manual setup
- Create a moneydevkit account at moneydevkit.com or run
npx @moneydevkit/create to generate credentials locally, then grab your api_key and mnemonic.
- Install the SDK:
npm install @moneydevkit/nextjs
- Add environment variables to
.env:
MDK_ACCESS_TOKEN=your_api_key_here
MDK_MNEMONIC=your_mnemonic_here
- Expose the moneydevkit endpoint:
// app/api/mdk/route.js
export { POST } from '@moneydevkit/nextjs/server/route'
- Configure Next.js:
// next.config.js / next.config.mjs
import withMdkCheckout from '@moneydevkit/nextjs/next-plugin'
export default withMdkCheckout({})
Option A: Use Replit Agent (recommended)Click the button below to install moneydevkit directly in Replit Agent. When the agent asks for your email, use a real email address so you can log in to your dashboard later.
After signup, it’s highly recommended to log in at moneydevkit.com and switch to the authenticated MCP server (see the “Existing Account” tab). This connects your agent to your account so it can manage apps, view payments, and access your dashboard. Option B: Manual setup
- Create a moneydevkit account at moneydevkit.com or run
npx @moneydevkit/create to generate credentials locally, then grab your api_key and mnemonic.
- Install the SDK (Express is a peer dependency):
npm install @moneydevkit/replit express
- Add environment variables to
.env (or Replit Secrets):
MDK_ACCESS_TOKEN=your_api_key_here
MDK_MNEMONIC=your_mnemonic_here
- Mount the moneydevkit endpoint in your Express server:
// server/index.ts
import { createMdkExpressRouter } from '@moneydevkit/replit/server/express'
app.use('/api/mdk', createMdkExpressRouter())
If you’ve already set up moneydevkit for checkouts, you can skip the steps above — L402 uses the same MDK_ACCESS_TOKEN and MDK_MNEMONIC.
Basic usage
Wrap any route handler with withPayment to require a Lightning payment:
// app/api/premium/route.ts
import { withPayment } from '@moneydevkit/nextjs/server'
const handler = async (req: Request) => {
return Response.json({ content: 'Premium data' })
}
export const GET = withPayment(
{ amount: 100, currency: 'SAT' },
handler,
)
import { withPayment } from '@moneydevkit/replit/server/express'
const handler = async (req: Request) => {
return Response.json({ content: 'Premium data' })
}
app.get('/api/premium', withPayment(
{ amount: 100, currency: 'SAT' },
handler,
))
Every request without a valid token returns a 402 with a Lightning invoice per the L402 protocol. After payment, the same request with the authorization header returns the premium data.
PaymentConfig
| Field | Type | Description |
|---|
amount | number | (req: Request) => number | Fixed amount or function that computes the price from the request |
currency | 'SAT' | 'USD' | Currency for pricing |
expirySeconds | number (optional) | Token and invoice lifetime. Default: 900 (15 minutes) |
Dynamic pricing
Pass a function instead of a fixed number to compute the price from the request:
export const POST = withPayment(
{
amount: (req: Request) => {
const url = new URL(req.url)
const tier = url.searchParams.get('tier')
if (tier === 'pro') return 500
return 100
},
currency: 'SAT',
},
handler,
)
The pricing function is evaluated both when creating the invoice and when verifying the token. If the price changes between issuance and verification (e.g., the client replays a cheap token on an expensive tier), the request is rejected with amount_mismatch.
Fiat pricing
Use currency: 'USD' to price in US cents. The SDK converts to sats at the current exchange rate when generating the invoice:
export const GET = withPayment(
{ amount: 50, currency: 'USD' }, // $0.50
handler,
)
Token expiry
Tokens and their invoices expire after 15 minutes by default. Override with expirySeconds:
export const GET = withPayment(
{ amount: 100, currency: 'SAT', expirySeconds: 300 }, // 5 minutes
handler,
)
Client integration
Any HTTP client can consume an L402 endpoint.
curl
# 1. Request the protected resource
curl -s https://example.com/api/premium
# Response: 402
# {
# "macaroon": "eyJ...",
# "invoice": "lnbc...",
# "paymentHash": "abc123...",
# "amountSats": 100,
# "expiresAt": 1234567890
# }
# 2. Pay the invoice with any Lightning wallet and get the preimage
# 3. Retry with the token and preimage
curl -s https://example.com/api/premium \
-H "Authorization: L402 eyJ...:ff00aa..."
# Response: 200 { "content": "Premium data" }
The WWW-Authenticate header follows the bLIP-26 format:
WWW-Authenticate: L402 macaroon="eyJ...", invoice="lnbc..."
Programmatic (Node.js / AI agent)
async function callPaidEndpoint(
url: string,
payFn: (invoice: string) => Promise<string>,
) {
// Step 1: get the 402 challenge
const challenge = await fetch(url)
if (challenge.status !== 402) return challenge
const { macaroon, invoice } = await challenge.json()
// Step 2: pay the invoice (returns preimage)
const preimage = await payFn(invoice)
// Step 3: retry with token + proof of payment
return fetch(url, {
headers: { Authorization: `L402 ${macaroon}:${preimage}` },
})
}
Error codes
| Status | Code | Meaning |
|---|
| 402 | payment_required | No valid token — pay the returned invoice |
| 401 | invalid_credential | Token is malformed or has a bad signature |
| 401 | invalid_payment_proof | Preimage does not match the payment hash |
| 403 | resource_mismatch | Token was issued for a different endpoint |
| 403 | amount_mismatch | Token was issued for a different price |
| 500 | configuration_error | MDK_ACCESS_TOKEN is not set |
| 500 | pricing_error | Dynamic pricing function threw an error |
| 502 | checkout_creation_failed | Failed to create the checkout or invoice |