# Agent Skills Source: https://docs.moneydevkit.com/agent-skills Install Money Dev Kit skills for coding agents Money Dev Kit agent skills teach coding agents how to add Lightning payments to your app. Use them to set up checkout, connect the MCP server, build L402 paid APIs, and work with the Money Dev Kit agent wallet. Skills give your agent reusable Money Dev Kit implementation guidance. For live account and app operations, use the [Money Dev Kit MCP server](/nextjs#install-with-ai-coding-tools) alongside these skills. ## Install Install the Money Dev Kit skills with the [Skills CLI](https://vercel.com/docs/agent-resources/skills): ```bash theme={null} npx skills add moneydevkit/agent-skills ``` To install one skill from the collection: ```bash theme={null} npx skills add moneydevkit/agent-skills --skill mdk-nextjs-checkout ``` To list the available skills before installing: ```bash theme={null} npx skills add moneydevkit/agent-skills --list ``` ## Available skills | Skill | Use it for | | --------------------- | ------------------------------------------------------------------------- | | `mdk-nextjs-checkout` | Add `@moneydevkit/nextjs` checkout to a Next.js App Router app | | `mdk-replit-checkout` | Add `@moneydevkit/replit` checkout to a Replit Vite + React + Express app | | `mdk-l402-api` | Protect API routes with L402 pay-per-call payments | | `mdk-mcp` | Install or choose the Money Dev Kit MCP endpoint | | `mdk-agent-wallet` | Use `@moneydevkit/agent-wallet` for agent Lightning payments | ## Example prompts ```text theme={null} Use $mdk-nextjs-checkout to add Money Dev Kit checkout to this Next.js App Router app. Use MCP to create or connect the Money Dev Kit app if available. ``` ```text theme={null} Use $mdk-replit-checkout to add Money Dev Kit checkout to this Replit Vite, React, and Express app. Follow the Replit docs exactly. ``` ```text theme={null} Use $mdk-l402-api to protect this API route with Money Dev Kit L402 payments and add agent-readable usage docs. ``` ```text theme={null} Use $mdk-agent-wallet to initialize an agent wallet, check its balance, and pay this L402 endpoint. ``` ## Update Refresh installed skills with: ```bash theme={null} npx skills update ``` Each Money Dev Kit skill includes a version, review date, and canonical docs link. If your agent is unsure about an API or package behavior, tell it to compare the skill with the latest docs index at [llms.txt](https://docs.moneydevkit.com/llms.txt). # Agent Wallet Source: https://docs.moneydevkit.com/agent-wallet Self-custodial Lightning wallet for AI agents. No API keys required. A self-custodial Lightning wallet designed for AI agents. Run it as a CLI — every command outputs JSON to stdout for easy parsing. No API keys, no accounts, no webhooks. The agent wallet runs a local daemon that leverages moneydevkit's lightning infrastructure. No node management required. ## Quick start ```bash theme={null} npx @moneydevkit/agent-wallet@latest init ``` This generates a mnemonic and saves it to `~/.mdk-wallet/config.json`. Back up the mnemonic — it controls your funds. ```bash theme={null} npx @moneydevkit/agent-wallet@latest receive 1000 ``` Returns a BOLT11 invoice. The daemon auto-starts and polls for incoming payments. ```bash theme={null} npx @moneydevkit/agent-wallet@latest send user@getalby.com 500 ``` Supports bolt11, bolt12, LNURL, and Lightning addresses. ```bash theme={null} npx @moneydevkit/agent-wallet@latest balance ``` ## How it works The CLI automatically starts a daemon on first command. The daemon: * Runs a local HTTP server on `localhost:3456`, gated by a bearer token * Polls for incoming payments every 30 seconds * Persists payment history to `~/.mdk-wallet/` No lightning infrastructure needed — the daemon handles everything locally. ## Commands | Command | Description | | -------------------------------------- | ----------------------------------------------- | | `init` | Generate mnemonic, create config | | `init --show` | Show config (mnemonic redacted) | | `init --network signet` | Initialize on signet testnet | | `start` | Start the daemon | | `stop` | Stop the daemon | | `restart` | Restart the daemon | | `status` | Check if daemon is running | | `balance` | Get balance in sats | | `receive ` | Generate BOLT11 invoice | | `receive` | Generate variable-amount invoice | | `receive --description "..."` | Invoice with custom description | | `receive-bolt12` | Generate BOLT12 offer (variable amount) | | `send [amount]` | Pay bolt11, bolt12, LNURL, or Lightning address | | `payments` | List payment history | ## Output format All commands output JSON to stdout. Exit code `0` means success, `1` means error. ```bash theme={null} $ npx @moneydevkit/agent-wallet@latest balance {"balance_sats":50000} $ npx @moneydevkit/agent-wallet@latest receive 1000 {"invoice":"lnbc10n1...","payment_hash":"abc123...","expires_at":"2024-01-15T12:00:00.000Z"} $ npx @moneydevkit/agent-wallet@latest send user@example.com 500 {"payment_hash":"def456..."} ``` ## Supported destinations The `send` command auto-detects the destination type: | Format | Example | | ----------------- | -------------------------------- | | Bolt11 | `lnbc...`, `lntb...`, `lntbs...` | | Bolt12 | `lno...` | | LNURL | `lnurl...` | | Lightning Address | `user@domain.com` | Amount is optional for bolt11 invoices that already include an amount. ## Configuration Config is stored in `~/.mdk-wallet/config.json`: ```json theme={null} { "mnemonic": "word word word ...", "walletId": "uuid" } ``` Environment variable overrides: | Variable | Description | | --------------------- | ----------------------------- | | `MDK_WALLET_MNEMONIC` | Override mnemonic | | `MDK_WALLET_PORT` | Server port (default: `3456`) | ## Local API security The daemon's local HTTP API is authenticated. Every route requires a bearer token from `~/.mdk-wallet/auth.token` (mode `0600`, minted on first use), and requests that do not come from local tooling get `401 UNAUTHORIZED`. The CLI handles this for you. Guard `auth.token` like the mnemonic: whoever reads it can spend the balance. It is not a boundary against code running as your own user, which can read the mnemonic in the same directory. After upgrading, run `restart` so the new binary serves the API. ## AI agent integration This wallet is designed for AI agents that need to send and receive Lightning payments. Every command outputs JSON for easy parsing, and the daemon auto-starts on first use. ### Paying L402 endpoints The agent wallet pairs naturally with [L402](/l402) pay-per-call APIs. Simply pay the invoice and return the token and preimage to unlock the API. ## Upgrading ```bash theme={null} # Stop the running daemon npx @moneydevkit/agent-wallet@latest stop # Run with @latest to pull the newest version npx @moneydevkit/agent-wallet@latest start ``` Your wallet config and payment history in `~/.mdk-wallet/` are preserved across upgrades. ## Troubleshooting If the wallet becomes unresponsive (commands hang or return no output), restart the daemon: ```bash theme={null} npx @moneydevkit/agent-wallet@latest restart ``` `init` will refuse to overwrite an existing wallet. To reinitialize, stop the daemon and delete `~/.mdk-wallet/` first. **Back up your mnemonic before deleting** — it controls your funds. # Checkouts Source: https://docs.moneydevkit.com/dashboard/checkouts Monitor checkout sessions and conversion in the moneydevkit dashboard Checkouts are payment sessions created when customers initiate a purchase. The checkouts page gives you visibility into your checkout flow and helps identify where customers may be dropping off. ## What are Checkouts? A checkout is created when a customer starts the payment process. Checkouts track the entire payment journey from initiation to completion (or abandonment). ## Checkouts Dashboard The checkouts page in your [moneydevkit dashboard](https://moneydevkit.com/dashboard/checkouts) displays all checkout sessions with: * **Checkout ID** - Unique identifier for the session * **Status** - Current state of the checkout (pending, completed, expired) * **Amount** - The checkout amount and currency * **Created Date** - When the checkout was initiated * **Customer** - Associated customer information (if provided) * **Invoice** - Bitcoin invoice details ## Checkout Statuses | Status | Description | | ------------- | -------------------------------------------- | | **Pending** | Checkout created, awaiting payment | | **Completed** | Payment received and confirmed | | **Expired** | Checkout expired before payment was received | ## Analyzing Your Checkout Flow Checkouts help you understand: * **Conversion rate** - Compare total checkouts to completed orders * **Drop-off points** - Identify where customers abandon the payment process * **Payment timing** - See how long customers take to complete payments High numbers of pending or expired checkouts may indicate friction in your checkout experience. Consider simplifying your checkout flow or providing clearer payment instructions. ## Creating Checkouts Checkouts are created programmatically using the moneydevkit SDK: ```tsx theme={null} import { useCheckout } from '@moneydevkit/nextjs' const { createCheckout, isLoading } = useCheckout() // Create a checkout const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', description: 'Product description', amount: 500, // Amount in cents (USD) or sats (SAT) currency: 'USD', successUrl: '/checkout/success', metadata: { orderId: 'order-123' } }) if (result.error) { console.error(result.error.message) return } window.location.href = result.data.checkoutUrl ``` ### Requiring Customer Information Use `requireCustomerData` to collect customer information during checkout. The checkout will display a form for customers to enter the specified fields: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', requireCustomerData: ['email', 'name'] }) ``` See the [Customers documentation](/dashboard/customers) for more details on collecting customer data. See the [Replit integration guide](/replit) or [Next.js integration guide](/nextjs) for complete setup instructions. # Customers Source: https://docs.moneydevkit.com/dashboard/customers Manage customer information and order history in the moneydevkit dashboard Customers are people who have completed orders through your moneydevkit checkout. The customers page helps you manage customer relationships, issue refunds, and track purchase history. ## What are Customers? Customers let you collect information on people who complete orders so you can: * **Contact them later** - Reach out for support, updates, or marketing * **Issue refunds** - Process refunds to their refund address * **Track purchases** - View their complete order history ## Customers Dashboard The customers page in your [moneydevkit dashboard](https://moneydevkit.com/dashboard/customers) displays: * **Customer list** - All customers with name and email * **Search** - Find customers by name or email * **Customer details** - View individual customer information and order history ### Customer Information Each customer record includes: * **Name** - Customer's name (if provided) * **Email** - Customer's email address * **Refund Address** - Lightning address for processing refunds * **Order Count** - Number of completed orders * **Checkout Count** - Number of checkout sessions * **Order History** - List of recent orders with amounts and dates * **Subscriptions** - Active and past subscriptions ## Collecting Customer Information Customer information can be collected in two ways: by passing it programmatically when creating a checkout, or by requiring customers to fill out a form during checkout. ### Option 1: Pass Customer Data Directly If you already know the customer's information (e.g., from your authentication system), pass it when creating the checkout: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', customer: { email: 'customer@example.com', name: 'Jane Doe', externalId: 'user-123' // Your system's user ID } }) ``` ### Option 2: Require Customer Data Entry Use `requireCustomerData` to display a form that collects customer information before payment. This is useful when you don't have the customer's details upfront: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', requireCustomerData: ['email', 'name'] }) ``` The checkout will display a form requiring the specified fields before the customer can proceed to payment. #### Available Fields | Field | Description | | ------------- | -------------------------------------------------------- | | `email` | Customer's email address | | `name` | Customer's full name | | Custom fields | Any additional field name (e.g., `'company'`, `'phone'`) | Field names are case-sensitive and must match exactly (e.g., `taxId` in `requireCustomerData` requires `taxId` in `customer`, not `tax_id`). #### Combining Both Approaches You can combine pre-filled data with required fields. The form will only show fields that aren't already provided: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', customer: { externalId: 'user-123', // Known from your auth system name: 'Jane Doe' // Already known }, requireCustomerData: ['email', 'name', 'company'] // Form will only ask for email and company since name is already provided }) ``` If you pass an `externalId` for an existing customer, moneydevkit will automatically backfill any fields they've previously provided. This means returning customers may not need to fill out the form at all. #### Using externalId for Authenticated Users When your user is already authenticated in your app, pass `externalId` to link checkouts to their account: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: "Premium Plan", description: 'Monthly subscription', amount: 1000, currency: 'USD', successUrl: '/checkout/success', customer: { externalId: user.id, // Your app's user ID name: user.name, email: user.email, }, requireCustomerData: ['name', 'email'], }) ``` When `externalId` is provided: * The system assumes the user is authenticated * If the customer already exists (matched by `externalId`), their stored `name` and `email` are used * Only fields missing from the customer record are requested * This prevents authenticated users from being asked for data you already have ### Customer Matching Customers are matched and deduplicated using: 1. **External ID** - If you provide an `externalId`, existing customers with that ID are found and **updated** with new email/name/metadata from the checkout 2. **Email** - If no external ID match, customers are matched by email and **linked without updating** (existing data is preserved) This ensures you don't create duplicate customer records when the same person makes multiple purchases. **Email is required** to create a customer record. Without an email, customer data is attached to the checkout but no customer record is created. When using `externalId` for authenticated users, be aware that checkout data will overwrite existing customer fields. If you want to preserve existing data, omit fields you don't want to change. ## Managing Customers ### Editing Customers Click the edit button to update customer information: * Name * Email * Refund address * Custom metadata ### Refunds To issue a refund: 1. Find the customer in the customers list 2. View their order history 3. Use the refund address to send funds back Refunds are processed manually by sending to the customer's refund address. Ensure the refund address is valid before sending funds. ## Customer Metadata You can store additional information in customer metadata: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', customer: { email: 'customer@example.com', name: 'Jane Doe', // Custom fields are stored in userMetadata shippingAddress: '123 Main St', phoneNumber: '+1-555-0123' } }) ``` Custom fields beyond `name`, `email`, and `externalId` are stored in the customer's metadata and visible in the dashboard. ## Fetching Customer Data Use the `useCustomer` hook to fetch customer data in your application, including subscription status: ```tsx theme={null} import { useCustomer } from '@moneydevkit/nextjs' function CustomerProfile() { const { customer, isLoading, error, refetch } = useCustomer({ externalId: user.id }) if (isLoading) return

