> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moneydevkit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Embed the moneydevkit checkout loop inside your Next.js App Router project

<Info>
  `@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.
</Info>

## Install with AI Coding Tools

Use an AI coding assistant to set up moneydevkit in your project. Choose the option that matches your situation:

<Tabs>
  <Tab title="New Account">
    Creates a moneydevkit account and implements the SDK for you.

    <CardGroup cols={2}>
      <Card title="Cursor" icon="wand-magic-sparkles" href="cursor://anysphere.cursor-deeplink/mcp/install?name=moneydevkit&config=eyJ1cmwiOiJodHRwczovL21jcC5tb25leWRldmtpdC5jb20vbWNwLyJ9">
        Click to install MCP in Cursor
      </Card>

      <Card title="VS Code" icon="code" href="vscode://mcp/install?name=moneydevkit&config=%7B%22url%22%3A%22https%3A//mcp.moneydevkit.com/mcp/%22%7D">
        Click to install MCP in VS Code
      </Card>
    </CardGroup>

    **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/
    ```
  </Tab>

  <Tab title="Existing Account">
    Already have a moneydevkit account? Use these to connect your existing credentials.

    <CardGroup cols={2}>
      <Card title="Cursor" icon="wand-magic-sparkles" href="cursor://anysphere.cursor-deeplink/mcp/install?name=moneydevkit&config=eyJ1cmwiOiJodHRwczovL21jcC5tb25leWRldmtpdC5jb20vbWNwL2FjY291bnQvIn0=">
        Click to install MCP in Cursor
      </Card>

      <Card title="VS Code" icon="code" href="vscode://mcp/install?name=moneydevkit&config=%7B%22url%22%3A%22https%3A//mcp.moneydevkit.com/mcp/account/%22%7D">
        Click to install MCP in VS Code
      </Card>
    </CardGroup>

    **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/
    ```
  </Tab>
</Tabs>

## 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)

<Steps>
  <Step title="Trigger a checkout from any client component">
    ```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 (
        <div>
          {error && <p style={{ color: 'red' }}>{error}</p>}
          <button onClick={handlePurchase} disabled={isLoading}>
            {isLoading ? 'Creating checkout…' : 'Buy Now'}
          </button>
        </div>
      )
    }
    ```
  </Step>

  <Step title="Render the hosted checkout page">
    ```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 <Checkout id={id} />;
    }
    ```
  </Step>

  <Step title="Expose the unified Money Dev Kit endpoint">
    ```js theme={null}
    // app/api/mdk/route.js
    export { POST } from '@moneydevkit/nextjs/server/route'
    ```
  </Step>

  <Step title="Configure Next.js">
    ```js theme={null}
    // next.config.js / next.config.mjs
    import withMdkCheckout from '@moneydevkit/nextjs/next-plugin'

    export default withMdkCheckout({})
    ```
  </Step>
</Steps>

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 <p>Verifying payment…</p>
  }

  if (!isCheckoutPaid) {
    return <p>Payment has not been confirmed.</p>
  }

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

  return (
    <div>
      <p>Payment confirmed. Enjoy your purchase!</p>
    </div>
  )
}
```

## 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.

<Warning>
  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.
</Warning>

### 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
}
```

<Note>
  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.
</Note>

### 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.
