Skip to main content
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

1

Client requests a protected endpoint without credentials

2

Server returns 402 with a Lightning invoice and a signed token

3

Client pays the invoice and receives a preimage (proof of payment)

4

Client retries with Authorization: L402 <token>:<preimage>

5

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
  1. Create a moneydevkit account at moneydevkit.com or run npx @moneydevkit/create to generate credentials locally, then grab your api_key and mnemonic.
  2. Install the SDK:
    npm install @moneydevkit/nextjs
    
  3. Add environment variables to .env:
    MDK_ACCESS_TOKEN=your_api_key_here
    MDK_MNEMONIC=your_mnemonic_here
    
  4. Expose the moneydevkit endpoint:
    // app/api/mdk/route.js
    export { POST } from '@moneydevkit/nextjs/server/route'
    
  5. Configure Next.js:
    // next.config.js / next.config.mjs
    import withMdkCheckout from '@moneydevkit/nextjs/next-plugin'
    
    export default withMdkCheckout({})
    
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,
)
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

FieldTypeDescription
amountnumber | (req: Request) => numberFixed amount or function that computes the price from the request
currency'SAT' | 'USD'Currency for pricing
expirySecondsnumber (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

StatusCodeMeaning
402payment_requiredNo valid token — pay the returned invoice
401invalid_credentialToken is malformed or has a bad signature
401invalid_payment_proofPreimage does not match the payment hash
403resource_mismatchToken was issued for a different endpoint
403amount_mismatchToken was issued for a different price
500configuration_errorMDK_ACCESS_TOKEN is not set
500pricing_errorDynamic pricing function threw an error
502checkout_creation_failedFailed to create the checkout or invoice