Loading...

if (error) return

Error: {error.message}

if (!customer) return

Customer not found

return (

Email: {customer.email}

Name: {customer.name}

Has active subscription: {customer.hasActiveSubscription ? 'Yes' : 'No'}

) } ``` ### CustomerIdentifier Types You can identify customers using any of these options: | Identifier | Description | Example | | ------------ | ------------------------ | ------------------------------- | | `externalId` | Your app's user ID | `{ externalId: user.id }` | | `email` | Customer's email address | `{ email: 'user@example.com' }` | | `customerId` | moneydevkit customer ID | `{ customerId: 'cus_abc123' }` | Use `externalId` when you have authenticated users. This links moneydevkit customers to your user accounts and enables features like checking subscription status. See the [Subscriptions guide](/subscriptions) for more details on managing recurring payments. # Orders Source: https://docs.moneydevkit.com/dashboard/orders Track paid checkouts and revenue in the moneydevkit dashboard Orders represent completed payments in your moneydevkit account. When a customer completes a checkout, it becomes an order with associated payment details and customer information. ## What are Orders? Orders are paid checkouts, products, or subscriptions. Each order represents a successful transaction where the customer has completed payment. ## Orders Dashboard The orders page in your [moneydevkit dashboard](https://moneydevkit.com/dashboard/orders) displays: ### Metrics Overview * **Total Orders** - The cumulative count of all completed orders * **Today's Revenue** - Revenue generated from orders placed today * **Cumulative Revenue** - Total revenue across all orders ### Order Details Each order includes: * **Order ID** - Unique identifier for the order * **Customer** - Associated customer information (if provided) * **Amount** - Total amount paid including taxes * **Currency** - The currency of the transaction * **Status** - Current order status * **Created Date** - When the order was placed * **Invoice** - Associated Bitcoin invoice details ## Viewing Order Details Click on any order to see the full details including: * Payment breakdown (subtotal, tax, total) * Bitcoin payment information (BTC amount, price at time of payment) * Customer information linked to the order * Order metadata ## Using Orders Data Orders are useful for: * **Revenue tracking** - Monitor daily and cumulative revenue * **Customer support** - Look up specific transactions for customer inquiries * **Refunds** - Access order details to process refunds via the customer record * **Analytics** - Understand your sales patterns and popular products Orders only include completed payments. To see in-progress or abandoned checkouts, visit the [Checkouts](/dashboard/checkouts) page. # Payouts Source: https://docs.moneydevkit.com/dashboard/payouts How to get your money out of moneydevkit: manual, automatic, and programmatic payouts. moneydevkit is self-custodial — your app runs its own Lightning node, and moneydevkit never holds your money. A payout moves funds off that node: either to a wallet you control, or (with programmatic payouts) to a destination your own server code chooses to pay. There are three ways to pay out. Choose one under **How do you want to receive payouts?** on the [Payouts page](https://moneydevkit.com/dashboard/payouts) of the dashboard: | Method | Who initiates | Where the money goes | | ------------------------------------- | -------------------------------------- | -------------------------------- | | [Manual](#manual-payouts) | You, from the dashboard | Your `WITHDRAWAL_DESTINATION` | | [Auto-payout](#automatic-payouts) | moneydevkit, on every incoming payment | Your `WITHDRAWAL_DESTINATION` | | [Programmatic](#programmatic-payouts) | Your app's server code | A destination your code supplies | Manual withdrawals are always available, whichever method you select. ## Setting your payout destination Manual and automatic payouts send funds to a destination that only your app knows — moneydevkit never sees it. Set `WITHDRAWAL_DESTINATION` in your app's environment variables to a **Lightning Address** (name\@domain), **LNURL**, or **Bolt12 offer**: ``` WITHDRAWAL_DESTINATION=yourname@walletdomain.com ``` ### How do I payout to CashApp? Make sure you have a CashApp account with the ability to send and receive Bitcoin. Find your CashTag by tapping your profile picture in the top right of CashApp. Your cashtag will have a \$ sign in front of it. Set your `WITHDRAWAL_DESTINATION` to your [cashtag@cash.app](mailto:cashtag@cash.app). Do not include the \$. ``` WITHDRAWAL_DESTINATION=cashtag@cash.app ``` ### How do I payout to Phoenix wallet? Tap "Receive" then select "Reusable" for a Bolt12 offer. Tap "Copy" then copy "Lightning Bolt12." Your Bolt12 offer should start with `lno`. Set `WITHDRAWAL_DESTINATION` to your Bolt12 offer. ``` WITHDRAWAL_DESTINATION=lno... ``` ## Manual payouts The default. On the Payouts page, enter an amount in sats (or click **Send max**) and click **Send payout**. Your node pays the amount to your `WITHDRAWAL_DESTINATION`. The dashboard shows how much is *available to withdraw*, which is slightly less than your total balance — a small portion is reserved to cover Lightning routing fees. ## Automatic payouts The recommended option for most apps. With **Auto-payout** enabled, every incoming payment triggers a payout of your full balance to your `WITHDRAWAL_DESTINATION`. A small routing-fee reserve is kept back, so some funds always remain on the node. There's nothing to schedule or configure beyond `WITHDRAWAL_DESTINATION` — it just runs after each payment. Failed payouts are retried automatically, and you can still withdraw manually whenever you like. ## Programmatic payouts An advanced option that lets your app's **server code** send Lightning payments — to destinations your code supplies, not your `WITHDRAWAL_DESTINATION`. This is what powers agentic commerce flows like paying [L402](/l402) invoices with `pay402`, or any business logic where your app pays a supplier, affiliate, or user out of its own balance. The [Merch Store example](/examples/merch-store) uses it to pay a print-on-demand supplier on every sale. Enable it by selecting **Programmatic payouts** on the Payouts page. Your app also needs an app-scoped `MDK_ACCESS_TOKEN` in its environment — programmatic payout requests are authenticated and scoped to a single app. From code, use `programmaticPayout` (or `pay402`, which wraps it) from `@moneydevkit/nextjs/server`: ```ts theme={null} import { programmaticPayout, waitForPayoutResult } from "@moneydevkit/nextjs/server"; const { paymentId } = await programmaticPayout({ destination: "lnbc1...", // BOLT11, BOLT12, LNURL, or Lightning Address amountSats: 21000, // required unless the BOLT11 has a fixed amount idempotencyKey: `order-${orderId}`, }); const result = await waitForPayoutResult({ paymentId }); ``` Several safety mechanisms are built in: * **Idempotency keys are required.** Every payout needs a stable `idempotencyKey`; retrying with the same key returns the original result instead of paying twice. Reusing a key with a different amount is rejected. * **Per-request cap.** A single payout can't exceed a platform-enforced maximum (currently 1,000,000 sats by default). Larger requests fail with `amount_too_large`. * **Rolling daily limit.** Each app has a rolling 24-hour programmatic payout budget (currently 5,000,000 sats by default). Requests over the limit fail with `daily_limit_exceeded`. Manual and automatic payouts don't count against this budget. * **Everything is recorded.** Programmatic payouts appear in your payout history on the Payouts page, just like withdrawals. If programmatic payouts are disabled for the app, requests fail with `programmatic_payouts_disabled` — flip the switch on the Payouts page to resolve it. # Products Source: https://docs.moneydevkit.com/dashboard/products Create and manage products for checkouts in the moneydevkit dashboard Products are pre-configured items you can sell through moneydevkit. Instead of specifying amount and description for each checkout, you can reference a product by ID and the checkout will use the product's price and details. ## What are Products? Products let you define reusable items with fixed or variable pricing. When you create a checkout using a product, the checkout automatically uses the product's name, description, and price configuration. ## Products Dashboard The products page in your [moneydevkit dashboard](https://moneydevkit.com/dashboard/products) displays all your products: Products list in the moneydevkit dashboard Each product shows: * **Product name** and description * **Price** - Fixed amount or "Pay what you want" range * **Billing** - One-time * **Created date** * **Actions** - View details, edit, or delete ## Creating a Product Create product form in moneydevkit dashboard 1. Navigate to Products in the dashboard 2. Click the **+** button in the top right 3. Fill in the product details: * **Name** - Product name shown to customers * **Description** - Brief description of what's included * **Price Type** - Fixed price or Pay what you want * **Currency** - USD (dollars) or SAT (satoshis) * **Price** - Amount (for fixed price products) 4. Click **Create Product** ## Pricing Options | Type | Description | Use Case | | --------------------- | ----------------------- | --------------------------------- | | **Fixed price** | Set specific amount | Standard products, subscriptions | | **Pay what you want** | Customer chooses amount | Donations, tips, pay-what-you-can | ## Product Details Click any product to view its details in a sidebar: Product detail sidebar in moneydevkit dashboard * **Product ID** - Unique identifier (click to copy) * **Description** - Product description * **Billing** - One-time or subscription interval * **Created/Modified** - Timestamps * **Pricing** - Price amount, type, and currency ## Using Products in Code ### Fetch Available Products ```tsx theme={null} import { useProducts } from '@moneydevkit/nextjs' function ProductList() { const { products, isLoading, error } = useProducts() if (isLoading) return

Loading products...

if (error) return

Error: {error.message}

return (
    {products.map(product => (
  • {product.name}
  • ))}
) } ``` ### Create a Product Checkout ```tsx theme={null} import { useCheckout, useProducts } from '@moneydevkit/nextjs' function BuyProduct() { const { createCheckout, isLoading } = useCheckout() const { products } = useProducts() const handleBuy = async (productId: string) => { const result = await createCheckout({ type: 'PRODUCTS', product: productId, successUrl: '/checkout/success', }) if (result.error) { console.error(result.error.message) return } window.location.href = result.data.checkoutUrl } return (
{products.map(product => ( ))}
) } ``` When using product checkouts, you don't need to specify `amount` or `currency` - these are automatically pulled from the product's price configuration. ## Checkout Types | Type | Description | Required Fields | | ------------------ | --------------------------------------------- | ---------------------- | | `type: 'PRODUCTS'` | Sell products from your dashboard | `product` (product ID) | | `type: 'AMOUNT'` | Dynamic amounts for donations, tips, invoices | `amount`, `currency` | Use products when you have fixed catalog items. Use amount checkouts for dynamic pricing scenarios like invoices or custom quotes. ## Pay What You Want (CUSTOM prices) Products can have CUSTOM prices that let customers choose their own amount. When a checkout includes a product with a CUSTOM price, the checkout UI automatically shows an amount input field: ```tsx theme={null} const result = await createCheckout({ type: 'PRODUCTS', product: customPriceProductId, // Product configured with CUSTOM price in dashboard successUrl: '/checkout/success', }) ``` The customer enters their desired amount during checkout. For USD, amounts are in dollars (converted to cents internally). For SAT, amounts are in satoshis. See the [Replit integration guide](/replit) or [Next.js integration guide](/nextjs) for complete setup instructions. # Subscriptions Source: https://docs.moneydevkit.com/dashboard/subscriptions View and manage customer subscriptions in the moneydevkit dashboard The subscriptions page in your moneydevkit dashboard lets you view all recurring subscriptions, track their status, and manage renewals and cancellations. ## What are Subscriptions? Subscriptions are created when a customer completes a checkout for a product with a recurring interval. Unlike one-time purchases, subscriptions: * Bill customers on a recurring schedule (monthly, quarterly, or yearly) * Send automatic renewal reminder emails * Include a grace period for missed payments * Can be canceled by customers or merchants ## Subscriptions Dashboard The subscriptions page in your [moneydevkit dashboard](https://moneydevkit.com/dashboard/subscriptions) displays all subscriptions with key metrics: * **Active subscriptions** - Currently billing subscriptions * **Past due subscriptions** - Subscriptions in the grace period awaiting payment * **Canceled subscriptions** - Subscriptions that have ended ## Subscription Details Each subscription record includes: | Field | Description | | ---------------------- | ----------------------------------------------------------- | | **Subscription ID** | Unique identifier (click to copy) | | **Customer** | Customer name and email | | **Product** | The recurring product being billed | | **Amount** | Subscription price per period | | **Currency** | USD or SAT | | **Status** | Current subscription status | | **Recurring Interval** | Monthly, Quarterly, or Yearly | | **Current Period** | Start and end dates of the current billing period | | **Started At** | When the subscription was created | | **Canceled At** | When cancellation was requested (if applicable) | | **Ends At** | When the subscription will end (if canceling at period end) | ## Subscription Statuses | Status | Description | Customer Access | | ---------- | ------------------------------------- | -------------------------- | | `active` | Subscription is current and paid | Full access | | `past_due` | Payment missed, in 3-day grace period | Full access (grace period) | | `canceled` | Subscription has ended | No access | ### Grace Period Behavior When a subscription enters the `past_due` status: 1. The customer receives daily emails for 3 days with a renewal link 2. The subscription remains active during this period 3. If no payment is received after 3 days, the subscription auto-cancels Use `hasActiveSubscription` in your code to check if a customer should have access. This returns `true` for both `active` and implicitly covers subscriptions still in their billing period. ## Managing Subscriptions ### Viewing Subscriptions 1. Navigate to Subscriptions in the dashboard 2. Use filters to view by status (Active, Past Due, Canceled) 3. Click any subscription to view details ### Canceling Subscriptions Subscriptions can be canceled in two ways: **Cancel at period end** (default): * Subscription remains active until the current period ends * Customer retains access until the billing period expires * No refund needed since they've already paid for the current period **Cancel immediately**: * Subscription ends immediately * Customer loses access right away * Consider issuing a prorated refund Immediate cancellation removes customer access instantly. Use cancel at period end for a better customer experience. ## Subscription Lifecycle ``` Created → Active → [Renewal payment] → Active (new period) ↘ Past Due → [Payment within 3 days] → Active ↘ Canceled (auto or manual) ``` See the [Subscriptions integration guide](/subscriptions) for implementation details. # Merch Store Source: https://docs.moneydevkit.com/examples/merch-store Build a merch store that sells an AI-designed t-shirt over Lightning and fulfills orders by paying a print-on-demand supplier over L402. Screenshot of the merch store example app. See the live example: [mdk-merchstore-example.vercel.app](https://mdk-merchstore-example.vercel.app) This is an example for building a merch store with moneydevkit. We'll sell a single physical product — an AI-designed t-shirt — for a fixed price over the Lightning Network. This example is different from the others: it involves a real physical product and a real outbound payment on every sale. When a customer pays, the store programmatically pays a print-on-demand supplier over L402 to produce and ship the shirt, and keeps the spread between the retail price and the production cost. ## Step 1: Install the MCP server The MCP server lets your coding agent connect to your existing moneydevkit account and wire up your app for you. Pick your coding agent below: Install MCP in Cursor Install MCP in VS Code Run this in your project directory: ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/account/ ``` Run this in your project directory: ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/account/ ``` ## Step 2: Start a new project Create an empty folder and open it in your coding agent. This will be the root of your new Next.js project. ## Step 3: Prompt your coding agent Paste the following prompt into your coding agent to scaffold the merch store. Note that the build has a couple of paid steps (generating the artwork and creating the product cost roughly $0.25–$0.50 total), so your agent will pause and ask you to fund its agent wallet with a few dollars of bitcoin before it proceeds. The goal is to build a Next.js (App Router) merch store that sells a single physical product — an AI-designed t-shirt — for a fixed price over the Lightning Network using moneydevkit, and fulfills each order automatically by paying a print-on-demand service over L402. This exercises moneydevkit [checkout](https://docs.moneydevkit.com/nextjs.md), [programmatic payouts + L402/pay402](https://docs.moneydevkit.com/l402.md), the [agent wallet](https://docs.moneydevkit.com/agent-wallet.md), and [webhooks](https://docs.moneydevkit.com/webhooks.md). Read the docs index first at [https://docs.moneydevkit.com/llms.txt](https://docs.moneydevkit.com/llms.txt) and the linked pages before you start. Business model: the customer pays a retail price (e.g. $35) to the store via moneydevkit checkout. When that payment settles, the store programmatically pays the print supplier the production cost + shipping (well under $35) over L402, and keeps the spread. Do not hardcode the supplier price — discover it from the supplier's catalog at build time. Keep it simple: there is no need to create a matching product in the moneydevkit dashboard. Use an AMOUNT-type checkout for the fixed retail price (details in phase 4). The only product that has to exist is the one at the print supplier; the storefront just charges a flat price and there is no 1:1 mapping to maintain between an unhuman.ink product and a moneydevkit product. Build it in these phases: 1. Agent wallet. Install and initialize the moneydevkit agent wallet (`npx @moneydevkit/agent-wallet@latest`). You will use it to pay the L402 invoices for design generation and product creation. Check the balance first; if it is too low, STOP and tell the user to fund the wallet with a small amount of bitcoin (CashApp, Strike, or any Lightning wallet) — creating the product costs roughly $0.25–$0.50 total, so ask for a few dollars of headroom. Do not proceed to any paid step with an empty wallet. 2. Get the artwork. By default, generate it yourself the first time: fetch [https://unhuman.design/llms.txt](https://unhuman.design/llms.txt) and follow its documented API exactly to generate a print-ready artwork image, paying any L402 challenge with the agent wallet. But if the user has provided their own artwork (a file they point you at, a path, or a URL), use that instead of generating one — only generate when no artwork is supplied. Either way, save the chosen image locally (the store's `assets/`) and use it on the marketing page. If unhuman.design's flow differs from what you expect, trust its llms.txt over assumptions. 3. Create the product with unhuman.ink. Fetch [https://unhuman.ink/llms.txt](https://unhuman.ink/llms.txt) and follow it exactly. The flow is: GET the catalog and pick a 100% cotton t-shirt template (note: some colors are cotton/poly heather blends — pick a solid color that is 100% cotton) → POST `/api/artworks/challenge` (L402, pay with agent wallet) to get an `upload_token` → POST `/api/artworks` (Bearer upload\_token, multipart) to stage the artwork → POST `/api/products` (L402) to create the product. CRITICAL PRICING: unhuman.ink charges the store the product's `retail_price` on every order, so set `retail_price` equal to the catalog production cost (no markup on their side) — that is what lets the store keep the spread against the \$35 checkout price. After creation, you MUST save the returned `product_uuid`, `bearer_token`, and `product_url` to `~/.unhuman/ink/products.json` (append to the array; the bearer token is shown only once). Then GET `/api/products/{product_uuid}` and record the variant's `sync_variant_id` — this is DIFFERENT from the catalog `catalog_variant_id` and is the id you must use when placing orders. 4. Next.js + moneydevkit checkout. Follow [https://docs.moneydevkit.com/nextjs.md](https://docs.moneydevkit.com/nextjs.md) to install `@moneydevkit/nextjs` and add the `/api/mdk` operational route. Build the storefront as a single product page with a size picker and a shipping-address form. Start the checkout with `createCheckout` as an AMOUNT-type checkout for the fixed retail price (title/description set in code) — do NOT use a PRODUCTS checkout and do NOT require a moneydevkit dashboard product. Confirm the exact amount/currency options for AMOUNT checkouts in nextjs.md (e.g. whether the amount is fiat like USD or denominated in sats) and use whatever it supports for a fixed \$35-equivalent price. moneydevkit checkout metadata is validated as a flat string map (`z.record(z.string())`), so JSON-stringify any nested object (like the shipping address) before putting it in metadata, and parse it back on the server. Set `productKind: "shirt"` (or similar) in metadata so the fulfillment handler can tell real orders apart. 5. Programmatic fulfillment. Add a `checkout.completed` notification webhook route (e.g. `/api/webhooks/checkout-completed`) that: verifies the Standard-Webhooks signature with the webhook secret; reads the shipping address and product kind from the checkout metadata; and calls `pay402("https://unhuman.ink/api/orders", ...)` with the product UUID, the `sync_variant_id`, quantity, recipient, a `maxAmountSats` cap, and an idempotency key derived from the checkout id. pay402 handles the L402 loop (initial POST → 402 → programmatic payout of the invoice → replay with the token). Set `maxAmountSats` above the real order cost (production cost + shipping — query unhuman.ink's free `/api/shipping` endpoint to size it) with headroom for exchange-rate movement, but comfortably below the retail price so every order stays profitable. On any failure, return a 5xx so moneydevkit retries the delivery; the idempotency key prevents double-paying on retries. 6. Marketing / agent-readiness. Give the store a polished storefront and an `llms.txt` describing the product so an agent could buy it. Follow Cloudflare's agent-readiness guidance ([https://isitagentready.com/](https://isitagentready.com/)). Things to get right (these are the failure modes that break this in production): * The `UNHUMAN_VARIANT_ID` you order with must be the product's `sync_variant_id` from GET `/api/products/{uuid}`, NOT the catalog `catalog_variant_id`. Ordering with the catalog id is rejected ("variant does not belong to this product"). * The `UNHUMAN_PRODUCT_UUID` and `UNHUMAN_VARIANT_ID` values you generate at build time live only in your local `.env.local`. They are NOT on the deploy target until the user adds them. The fulfillment handler must fail loudly (clear error) if either is missing, rather than silently doing nothing. * Use environment variables for the product UUID, sync variant id, moneydevkit access token/mnemonic, and the webhook secret. Never commit secrets. When you finish, tell the user exactly what they must do on their end: enable programmatic payouts on their moneydevkit app (the switch is on the dashboard's Payouts page — [https://docs.moneydevkit.com/dashboard/payouts.md](https://docs.moneydevkit.com/dashboard/payouts.md)); configure a `checkout.completed` notification webhook pointing at `/api/webhooks/checkout-completed` and copy the signing secret into `MDK_WEBHOOK_SECRET`; add all the environment variables to their deployment and redeploy; and keep the `checkout.completed` webhook enabled (moneydevkit auto-disables an endpoint after repeated failed deliveries). Then give them a local preview link. ## Step 4: Preview and deploy When your coding agent finishes, you can preview your app. The agent will give you a link that you can open in your browser to preview the app locally. When you're ready to ship it, head to [Vercel](https://vercel.com/new) to deploy. Because this store pays a supplier on every sale, a few things have to be wired up by you (your agent can't do these on your behalf): 1. **Create a moneydevkit app** (or connect your existing one via the MCP) and copy `MDK_ACCESS_TOKEN` and `MDK_MNEMONIC`. 2. **[Enable programmatic payouts](/dashboard/payouts#programmatic-payouts)** — the switch is on the Payouts page of the dashboard. The store pays the supplier out of its balance on every sale; each app has a rolling daily programmatic payout limit, so make sure it comfortably covers your expected order volume. There's no dashboard product to create; the store uses an AMOUNT-type checkout for the fixed price. 3. **Configure a `checkout.completed` notification webhook** pointing at `https://YOUR_HOST/api/webhooks/checkout-completed`, and copy its signing secret into `MDK_WEBHOOK_SECRET`. Point it at your production alias, not a one-off immutable deployment URL. 4. **Add the environment variables to your deployment**: `UNHUMAN_PRODUCT_UUID`, `UNHUMAN_VARIANT_ID` (the sync variant id), `MDK_ACCESS_TOKEN`, `MDK_MNEMONIC`, and `MDK_WEBHOOK_SECRET` — then **redeploy**. Environment changes don't apply to a running deployment until you redeploy. 5. **Keep the `checkout.completed` webhook enabled.** moneydevkit auto-disables an endpoint after a streak of failed deliveries, so if early orders fail while you finish wiring things up, the webhook can silently switch off. Re-enable it and confirm one delivery returns 200 so it stays on. # Paid API Source: https://docs.moneydevkit.com/examples/paid-api Build a moneydevkit app that sells agent access to a paid orbital tracking API. Screenshot of the paid API example app. See the live example: [mdk-api-example.vercel.app](https://mdk-api-example.vercel.app) This is an example for building a simple paid API with moneydevkit. We'll build an orbital tracking API that uses L402 payments so AI agents can pay for access to current demo data about satellites, space stations, rockets in transit, orbital debris, and other Earth-orbit traffic. ## Step 1: Install the MCP server The MCP server lets your coding agent connect to your existing moneydevkit account and wire up your app for you. Pick your coding agent below: Install MCP in Cursor Install MCP in VS Code Run this in your project directory: ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/account/ ``` Run this in your project directory: ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/account/ ``` ## Step 2: Start a new project Create an empty folder and open it in your coding agent. This will be the root of your new Next.js project. ## Step 3: Prompt your coding agent Paste the following prompt into your coding agent to scaffold the paid orbital tracking API: The goal is to build a Next.js website that demonstrates the functionality of moneydevkit's [l402](https://docs.moneydevkit.com/l402.md) and [agent wallet](https://docs.moneydevkit.com/agent-wallet) features. Style it like a slick-looking developer API platform. The platform gives you precise, current information on Earth orbital traffic: satellites, space stations, rockets in-transit, known orbital debris, and more. (The data is fake and for demo purposes only) For the human UI, design a cool marketing psge that advertises the product and provides enough information so that they can give their AI agent access to the API (e.g. tell your agent to read these docs to use the API). Establish an llms.txt file that. explains how to use the API for the agent. Includes instructions on installing the moneydevkit agent wallet so the agent can pay for the API access. If the agent needs money in their wallet, the agent should suggest to the user that they fund the wallet using CashApp, Strike, or another bitcoin-lightning wallet. Follow the instructions [here](https://docs.moneydevkit.com/nextjs.md) for setting up moneydevkit with Next.js. Make sure that the demo site follows best practices for agent-readiness according to Cloudflare's guidance ([https://isitagentready.com/](https://isitagentready.com/)). ## Step 4: Preview and deploy When your coding agent finishes, you can preview your app. The agent will give you a link that you can open in your browser to preview the app locally. When you're ready to ship it, head to [Vercel](https://vercel.com/new) to deploy. # Subscriptions Source: https://docs.moneydevkit.com/examples/subscriptions Build a moneydevkit app where supporters can subscribe to fund an independent journalist. Screenshot of the subscriptions example app. See the live example: [mdk-supporters.vercel.app](https://mdk-supporters.vercel.app) This is an example for building a recurring-payments app with moneydevkit. We'll build a supporters page for an independent journalist, but the same approach applies to membership sites, paid newsletters, creator memberships, and any other use case where you want to charge a recurring fee. ## Step 1: Install the MCP server The MCP server lets your coding agent create a moneydevkit account and wire up your app for you. Pick your account type and coding agent below: Install MCP in Cursor Install MCP in VS Code Run this in your project directory: ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/ ``` Run this in your project directory: ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/ ``` Install MCP in Cursor Install MCP in VS Code Run this in your project directory: ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/account/ ``` Run this in your project directory: ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/account/ ``` ## Step 2: Start a new project Create an empty folder and open it in your coding agent. This will be the root of your new Next.js project. ## Step 3: Prompt your coding agent Paste the following prompt into your coding agent to scaffold the supporters page: The goal is to build a Next.js website that demonstrates the functionality of moneydevkit's [subscriptions](https://docs.moneydevkit.com/subscriptions.md), [customers](https://docs.moneydevkit.com/dashboard/customers.md), and [checkout](https://docs.moneydevkit.com/dashboard/checkouts.md) features. If available, use \$mdk-nextjs skills to accomplish this. Style it like the website for an independent journalist who does reporting in dangerous parts of the world. This is a place where people can support his work by leaving a donation or subscribing to support the journalist monthly. If available, use \$impeccable skills to craft a great design. For the human UI, offer several subscription options: $5 / mo, $25 / mo, and \$100 / mo. Use the moneydevkit MCP to create the subscription products if moneydevkit MCP tools are available. Otherwise, set it up so I can add the product IDs later in env vars. Also enable [l402](https://docs.moneydevkit.com/l402.md) functionality so AI agents can leave leave donations (or subscribe) on behalf of their users. Use \$mdk-l402. Follow the instructions [here](https://docs.moneydevkit.com/nextjs.md) for setting up moneydevkit with Next.js. When you are finished working, run this in the dev server locally and give the user a link to where they can open it in the browser. ## Step 4: Preview and deploy When your coding agent finishes, you can preview your app. The agent will give you a link that you can open in your browser to preview the app locally. When you're ready to ship it, head to [Vercel](https://vercel.com/new) to deploy. # Tip Jar Source: https://docs.moneydevkit.com/examples/tip-jar Build the simplest possible moneydevkit app: a tip jar for a content creator. Screenshot of the tip jar example app. See the live example: [mdk-tipjar-example.vercel.app](https://mdk-tipjar-example.vercel.app) This is an example for building the absolute simplest of apps with moneydevkit: an app that can receive money. We'll build a tip jar for a content creator, but the same approach applies to donation pages, pay-what-you-want downloads, support-the-author buttons, and many other use cases. ## Step 1: Install the MCP server The MCP server lets your coding agent create a moneydevkit account and wire up your app for you. Pick your account type and coding agent below: Install MCP in Cursor Install MCP in VS Code Run this in your project directory: ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/ ``` Run this in your project directory: ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/ ``` Install MCP in Cursor Install MCP in VS Code Run this in your project directory: ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/account/ ``` Run this in your project directory: ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/account/ ``` ## Step 2: Start a new project Create an empty folder and open it in your coding agent. This will be the root of your new Next.js project. ## Step 3: Prompt your coding agent Paste the following prompt into your coding agent to scaffold the tip jar: The goal is to build a Next.js website that demonstrates the functionality of moneydevkit's [customers](https://docs.moneydevkit.com/dashboard/customers.md), and [checkout](https://docs.moneydevkit.com/dashboard/checkouts.md) features. Style it like a simple platform for a content creator (Cindy - AI Art Sorceress) to accept tips from followers. For the human UI, offer several pre-built amount options to chose from: $5, $10, \$25, and Custom. Also enable [l402](https://docs.moneydevkit.com/l402.md) functionality so AI agents can leave tips on behalf of their users. Follow the instructions [here](https://docs.moneydevkit.com/nextjs.md) for setting up moneydevkit with Next.js. Use the moneydevkit MCP to create an account (if not already done) and setup an app with mnemonic. When you are finished working, run this in the dev server locally and give the user a link to where they can open it in the browser. ## Step 4: Preview and deploy When your coding agent finishes, you can preview your app. The agent will give you a link that you can open in your browser to preview the app locally. When you're ready to ship it, head to [Vercel](https://vercel.com/new) to deploy. # Coming from Stripe? Source: https://docs.moneydevkit.com/from-stripe How moneydevkit differs from Stripe and how to migrate your payment flow If you're familiar with Stripe, you'll find moneydevkit's approach refreshingly simple. The biggest difference: **no webhook handlers to write**. You set up the `/api/mdk` route once during initial setup, and that's it—no webhook event handling code required. ## Key Differences | Stripe | moneydevkit | | ----------------------------------------- | --------------------------------------- | | Create PaymentIntent, wait for webhook | Create checkout, check status directly | | Write webhook handlers for payment events | One-time route setup, no event handlers | | Handle webhook verification & retries | Payment status available immediately | | Complex idempotency handling | Simple client-side state | ## No Webhook Handlers to Write With Stripe, your backend typically relies on webhooks to know when a payment succeeds: ```tsx theme={null} // Stripe: You need a webhook endpoint app.post('/webhooks/stripe', async (req, res) => { const event = stripe.webhooks.constructEvent(...) if (event.type === 'payment_intent.succeeded') { // Now you know payment succeeded await fulfillOrder(event.data.object.metadata.orderId) } }) ``` With moneydevkit, payment status is available directly on your success page: ```tsx theme={null} // moneydevkit: No webhook needed 'use client' import { useCheckoutSuccess } from '@moneydevkit/nextjs' export default function SuccessPage() { const { isCheckoutPaid, metadata } = useCheckoutSuccess() if (isCheckoutPaid) { // Payment confirmed - fulfill the order return

Payment confirmed!

} return

Verifying payment…

} ``` ## What About the `/api/mdk` Route? The initial setup includes an `/api/mdk` route: ```js theme={null} // app/api/mdk/route.js export { POST } from '@moneydevkit/nextjs/server/route' ``` This is a one-time setup—you export the handler and you're done. Unlike Stripe webhooks, you don't write any event handling code. The route is internal infrastructure that allows your serverless Lightning node to receive payments. You never need to parse events, verify signatures, or handle retries. The `/api/mdk` route may handle additional functionality in the future, but payment confirmation will always be available directly via `useCheckoutSuccess`. ## Linking Checkouts to Your Users moneydevkit has first-class support for linking checkouts to your authenticated users via `customer.externalId`. ### Using `externalId` for Authenticated Users When a user is logged into your app, pass their ID via `customer.externalId`: ```tsx theme={null} 'use client' import { useCheckout } from '@moneydevkit/nextjs' export default function PurchasePage({ user }) { const { createCheckout, isLoading } = useCheckout() const handlePurchase = async () => { const result = await createCheckout({ type: 'AMOUNT', amount: 500, currency: 'USD', title: 'Premium Plan', successUrl: '/checkout/success', customer: { externalId: user.id, // Your app's user ID name: user.name, email: user.email, } }) if (!result.error) { window.location.href = result.data.checkoutUrl } } return } ``` When `externalId` is provided: * The checkout is linked to a customer record in moneydevkit * If the customer already exists (matched by `externalId`), their stored data is used * Returning customers won't be asked for information you already have * All checkouts and orders are linked to the same customer record ### Using `metadata` for Custom Data For other custom data (like a registration ID, order reference, or plan type), use the `metadata` field: ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', amount: 500, currency: 'USD', title: 'Event Registration', successUrl: '/checkout/success', customer: { externalId: user.id, }, metadata: { registrationId: 'reg_xyz789', plan: 'premium' } }) ``` Retrieve it on your success page: ```tsx theme={null} 'use client' import { useCheckoutSuccess } from '@moneydevkit/nextjs' export default function SuccessPage() { const { isCheckoutPaid, metadata } = useCheckoutSuccess() if (isCheckoutPaid) { const registrationId = metadata?.registrationId // Use registrationId to update your database, send confirmation, etc. return

Registration {registrationId} confirmed!

} return

