Skip to content

Plan: Lightning Address payment hash verification API endpoint #966

@orveth

Description

@orveth

Summary

Build a payment hash verification API endpoint that CDK mints call to validate whether a payment hash belongs to a merchant before allowing ecash to be melted. This enables closed-loop lightning address payments.

Context

The CDK ClosedLoopPayment decorator (see CDK plan) intercepts melt requests and calls this API to verify: "does this payment hash belong to this merchant?" If yes, the melt proceeds. If no, it's rejected.

API Specification

GET /api/payment-hash/{merchant_user_id}/{payment_hash}

Success Response (200)

{
  "status": "OK",
  "found": true,
  "state": "PENDING",
  "created_at": "2026-03-30T..."
}

Not Found Response (404)

{
  "status": "ERROR",
  "reason": "Payment hash not found for this merchant"
}

Implementation Plan

1. Create the API route

File: app/routes/api.payment-hash.$merchantUserId.$paymentHash.ts

Follow existing patterns from api.lnurlp.callback.$userId.ts:

  • Export a loader function (GET only)
  • Return JSON with CORS headers (Access-Control-Allow-Origin: *)
  • Use LNURL-style error format

2. Query payment hashes from existing tables

Payment hashes are already stored in:

  • wallet.cashu_receive_quotes — has payment_hash column
  • wallet.spark_receive_quotes — has payment_hash column

Query both tables for matching (user_id, payment_hash) where user_id = merchant's Agicash user ID.

3. Return payment state

Map quote states to response:

  • UNPAID → invoice created but not yet paid
  • PENDING → payment in flight
  • PAID/COMPLETED → payment settled
  • EXPIRED/FAILED → invoice expired or failed

4. Database indexing

Check if (user_id, payment_hash) index exists on receive quote tables. Spark send quotes already have spark_send_quotes_payment_hash_active_unique — receive quotes may need similar indexing since this endpoint will be called frequently by CDK mints during melt operations.

5. No auth required

This endpoint is called by CDK mints (server-to-server), not by users. No Supabase auth needed. The payment hash itself acts as a capability — knowing it proves you have the invoice.

Design Decisions

  • Fail closed: If the API is unreachable or errors, the CDK mint rejects the melt (safety default)
  • Merchant identifier: Agicash user ID (UUID), not username (stable across renames)
  • Scope: Melt validation only — this doesn't affect mint or receive flows
  • Both quote types: Must check both Cashu and Spark receive quotes since either could have generated the invoice

Existing Patterns to Follow

  • Route structure: app/routes/api.*.ts with loader export
  • Service layer: Create PaymentHashVerificationService similar to LightningAddressService
  • Response format: JSON + CORS, LNURL error style
  • Money handling: Use Money class from @agicash/sdk/lib/money

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions