Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/mdk-nextjs-demo/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ MDK_MNEMONIC=your_mnemonic_words
# MDK_RGS_URL=
# MDK_LSP_NODE_ID=
# MDK_LSP_ADDRESS=

# Payout address - accepts LNURL, Lightning Address, Bolt12 Offer, or BIP-353
# PAYOUT_ADDRESS=

# Deprecated: Use PAYOUT_ADDRESS instead
# WITHDRAWAL_BOLT_11=
# WITHDRAWAL_BOLT_12=
# WITHDRAWAL_LNURL=
1 change: 1 addition & 0 deletions packages/core/src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './list_channels'
export * from './pay_bolt_11'
export * from './pay_bolt_12'
export * from './pay_ln_url'
export * from './payout'
export * from './ping'
export * from './sync_rgs'
export * from './webhooks'
5 changes: 3 additions & 2 deletions packages/core/src/handlers/pay_bolt_11.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { log } from "../logging";
import { createMoneyDevKitNode } from "../mdk";
import { getPayoutAddressForType } from "../payout-address";

export async function handlePayBolt11(_request: Request): Promise<Response> {
try {
const node = createMoneyDevKitNode();
const bolt11Invoice = process.env.WITHDRAWAL_BOLT_11;
const bolt11Invoice = getPayoutAddressForType('bolt11');

log("Initiating Bolt 11 payment flow");

if (!bolt11Invoice) {
return new Response("Bolt 11 invoice not configured", { status: 500 });
}

node.payBolt11(bolt11Invoice);
await node.payBolt11(bolt11Invoice);

return new Response("OK", { status: 200 });
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/handlers/pay_bolt_12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";

import { log } from "../logging";
import { createMoneyDevKitNode } from "../mdk";
import { getPayoutAddressForType } from "../payout-address";

const bolt12Schema = z.object({
amount: z.number().positive(),
Expand All @@ -13,7 +14,7 @@ export async function handlePayBolt12(request: Request): Promise<Response> {
const parsed = bolt12Schema.parse(body);

const node = createMoneyDevKitNode();
const bolt12Offer = process.env.WITHDRAWAL_BOLT_12;
const bolt12Offer = getPayoutAddressForType('bolt12');

log("Initiating Bolt 12 payment flow");

Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/handlers/pay_ln_url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";

import { log } from "../logging";
import { createMoneyDevKitNode } from "../mdk";
import { getPayoutAddressForType, getPayoutConfig } from "../payout-address";

const lnurlSchema = z.object({
amount: z.number().positive(),
Expand All @@ -13,7 +14,22 @@ export async function handlePayLNUrl(request: Request): Promise<Response> {
const parsed = lnurlSchema.parse(body);

const node = createMoneyDevKitNode();
const lnurl = process.env.WITHDRAWAL_LNURL;

// LNURL handler supports LNURL, Lightning Address, and BIP-353 formats
const config = getPayoutConfig();
let lnurl: string | null = null;

if (config.address) {
const { type, address } = config.address;
if (type === 'lnurl' || type === 'lightning_address' || type === 'bip353') {
lnurl = address;
}
}

// Fall back to type-specific lookup for legacy compatibility
if (!lnurl) {
lnurl = getPayoutAddressForType('lnurl');
}

log("Initiating LNURL payment flow");

Expand Down
87 changes: 87 additions & 0 deletions packages/core/src/handlers/payout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { z } from 'zod'

import { log } from '../logging'
import { createMoneyDevKitNode } from '../mdk'
import { getPayoutConfig, PayoutAddressType } from '../payout-address'

const payoutSchema = z.object({
amount: z.number().positive(),
})

function jsonResponse(status: number, body: Record<string, unknown>) {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
})
}

/**
* Unified payout handler that automatically detects the payout address type
* from PAYOUT_ADDRESS env var and routes to the appropriate payment method.
*
* Supports: LNURL, Lightning Address, Bolt12 Offer, BIP-353
*
* Request body:
* - amount: number (required) - amount in millisatoshis
*/
export async function handlePayout(request: Request): Promise<Response> {
try {
const body = await request.json()
const parsed = payoutSchema.safeParse(body)

if (!parsed.success) {
return jsonResponse(400, {
error: 'Invalid payout request',
details: parsed.error.issues,
})
}

const config = getPayoutConfig()

if (!config.address) {
return jsonResponse(500, {
error: 'Payout address not configured. Set PAYOUT_ADDRESS environment variable.',
})
}

const { address, type } = config.address
const { amount } = parsed.data

log(`Initiating payout flow with ${type} address`)

const node = createMoneyDevKitNode()

switch (type) {
case 'bolt12':
await node.payBolt12Offer(address, amount)
break

case 'lnurl':
case 'lightning_address':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For lightning_address as it is implemented now I think the payment type is still ambiguous. I think we have to try fetching an invoice first from an LNURL server then fallback to fetching payment instructions from a DNS server. could also it it the other way around but LNUrl is more common right now.

case 'bip353':
// LNURL, Lightning Address, and BIP-353 all use the same payment method
await node.payLNUrl(address, amount)
Copy link
Contributor

@amackillop amackillop Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to parse the payment instructions at the bip353 address to figure out what to use? Is it common for people to put an lnurl address behind bip-353? Thought sticking a Bolt12 offer at the bip-353 address would be the most common case (onchain aside)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is leveraging the fact that payLNUrl is actually just doing bitcoin-payment-instructions and handles it for you

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Confusing method name

break

case 'bolt11':
// Bolt11 invoices are typically one-time use with a fixed amount
// The amount parameter is ignored for bolt11
await node.payBolt11(address)
break

default:
return jsonResponse(500, {
error: `Unsupported payout address type: ${type satisfies never}`,
})
}

return jsonResponse(200, {
success: true,
type,
amount,
})
} catch (error) {
console.error('Payout error:', error)
return jsonResponse(500, { error: 'Internal Server Error' })
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './mdk-client'
export * from './mdk-config'
export * from './money-dev-kit'
export * from './payment-state'
export * from './payout-address'
export * from './preview'
export * from './startup-validation'
export * from './types'
Expand Down
Loading