Verifying payment…

} ``` See the [Customers documentation](/dashboard/customers) for more details. ## Quick Comparison: Checkout Flow ```tsx theme={null} // Server-side const paymentIntent = await stripe.paymentIntents.create({ amount: 500, currency: 'usd', metadata: { orderId: '123' } }) // Return clientSecret to frontend ``` ```tsx theme={null} // Client-side const result = await createCheckout({ type: 'AMOUNT', amount: 500, currency: 'USD', metadata: { orderId: '123' }, successUrl: '/success' }) window.location.href = result.data.checkoutUrl ``` ```tsx theme={null} // Server-side webhook handler you must write and maintain if (event.type === 'payment_intent.succeeded') { await fulfillOrder(event.data.object.metadata.orderId) } ``` ```tsx theme={null} // No webhook handler needed - check status directly const { isCheckoutPaid, metadata } = useCheckoutSuccess() if (isCheckoutPaid) { // metadata.orderId available here } ``` ## Summary * **No webhook handlers to write** - One-time `/api/mdk` route setup, then you're done * **Payment status available directly** - Use `useCheckoutSuccess` instead of waiting for webhook events * **`externalId` is first-class** - Link checkouts to your authenticated users, with automatic data backfill for returning customers * **Use `metadata`** for custom checkout data (registration IDs, order references, etc.) # How Does moneydevkit Work? Source: https://docs.moneydevkit.com/howitworks FAQs and more information for people who want to know what's going on behind the scenes. # What is moneydevkit? moneydevkit is an easy to use developer tool that provides fast global payments for any app. Under the hood, moneydevkit uses self-custodial Bitcoin lightning payments. Self-custodial means we never hold or have access to your funds, you control your keys. ## What is Bitcoin? Bitcoin is a trustless, peer to peer, global electronic cash system. Read more about Bitcoin [here](https://bitcoincore.org/bitcoin.pdf). ## What is lightning? Lightning is a trustless, peer to peer, "Layer 2" built on top of Bitcoin. It keeps the trustless and self-custody properties of Bitcoin while making transactions nearly instant, private, and hugely scalable. Read more about Lightning [here](https://lightning.network). ## Why Bitcoin and Lightning? Bitcoin is the most global, accessible digital cash we have, it is truly native internet money. Lightning makes Bitcoin move quickly, the way you expect payments to run.\ \ For people who use moneydevkit, accepting lightning payments means that anyone with a lightning enabled wallet or app will be able to pay you. For example, any of CashApp's 58M active users have the ability to pay a moneydevkit checkout seamlessly. Most importantly, since moneydevkit is self-custodial, anyone can use moneydevkit. # What's going on behind the scenes? When you add moneydevkit to your project, you're adding a library that's harnessing Spiral's [Lightning Dev Kit](https://lightningdevkit.org). Lightning dev kit is a very flexible and modular way to implement lightning payments. Our NextJS package is serverless, and we think one of the first truly serverless implementations of lightning. ## When your first customer begins the checkout flow moneydevkit spins up a serverless lightning node and creates an invoice, that is added to our pre-built checkout page. ## When your customer sends a lightning payment 1. Our infrastructure reaches out to your website via webhook letting your serverless node know a payment is incoming. 2. Your serverless node spins up and accepts the payment 3. Checkout is completed and your serverless node spins down # How is moneydevkit self-custodial? Your keys and wallet are tied to `MDK_MNEMONIC`. This is why it's very important to keep your mnemonic backed up and safe. moneydevkit employees will never ask you for your mnemonic. Never give anyone your mnemonic. Your serverless lightning node is entirely hosted on your website's infrastructure. At no point do we have access to your keys or money, and we don't have the ability to stop or redirect your payments. ## Do I have to worry about nodes, lightning channels, or liquidity? No. Behind the scenes, we open lightning channels to your serverless node and make sure you have enough liquidity to receive payments. ## How can I recover if something goes wrong? Your mnemonic will allow you to recover locally if something happens. We will release an easy to use advanced recovery tool that you can use locally soon. # moneydevkit Source: https://docs.moneydevkit.com/index Global payments for any app in less than 5 minutes # Build with moneydevkit moneydevkit makes it trivial to add global payments to any app. We include everything to get you taking global payments in less than five minutes. Using Bitcoin under the hood means moneydevkit can be used by nearly anyone with little to no friction. It's the easiest, fastest, and most accessible way to take payments online. moneydevkit is in public beta. Please join us in the [discord](https://discord.gg/Ebb26MrJpT), ask questions, share your experience, and give us feedback ## Start building Install the SDK, run the CLI to mint credentials, and wire up the hosted checkout loop end-to-end. Have Replit's agent set up moneydevkit for you. Gate any API route behind a Lightning payment. No accounts, no subscriptions. Self-custodial Lightning wallet for AI agents. CLI with JSON output, no API keys required. Install Money Dev Kit skills that teach coding agents how to build checkout, MCP, L402, and wallet workflows. # L402: Pay-per-call APIs Source: https://docs.moneydevkit.com/l402 Gate any API route behind a Lightning payment using the L402 protocol (bLIP-26) 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](https://github.com/lightning/blips/pull/26)-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 ```mermaid theme={null} sequenceDiagram participant C as Client participant S as Your Server participant L as Lightning C->>S: GET /api/premium S-->>C: 402 { invoice, token } C->>L: pay invoice L-->>C: preimage C->>S: GET /api/premium
Authorization: L402 token:preimage S->>S: verify token + preimage S-->>C: 200 OK { data } ``` ## 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. Click to install MCP in Cursor Click to install MCP in VS Code **Claude Code:** ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/ ``` After signup, it's highly recommended to log in at [moneydevkit.com](https://moneydevkit.com) and switch to the [authenticated MCP server](/nextjs#existing-account) (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](https://moneydevkit.com) or run `npx @moneydevkit/create` to generate credentials locally, then grab your `api_key` and mnemonic. 2. **Install the SDK**: ```bash theme={null} npm install @moneydevkit/nextjs ``` 3. **Add environment variables** to `.env`: ```env theme={null} MDK_ACCESS_TOKEN=your_api_key_here MDK_MNEMONIC=your_mnemonic_here ``` 4. **Expose the moneydevkit endpoint**: ```js theme={null} // app/api/mdk/route.js export { POST } from '@moneydevkit/nextjs/server/route' ``` 5. **Configure Next.js**: ```js theme={null} // 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. [![Install moneydevkit](https://replit.com/badge?caption=Install%20moneydevkit)](https://replit.com/integrations?mcp=eyJkaXNwbGF5TmFtZSI6Im1vbmV5ZGV2a2l0IiwiYmFzZVVybCI6Imh0dHBzOi8vbWNwLm1vbmV5ZGV2a2l0LmNvbS9tY3AifQ==) After signup, it's highly recommended to log in at [moneydevkit.com](https://moneydevkit.com) and switch to the [authenticated MCP server](/nextjs#existing-account) (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](https://moneydevkit.com) or run `npx @moneydevkit/create` to generate credentials locally, then grab your `api_key` and mnemonic. 2. **Install the SDK** (Express is a peer dependency): ```bash theme={null} npm install @moneydevkit/replit express ``` 3. **Add environment variables** to `.env` (or Replit Secrets): ```env theme={null} MDK_ACCESS_TOKEN=your_api_key_here MDK_MNEMONIC=your_mnemonic_here ``` 4. **Mount the moneydevkit endpoint** in your Express server: ```ts theme={null} // 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: ```ts theme={null} // 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, ) ``` ```ts theme={null} import express, { type Request as ExpressRequest, type Response as ExpressResponse } from 'express' import { withPayment } from '@moneydevkit/replit/server/express' const app = express() function expressToFetchRequest(req: ExpressRequest): Request { const headers = new Headers() for (const [key, value] of Object.entries(req.headers)) { if (key.toLowerCase() === 'content-length') { continue } if (Array.isArray(value)) { value.forEach((item) => headers.append(key, item)) } else if (value !== undefined) { headers.set(key, value) } } return new Request(`${req.protocol}://${req.get('host')}${req.originalUrl}`, { method: req.method, headers, }) } async function sendFetchResponse(res: ExpressResponse, response: Response) { response.headers.forEach((value, key) => res.setHeader(key, value)) res.status(response.status).send(Buffer.from(await response.arrayBuffer())) } const handler = async (req: Request) => { return Response.json({ content: 'Premium data' }) } const paidHandler = withPayment( { amount: 100, currency: 'SAT' }, handler, ) app.get('/api/premium', async (req, res) => { const response = await paidHandler(expressToFetchRequest(req)) await sendFetchResponse(res, response) }) ``` 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 `withPayment` accepts the same shape as the dashboard SDK's `createCheckout` from `@moneydevkit/core` — a single discriminated union that picks AMOUNT or PRODUCTS mode based on the `type` field. The L402 wrapper adds two things: every field accepts a `(req: Request) => value` resolver for per-request dynamic pricing/metadata, and an `expirySeconds` field controls the credential + invoice lifetime. | Field | Type | Required | Description | | --------------------- | ----------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `'AMOUNT' \| 'PRODUCTS'` | conditional | Discriminator. Defaults to `'AMOUNT'` when omitted. PRODUCTS endpoints must set `'PRODUCTS'` explicitly. | | `amount` | `number \| (req) => number` | AMOUNT | Fixed price or a function that derives it from the request. | | `currency` | `'SAT' \| 'USD'` | AMOUNT | Currency for the AMOUNT price. | | `product` | `string \| (req) => string` | PRODUCTS | Product ID. Price and currency come from the product's active price. | | `title` | `string \| (req) => string` | AMOUNT only | Short label for dashboard/order/webhook surfaces. Rejected on PRODUCTS endpoints — the product's own name drives those surfaces. | | `description` | `string \| (req) => string` | AMOUNT only | Free-form description. Flows through to the BOLT11 invoice's d-tag (server-side truncated to 200 bytes). Ignored in preview/sandbox mode. Rejected on PRODUCTS endpoints — the product's own description is used. | | `metadata` | `Record \| (req) => Record` | optional | Arbitrary string-valued metadata. Merged with system keys (`source: '402'`, `resource: `, `sandbox: 'true'` when in preview); system keys win. | | `customer` | `CustomerInput` | optional | Customer info attached to the checkout (merchant collects out-of-band). | | `requireCustomerData` | `string[]` | optional | Customer fields the merchant requires on the checkout (e.g., `['email']`). | | `expirySeconds` | `number` | optional | Credential + invoice lifetime in seconds. Default: `900` (15 minutes). | ```ts theme={null} // AMOUNT mode (default — `type` omitted) withPayment({ amount: 100, currency: 'SAT' }, handler) // AMOUNT mode with metadata withPayment( { amount: 100, currency: 'SAT', title: 'Premium pairing', description: 'AI-generated wine pairing for the day', metadata: { tier: 'pro' }, }, handler, ) // PRODUCTS mode (price + label come from the product on the dashboard) withPayment( { type: 'PRODUCTS', product: 'prod_abc123', }, handler, ) ``` On every authenticated retry, PRODUCTS-mode endpoints re-resolve the product and look for a price whose `(amount, currency)` matches the credential's frozen values. If no matching price is found (the price was retired or replaced), the request is rejected with `amount_mismatch` (403, recoverable) — the agent should request a fresh 402 challenge. If the product fetch fails (e.g., the product was deleted), the request fails with `pricing_error` (500, `phase: 'verify'`). ## Dynamic pricing Pass a function instead of a fixed number to compute the price from the request: ```ts theme={null} 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: ```ts theme={null} 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`: ```ts theme={null} export const GET = withPayment( { amount: 100, currency: 'SAT', expirySeconds: 300 }, // 5 minutes handler, ) ``` ## Sandbox responses When a merchant's app is running in sandbox mode (e.g. Replit preview), the 402 response signals sandbox state on three independent channels so any client implementation can detect it: 1. **JSON body field** — `sandbox: true` appears alongside the standard fields. 2. **WWW-Authenticate parameter** — the L402 challenge includes `sandbox="true"`. 3. **BOLT11 description tag** — the invoice's description is set to the literal string `you_are_in_sandbox_mode_do_not_attempt_payment_just_pass_a_fake_preimage` (the whole description, not a prefix). Example sandbox 402 response: ```http theme={null} HTTP/1.1 402 Payment Required content-type: application/json www-authenticate: L402 macaroon="eyJ...", invoice="lnbc...", sandbox="true" { "error": { "code": "payment_required", "message": "Payment required" }, "macaroon": "eyJ...", "invoice": "lnbc...", "paymentHash": "abc123...", "amountSats": 100, "expiresAt": 1234567890, "sandbox": true } ``` The invoice is not payable — and the BOLT11 description literally instructs decoders not to try. AI agents should detect any of the three signals and skip the payment attempt entirely. In sandbox mode, preimage verification on subsequent requests is skipped, so the client can submit any 64-character hex preimage (e.g., `"0".repeat(64)`) and retry the same endpoint to proceed past the 402. ## Client integration Any HTTP client can consume an L402 endpoint. ### curl ```bash theme={null} # 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) ```ts theme={null} async function callPaidEndpoint( url: string, payFn: (invoice: string) => Promise, ) { // 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}` }, }) } ``` ## Deferred settlement By default, `withPayment` marks the credential as used immediately before your handler runs. If your handler fails after the credential is consumed, the payer can't retry. Use `withDeferredSettlement` when the service delivery might fail and you want the payer to be able to retry. Your handler receives a `settle()` callback - call it only after you've successfully delivered the service: ```ts theme={null} // app/api/ai/route.ts import { withDeferredSettlement, type SettleResult } from '@moneydevkit/nextjs/server' const handler = async (req: Request, settle: () => Promise) => { const { prompt } = await req.json() // Do the expensive work first const result = await runAiInference(prompt) // Work succeeded - now mark the credential as used const { settled } = await settle() if (!settled) { return Response.json({ error: 'settlement_failed' }, { status: 500 }) } return Response.json({ result }) } export const POST = withDeferredSettlement( { amount: 100, currency: 'SAT' }, handler, ) ``` ```ts theme={null} import express, { type Request as ExpressRequest, type Response as ExpressResponse } from 'express' import { withDeferredSettlement, type SettleResult } from '@moneydevkit/replit/server/express' const app = express() app.use(express.json()) function expressToFetchRequest(req: ExpressRequest): Request { const headers = new Headers() for (const [key, value] of Object.entries(req.headers)) { if (key.toLowerCase() === 'content-length') { continue } if (Array.isArray(value)) { value.forEach((item) => headers.append(key, item)) } else if (value !== undefined) { headers.set(key, value) } } const init: RequestInit = { method: req.method, headers, } if (req.method !== 'GET' && req.method !== 'HEAD' && req.body !== undefined) { init.body = typeof req.body === 'string' || Buffer.isBuffer(req.body) ? req.body : JSON.stringify(req.body) } return new Request(`${req.protocol}://${req.get('host')}${req.originalUrl}`, init) } async function sendFetchResponse(res: ExpressResponse, response: Response) { response.headers.forEach((value, key) => res.setHeader(key, value)) res.status(response.status).send(Buffer.from(await response.arrayBuffer())) } const handler = async (req: Request, settle: () => Promise) => { const { prompt } = await req.json() const result = await runAiInference(prompt) const { settled } = await settle() if (!settled) { return Response.json({ error: 'settlement_failed' }, { status: 500 }) } return Response.json({ result }) } const paidHandler = withDeferredSettlement( { amount: 100, currency: 'SAT' }, handler, ) app.post('/api/ai', async (req, res) => { const response = await paidHandler(expressToFetchRequest(req)) await sendFetchResponse(res, response) }) ``` If your handler returns without calling `settle()` (e.g. it throws or the service fails), the credential stays valid and the payer can retry with the same macaroon and preimage. `settle()` is callable only once per request. A second call returns `{ settled: false, error: 'already_settled' }` without hitting the backend. A 402 is only returned when no L402/LSAT authorization header is present. If the header is present but malformed or invalid, you get a 401 - not a new invoice. This prevents wasting invoices on bad auth attempts. ## 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 | | 401 | `credential_consumed` | Credential has already been used | | 403 | `resource_mismatch` | Token was issued for a different endpoint | | 403 | `amount_mismatch` | Credential's frozen amount/currency no longer matches the endpoint's current price (AMOUNT: dynamic callback returned a different value; PRODUCTS: no active price on the product matches the credential). Recoverable: request a fresh 402. | | 500 | `configuration_error` | `MDK_ACCESS_TOKEN` is not set | | 500 | `pricing_error` | Dynamic `amount` or `product` callback threw an error. Carries `phase: 'create' \| 'verify'` to distinguish 402-issuance vs retry-time failure | | 500 | `config_error` | Dynamic `title`, `description`, or `metadata` callback threw an error | | 500 | `config_invalid` | Static config field failed validation (e.g., non-finite `amount`, empty `product`). Distinct from `pricing_error` which is reserved for runtime callback failures | | 502 | `checkout_creation_failed` | Failed to create the checkout or invoice | | 502 | `invoice_mint_failed` | `mintInvoice` returned a checkout without an attached invoice | ### Error envelope extensions Error responses optionally include two fields beyond `{ code, message, details }`: * **`recoverable: boolean`** — `true` means the client should discard the credential and request a fresh 402 (price changed, product/price retired). * **`phase: 'create' | 'verify'`** — present on `pricing_error` to indicate whether the failure occurred at 402 issuance (`create`) or during an authenticated retry (`verify`). Useful for merchants grepping logs by failure point. # Next.js Source: https://docs.moneydevkit.com/nextjs Embed the moneydevkit checkout loop inside your Next.js App Router project `@moneydevkit/nextjs` is the moneydevkit checkout SDK for App Router-based Next.js apps. It bundles the client hook, hosted checkout UI, API route handler, and config helpers required to launch Lightning-powered payments within minutes. ## Install with AI Coding Tools Use an AI coding assistant to set up moneydevkit in your project. Choose the option that matches your situation: Creates a moneydevkit account and implements the SDK for you. Click to install MCP in Cursor Click to install MCP in VS Code **Claude Code:** ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/ ``` **ChatGPT Codex:** ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/ ``` Already have a moneydevkit account? Use these to connect your existing credentials. Click to install MCP in Cursor Click to install MCP in VS Code **Claude Code:** ```bash theme={null} claude mcp add moneydevkit --transport http https://mcp.moneydevkit.com/mcp/account/ ``` **ChatGPT Codex:** ```bash theme={null} codex mcp add moneydevkit --url https://mcp.moneydevkit.com/mcp/account/ ``` ## Setup 1. **Create a Money Dev Kit account** at [moneydevkit.com](https://moneydevkit.com) or run `npx @moneydevkit/create` to generate credentials locally, then grab your `api_key` and mnemonic. 2. **Install the SDK** in your project: ```bash theme={null} npm install @moneydevkit/nextjs ``` 3. **Add required secrets** to `.env` (or similar): ```env theme={null} MDK_ACCESS_TOKEN=your_api_key_here MDK_MNEMONIC=your_mnemonic_here ``` ## Quick Start (Next.js App Router) ```jsx theme={null} // app/page.js 'use client' import { useCheckout } from '@moneydevkit/nextjs' import { useState } from 'react' export default function HomePage() { const { createCheckout, isLoading } = useCheckout() const [error, setError] = useState(null) const handlePurchase = async () => { setError(null) const result = await createCheckout({ type: 'AMOUNT', // or 'PRODUCTS' for product-based checkouts title: 'Describe the purchase shown to the buyer', description: 'A description of the purchase', amount: 500, // 500 USD cents or Bitcoin sats currency: 'USD', // or 'SAT' successUrl: '/checkout/success', metadata: { customField: 'internal reference for this checkout', name: 'John Doe' } }) if (result.error) { setError(result.error.message) return } window.location.href = result.data.checkoutUrl } return (
{error &&

{error}

}
) } ```
```jsx theme={null} // app/checkout/[id]/page.js "use client"; import { Checkout } from "@moneydevkit/nextjs"; import { use } from "react"; export default function CheckoutPage({ params }) { const { id } = use(params); return ; } ``` ```js theme={null} // app/api/mdk/route.js export { POST } from '@moneydevkit/nextjs/server/route' ``` ```js theme={null} // next.config.js / next.config.mjs import withMdkCheckout from '@moneydevkit/nextjs/next-plugin' export default withMdkCheckout({}) ```
You now have a complete Lightning checkout loop: the button creates a session, the dynamic route renders it, and the webhook endpoint signals your Lightning node to claim paid invoices. ## Verify successful payments When a checkout completes, use `useCheckoutSuccess()` on the success page. ```tsx theme={null} 'use client' import { useCheckoutSuccess } from '@moneydevkit/nextjs' export default function SuccessPage() { const { isCheckoutPaidLoading, isCheckoutPaid, metadata } = useCheckoutSuccess() if (isCheckoutPaidLoading || isCheckoutPaid === null) { return

Verifying payment…

} if (!isCheckoutPaid) { return

Payment has not been confirmed.

} // We set 'name' when calling createCheckout(), and it's accessible here on the success page. console.log('Customer name:', metadata?.name) // "John Doe" return (

Payment confirmed. Enjoy your purchase!

) } ``` ## Server-side payouts Programmatic payouts let your server send sats out to a Lightning destination (BOLT11 invoice, BOLT12 offer, or LNURL / Lightning address) without any user interaction. They must run from a server function (Server Action, route handler, cron, webhook), and the app must have programmatic payouts enabled in the moneydevkit dashboard. The destination is whatever your server passes in. There is no end-user confirmation. Always apply your own authorization and business rules first - who is allowed to trigger this, how much, where to. ### Minimal example ```ts theme={null} // app/actions.ts 'use server' import { programmaticPayout } from '@moneydevkit/nextjs/server' export async function sendTip(orderId: string) { const result = await programmaticPayout({ amountSats: 10_000, destination: 'lnbc...', // or 'satoshi@example.com', or 'lno1...' idempotencyKey: orderId, // pass the SAME value if you ever retry }) if (result.error) { throw new Error(result.error.message) } return result.data // { accepted: true, paymentId, paymentHash } } ``` ### About `idempotencyKey` The key is how moneydevkit dedupes retries. If your code (or a cron, or a Vercel retry) fires the same payout twice with the same key, the second call is a no-op instead of a double-pay. * **Do** use a stable id from your own database: `orderId`, `withdrawalId`, `userId + payoutDate`. * **Don't** generate a fresh `crypto.randomUUID()` on every call. That defeats the whole point and you can double-pay. * It's just a string, any length, your choice. ### Full example with error handling `result.error` tells you whether the failure is worth retrying: * `result.error.retryable === true` - transient (daily limit, dispatch failure). Retry the same call with the same `idempotencyKey`. * `result.error.retryable === false` - retrying won't help. Fix the input or your config. * `result.error.retryable === undefined` - the SDK couldn't classify it. Log and inspect. ```ts theme={null} // app/actions.ts 'use server' import { programmaticPayout } from '@moneydevkit/nextjs/server' const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) export async function sendPayout(orderId: string, destination: string) { let attempt = 0 while (attempt < 3) { const result = await programmaticPayout({ amountSats: 10_000, destination, idempotencyKey: orderId, }) if (!result.error) { return { ok: true as const, paymentId: result.data.paymentId } } switch (result.error.reason) { case 'app_scoped_api_key_required': // The API key in MDK_ACCESS_TOKEN is not attached to a specific app. // Copy the API key from your app's page in the Apps dashboard. return { ok: false as const, fatal: 'use_the_app_api_key_from_dashboard' } case 'programmatic_payouts_disabled': // Toggle is off in the dashboard for this app. return { ok: false as const, fatal: 'enable_programmatic_payouts_in_dashboard' } case 'amount_too_large': return { ok: false as const, fatal: 'amount_too_large' } case 'amount_invalid': return { ok: false as const, fatal: 'amount_invalid' } case 'daily_limit_exceeded': return { ok: false as const, fatal: 'come_back_tomorrow' } case 'payout_dispatch_failed': // Transient backend failure. Inspect error.message for the cause. // Safe to retry with the same idempotencyKey. await sleep(1_000 * 2 ** attempt) attempt++ continue default: return { ok: false as const, fatal: 'unknown_error', message: result.error.message, code: result.error.code, } } } return { ok: false as const, fatal: 'retries_exhausted' } } ``` ### Common gotchas * **Don't call from client code.** `programmaticPayout` checks for `window` and refuses to run in a browser. Server Actions, route handlers, cron jobs, or webhook receivers only. * **Set `MDK_ACCESS_TOKEN`.** Same env var as the rest of the SDK. If missing, you get `missing_access_token` (not retryable). * **Always pass the same `idempotencyKey` on retry.** Changing it makes moneydevkit treat it as a new payout - and you can double-pay. ### Error reference `result.error.reason` is a short machine-readable string. Use it for branching; use `result.error.message` for logs. | `reason` | `retryable` | What it means | | ------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `app_scoped_api_key_required` | false | The API key in `MDK_ACCESS_TOKEN` isn't tied to a specific app. Copy the key from the app's page in the Apps dashboard | | `programmatic_payouts_disabled` | false | Toggle is off in dashboard for this app | | `amount_too_large` | false | Above per-request cap | | `amount_invalid` | false | Backend rejected the amount (non-positive or non-integer sats) | | `daily_limit_exceeded` | true | 24h rolling cap hit; retry tomorrow | | `payout_dispatch_failed` | true | Backend dispatch failed (node offline, transient routing, fee issues). Inspect `error.message` for the specific cause; safe to retry with the same `idempotencyKey` | | *(undefined)* | *(undefined)* | New / unknown backend code. Log `error.code` and don't retry blindly | Client-side validation errors (always `retryable: false`): | `code` | When | | ------------------------- | ----------------------------------------------- | | `server_only` | Called from a browser runtime | | `invalid_amount` | `amountSats` is not a positive integer | | `invalid_destination` | Empty, too long, or contains control characters | | `invalid_idempotency_key` | Empty / missing | | `missing_access_token` | `MDK_ACCESS_TOKEN` not set | ## Reading the merchant balance `getBalance()` reads the spendable (outbound) balance of the Lightning node tied to your `MDK_ACCESS_TOKEN`. Same server-only constraints as `programmaticPayout`: refuses to run in a browser, routes through mdk.com over HTTPS, which in turn dials the merchant node over the WS control plane. ```ts theme={null} // app/balance/actions.ts 'use server' import { getBalance } from '@moneydevkit/nextjs/server' export async function fetchBalance() { const result = await getBalance() if (result.error) { // retryable === true: transient (merchant function spinning up, transient routing). // retryable === false: terminal (invalid key, legacy org-level key, banned user). throw new Error(result.error.message) } return result.data.balanceSats // number, in sats } ``` The first call after the merchant function has been idle may take a few seconds: mdk.com fires a spin-up webhook and waits for the node to register. Subsequent calls within the same function lifetime are fast. ### Notes * **App-scoped API key required.** Balance is meaningful per-app, not per-org. Legacy org-level keys return `GET_BALANCE_APP_KEY_REQUIRED` (not retryable). Use the API key from the App page in the dashboard. * **Server-only.** Same `typeof window` guard as `programmaticPayout`. Don't import from a client component. * **Idempotent.** Safe to retry. Transient errors are flagged `retryable: true`; auth and config errors are `retryable: false`. ### Error reference | `code` | `retryable` | What it means | | ------------------------------ | ----------- | ------------------------------------------------------------ | | `server_only` | false | Called from a browser runtime | | `missing_access_token` | false | `MDK_ACCESS_TOKEN` not set | | `GET_BALANCE_APP_KEY_REQUIRED` | false | Using a legacy org-level key. Copy the key from the App page | | `UNAUTHORIZED` / `FORBIDDEN` | false | Invalid API key or banned user | | `NOT_FOUND` | false | Procedure missing - pre-0.1.30 merchant SDK or older mdk.com | | `BAD_REQUEST` | false | Server rejected the request as malformed | | `GET_BALANCE_SPIN_UP_TIMEOUT` | true | Merchant function did not register WS in time. Safe to retry | | `get_balance_failed` | true | Network / unclassified error | ## Customers Collect customer information during checkout to track purchases and enable refunds. ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', customer: { email: 'customer@example.com', name: 'Jane Doe', externalId: 'user-123' // Your system's user ID }, requireCustomerData: ['email', 'name'] // Show form for missing fields }) ``` See the full [Customers documentation](/dashboard/customers) for details on customer matching, returning customers, and custom fields. ## Product Checkouts Sell products defined in your Money Dev Kit dashboard using `type: 'PRODUCTS'`: ```jsx theme={null} import { useCheckout, useProducts } from '@moneydevkit/nextjs' const { createCheckout } = useCheckout() const { products } = useProducts() const result = await createCheckout({ type: 'PRODUCTS', product: products[0].id, successUrl: '/checkout/success', }) ``` See the full [Products documentation](/dashboard/products) for details on creating products, pricing options, and pay-what-you-want pricing. # Replit (Express + Vite) Source: https://docs.moneydevkit.com/replit moneydevkit checkout package for Replit (Vite + React + Express) `@moneydevkit/replit` wires the moneydevkit checkout into a Replit workspace that uses Vite + React on the client and Express on the server. ## Install with Replit Agent Click the button below to install moneydevkit directly in Replit Agent: [![Install moneydevkit](https://replit.com/badge?caption=Install%20moneydevkit)](https://replit.com/integrations?mcp=eyJkaXNwbGF5TmFtZSI6Im1vbmV5ZGV2a2l0IiwiYmFzZVVybCI6Imh0dHBzOi8vbWNwLm1vbmV5ZGV2a2l0LmNvbS9tY3AifQ==) Replit Agent will: * Create an account for you * Implement moneydevkit for you **Already have a moneydevkit account?** Use this button instead: [![Install mdk (with account)](https://replit.com/badge?caption=Install%20mdk%20%28with%20account%29)](https://replit.com/integrations?mcp=eyJkaXNwbGF5TmFtZSI6Im1vbmV5ZGV2a2l0IChhdXRoZW50aWNhdGVkKSIsImJhc2VVcmwiOiJodHRwczovL21jcC5tb25leWRldmtpdC5jb20vbWNwL2FjY291bnQifQ==) Treat the rest of the docs as reference. If you're having trouble, tell the agent to follow the docs at [https://docs.moneydevkit.com/replit](https://docs.moneydevkit.com/replit) exactly. ## Before you start * When creating the Replit project, choose a full app template (not a design-only project) so you have both client and server runtimes. ## Setup 1. **Create a moneydevkit account** at [moneydevkit.com](https://moneydevkit.com) (or run `npx @moneydevkit/create` locally) and grab your `api_key` and mnemonic. 2. **Install the SDK** in your project (Express is a peer dependency): ```bash theme={null} npm install @moneydevkit/replit express ``` 3. **Add required environment variables** to `.env` (or Replit Secrets): ```env theme={null} MDK_ACCESS_TOKEN=your_api_key_here MDK_MNEMONIC=your_mnemonic_here ``` Both environment variables are required—`MDK_ACCESS_TOKEN` authorizes API calls and `MDK_MNEMONIC` is your wallet seed. If either is missing, checkout creation will fail. 4. **Allowlist moneydevkit packages** in `script/build.ts` so Replit’s bundler can import them: ```ts theme={null} // script/build.ts const allowlist = [ '@moneydevkit/core', '@moneydevkit/replit', // ... other packages ] ``` 5. **Agent: copy these instructions** to `replit.md` for easy access ## Backend: Express route Mount the unified moneydevkit endpoint at `/api/mdk`: ```ts theme={null} // server/index.ts (or server.js) import express from 'express' import { createMdkExpressRouter } from '@moneydevkit/replit/server/express' const app = express() app.use('/api/mdk', createMdkExpressRouter()) app.listen(3000, () => { console.log('Server listening on http://localhost:3000') }) ``` ## Frontend: Vite + React Trigger a checkout from your client code (the checkout component pulls in its own CSS; import `@moneydevkit/replit/mdk-styles.css` once globally only if you want to preload it): ```tsx theme={null} // src/App.tsx import { useCheckout } from '@moneydevkit/replit' import { useState } from 'react' export default function App() { const { createCheckout, isLoading } = useCheckout() const [error, setError] = useState(null) const handlePurchase = async () => { setError(null) const result = await createCheckout({ type: 'AMOUNT', // or 'PRODUCTS' for product-based checkouts title: 'Purchase title for the buyer', description: 'Description of the purchase', amount: 500, currency: 'USD', successUrl: '/checkout/success', metadata: { name: 'John Doe' }, }) if (result.error) { setError(result.error.message) return } window.location.href = result.data.checkoutUrl } return (
{error &&

{error}

}
) } ``` Render the hosted checkout page: ```tsx theme={null} // src/routes/checkout/[id].tsx (or similar) import { Checkout } from '@moneydevkit/replit' export default function CheckoutPage({ params }: { params: { id: string } }) { return } ``` Verify successful payments: ```tsx theme={null} import { useCheckoutSuccess } from '@moneydevkit/replit' export function SuccessPage() { const { isCheckoutPaidLoading, isCheckoutPaid, metadata } = useCheckoutSuccess() if (isCheckoutPaidLoading || isCheckoutPaid === null) return

Verifying payment…

if (!isCheckoutPaid) return

Payment has not been confirmed.

return

Payment confirmed for {metadata?.name ?? 'customer'}.

} ``` This wiring keeps the client pointed at `/api/mdk`, which the Express route handles by delegating to the shared moneydevkit core logic. ## Customers Collect customer information during checkout to track purchases and enable refunds. ```tsx theme={null} const result = await createCheckout({ type: 'AMOUNT', title: 'Product Name', amount: 500, currency: 'USD', successUrl: '/checkout/success', customer: { email: 'customer@example.com', name: 'Jane Doe', externalId: 'user-123' // Your system's user ID }, requireCustomerData: ['email', 'name'] // Show form for missing fields }) ``` See the full [Customers documentation](/dashboard/customers) for details on customer matching, returning customers, and custom fields. ## Product Checkouts Sell products defined in your Money Dev Kit dashboard using `type: 'PRODUCTS'`: ```tsx theme={null} import { useCheckout, useProducts } from '@moneydevkit/replit' const { createCheckout } = useCheckout() const { products } = useProducts() const result = await createCheckout({ type: 'PRODUCTS', product: products[0].id, successUrl: '/checkout/success', }) ``` See the full [Products documentation](/dashboard/products) for details on creating products, pricing options, and pay-what-you-want pricing. # Subscriptions Source: https://docs.moneydevkit.com/subscriptions Set up recurring payments with Lightning-powered subscriptions Subscriptions let you charge customers on a recurring basis using Lightning payments. Customers pay once to start their subscription, then receive renewal reminders before each billing period. ## What are Subscriptions? Subscriptions in moneydevkit work differently from traditional card-based subscriptions: * **No stored payment methods** - Lightning payments are push-based, so customers pay proactively * **Renewal reminders** - Customers receive emails 7, 4, and 1 days before their subscription renews * **Grace period** - If a customer misses a payment, they have 3 days to renew before cancellation * **Instant settlement** - Payments settle immediately in Bitcoin ## Setup In your [moneydevkit dashboard](https://moneydevkit.com/dashboard/products), create a product with a recurring interval: 1. Navigate to Products 2. Click the **+** button 3. Fill in product details 4. Select a **Recurring interval**: Monthly, Quarterly, or Yearly 5. Click **Create Product** Use the recurring product ID when creating checkouts: ```tsx theme={null} import { useCheckout } from '@moneydevkit/nextjs' function SubscribeButton() { const { createCheckout, isLoading } = useCheckout() const handleSubscribe = async () => { const result = await createCheckout({ type: 'PRODUCTS', product: 'your-recurring-product-id', successUrl: '/checkout/success', customer: { externalId: user.id, // Link to your user email: user.email, name: user.name, }, }) if (result.error) { console.error(result.error.message) return } window.location.href = result.data.checkoutUrl } return ( ) } ``` Subscriptions require customer data. Always pass `customer.email` so renewal reminders can be sent. Use the `useCustomer` hook to check if a user has an active subscription: ```tsx theme={null} import { useCustomer } from '@moneydevkit/nextjs' function PremiumContent() { const { customer, isLoading } = useCustomer({ externalId: user.id }) if (isLoading) return

Loading...

if (!customer?.hasActiveSubscription) { return } return
Premium content here
} ```
## Subscription Lifecycle ``` ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ ┌──────────┐ payment ┌────────┐ period ends ┌────────┐ │ │ │ Checkout │ ───────────▶ │ Active │ ───────────────▶ │past_due│ │ │ └──────────┘ └────────┘ └────────┘ │ │ │ │ │ │ │ cancel │ │ │ ▼ │ │ │ ┌────────┐ 3 day grace │ │ │ │canceled│ ◀─────────────────────┘ │ │ └────────┘ │ │ ▲ │ │ │ renewal payment │ │ │ │ │ ┌────────┐ │ │ │ Active │ (extended) │ │ └────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` | Status | Description | | ---------- | ------------------------------------------------------------------------- | | `active` | Subscription is active and within the current billing period | | `past_due` | Billing period ended, customer is in the 3-day grace period | | `canceled` | Subscription has been canceled (either by customer or after grace period) | ## Checking Subscription Status The `useCustomer` hook fetches customer data including their subscriptions: ```tsx theme={null} import { useCustomer } from '@moneydevkit/nextjs' function AccountPage() { const { customer, isLoading, error, refetch } = useCustomer({ externalId: user.id }) if (isLoading) return

Loading...

if (error) return

Error: {error.message}

if (!customer) return

Customer not found

return (

Subscription Status

{customer.hasActiveSubscription ? (

You have an active subscription

) : (

No active subscription

)}

All Subscriptions

    {customer.subscriptions.map(sub => (
  • {sub.status} - {sub.recurringInterval}
  • ))}
) } ``` ### CustomerIdentifier Types You can identify customers in three ways: | Identifier | Description | Example | | ------------ | ------------------------ | ------------------------------- | | `externalId` | Your app's user ID | `{ externalId: user.id }` | | `email` | Customer's email address | `{ email: 'user@example.com' }` | | `customerId` | moneydevkit customer ID | `{ customerId: 'cus_abc123' }` | ## Grace Period When a subscription's billing period ends without payment: 1. **Day 0** - Status changes to `past_due`, first grace period email sent 2. **Day 1** - Second grace period reminder 3. **Day 2** - Third grace period reminder 4. **Day 3** - Subscription auto-cancels if no payment received During the grace period, customers can still renew their subscription by clicking the renewal link in their email. ## Renewal Reminders Customers receive renewal reminder emails at: * **7 days** before the billing period ends * **4 days** before the billing period ends * **1 day** before the billing period ends Each email includes a link for the customer to renew their subscription. ## Recurring Intervals | Interval | Value | Billing Cycle | | --------- | --------- | --------------- | | Monthly | `MONTH` | Every 1 month | | Quarterly | `QUARTER` | Every 3 months | | Yearly | `YEAR` | Every 12 months | Billing dates use calendar math. A subscription started on January 31st will renew on February 28th (or 29th in leap years), then March 31st, and so on. ## Price Units Product prices are returned in base currency units: * **USD**: cents (divide by 100 for dollars) * **SAT**: satoshis (no conversion needed) ```tsx theme={null} const { products } = useProducts() // USD prices are in cents - divide by 100 for display const displayPrice = (product.prices[0]?.priceAmount ?? 0) / 100 ``` # Troubleshooting Source: https://docs.moneydevkit.com/troubleshooting Common Issues and Fixes # Payment Failures > I get to the checkout screen but my wallet cannot pay, hangs at pending, or fails with something like "No route found" The most common cause of payment failures is incomplete implementation—usually an AI agent not following the documentation exactly. Double-check that every step has been completed correctly. ## Check your implementation Review the docs for your specific implementation and verify each step has been completed: * [Next.js](/nextjs) * [Replit / Express](/replit) ## Make sure your app URL is correct In your [dashboard](https://moneydevkit.com/dashboard), set your URL to the base URL where your app is hosted: `https://yourapp.com` This is how moneydevkit reaches your app to complete payments. ## Testing locally To test production payments locally, you need to expose your local server to the internet. We recommend [ngrok](https://ngrok.com): ```bash theme={null} ngrok http 3000 ``` Then set your app URL in the dashboard to something like `https://.ngrok-free.app` # Webhooks Source: https://docs.moneydevkit.com/webhooks Receive real-time notifications when payment events occur Webhooks let your application receive real-time HTTP notifications when events happen in your moneydevkit account — like a checkout completing or a subscription renewing. Instead of polling the API, you register an endpoint URL and moneydevkit pushes events to you as they occur. ## Event Types | Event | Description | | ----------------------- | --------------------------------------------------------------- | | `checkout.completed` | A checkout session was paid successfully | | `subscription.created` | A new subscription was created after first payment | | `subscription.renewed` | An existing subscription was renewed for another period | | `subscription.canceled` | A subscription was canceled (by customer or after grace period) | ## Setup In your [dashboard](https://moneydevkit.com/dashboard), navigate to **Apps → your app → Webhooks → Add Endpoint**. Enter the URL where you want to receive events (e.g. `https://yourapp.com/api/webhooks/mdk`), then select the event types you want to subscribe to. After creating the endpoint, copy the **signing secret** (starts with `whsec_`). Store it in your environment variables and install the `standardwebhooks` package for signature verification: ```env theme={null} MDK_WEBHOOK_SECRET=whsec_your_secret_here ``` ```bash theme={null} npm install standardwebhooks ``` Keep your signing secret private. Never commit it to source control or expose it in client-side code. Add an endpoint in your app to receive and verify webhook events. Here's an example using Next.js App Router: ```ts theme={null} // app/api/webhooks/mdk/route.ts import { NextRequest, NextResponse } from "next/server" import { Webhook } from "standardwebhooks" const secret = process.env.MDK_WEBHOOK_SECRET! export async function POST(req: NextRequest) { const body = await req.text() const headers = { "webhook-id": req.headers.get("webhook-id") ?? "", "webhook-timestamp": req.headers.get("webhook-timestamp") ?? "", "webhook-signature": req.headers.get("webhook-signature") ?? "", } // Verify the signature const wh = new Webhook(secret) let payload: Record try { payload = wh.verify(body, headers) as Record } catch { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }) } // Event-specific fields are in payload.data const { type, data } = payload as { type: string, data: Record } switch (type) { case "checkout.completed": // Fulfill the order — data.amountSats, data.customer, etc. break case "subscription.created": // Activate the subscription — data.customerId, data.productId, etc. break case "subscription.renewed": // Extend access for the new period break case "subscription.canceled": // Revoke access or schedule revocation break } return NextResponse.json({ received: true }) } ``` ## Event Payloads Every webhook payload is wrapped in a standard envelope with `type`, `timestamp`, `id`, and a `data` object containing the event-specific fields. ### checkout.completed Sent when a checkout session is paid successfully. ```json theme={null} { "type": "checkout.completed", "timestamp": "2025-07-01T12:00:00.000Z", "id": "evt_abc123", "data": { "status": "COMPLETED", "amountSats": 10000, "currency": "SAT", "netAmount": 9900, "metadata": {}, "customer": { "email": "alice@example.com", "name": "Alice" }, "product": { "id": "prod_xyz", "name": "Premium Plan" } } } ``` ### subscription.created Sent when a customer's first subscription payment is confirmed. ```json theme={null} { "type": "subscription.created", "timestamp": "2025-07-01T12:00:00.000Z", "id": "evt_def456", "data": { "status": "active", "customerId": "cus_abc", "productId": "prod_xyz", "amount": 10000, "currency": "SAT", "recurringInterval": "MONTH", "currentPeriodEnd": "2025-08-01T00:00:00.000Z", "customer": { "email": "alice@example.com", "name": "Alice" } } } ``` ### subscription.renewed Sent when a subscription is successfully renewed. Same shape as `subscription.created`. ```json theme={null} { "type": "subscription.renewed", "timestamp": "2025-08-01T12:00:00.000Z", "id": "evt_ghi789", "data": { "status": "active", "customerId": "cus_abc", "productId": "prod_xyz", "amount": 10000, "currency": "SAT", "recurringInterval": "MONTH", "currentPeriodEnd": "2025-09-01T00:00:00.000Z", "customer": { "email": "alice@example.com", "name": "Alice" } } } ``` ### subscription.canceled Sent when a subscription is canceled. ```json theme={null} { "type": "subscription.canceled", "timestamp": "2025-07-15T12:00:00.000Z", "id": "evt_jkl012", "data": { "status": "canceled", "customerId": "cus_abc", "productId": "prod_xyz", "cancelAtPeriodEnd": true, "canceledAt": "2025-07-15T12:00:00.000Z", "endsAt": "2025-08-01T00:00:00.000Z", "customer": { "email": "alice@example.com", "name": "Alice" } } } ``` ## Signature Verification moneydevkit signs every webhook using the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Each request includes three headers: | Header | Description | | ------------------- | ------------------------------------------------ | | `webhook-id` | Unique message ID (use for idempotency) | | `webhook-timestamp` | Unix timestamp (seconds) when the event was sent | | `webhook-signature` | HMAC-SHA256 signature of the payload | The `standardwebhooks` library (used in the handler above) validates the signature, checks that the timestamp is recent (rejecting replay attacks), and returns the parsed payload. ## Retry Behavior If your endpoint returns a non-2xx status code or the request times out, moneydevkit retries with increasing delays: | Attempt | Delay after failure | | --------- | ------------------- | | 1st retry | 5 seconds | | 2nd retry | 5 minutes | | 3rd retry | 30 minutes | | 4th retry | 2 hours | | 5th retry | 8 hours | After **5 consecutive failures** across any events, the endpoint is automatically disabled. You can re-enable it from the dashboard after fixing the issue. ## Managing Endpoints From the dashboard (**Apps → your app → Webhooks**) you can: * **Toggle** endpoints on/off without deleting them * **Rotate** the signing secret if it's been compromised * **Delete** endpoints you no longer need Respond to webhooks with a `2xx` status code as quickly as possible. Do any heavy processing asynchronously after acknowledging receipt. moneydevkit times out after 30 seconds — if your handler takes longer, the delivery will be retried.