DamageBDD NWC API Manual
DamageBDD NWC API Manual
This manual describes the authenticated NWC API exposed by DamageBDD for creating, inspecting, and revoking ledger-backed Nostr Wallet Connect connections, and it makes the permission boundary around ledger credit explicit.
The current handler exposes these POST endpoints:
/api/nwc/mint/api/nwc/revoke/api/nwc/ledger/balance/api/nwc/ledger/credit
The important rule is simple:
/api/nwc/ledger/creditis a node-admin operation, not a general client top-up endpoint.
So there are really two flows to think about:
- Current implemented flow: admins can mint, inspect, credit, and revoke.
- Recommended public top-up flow: regular users mint, inspect, and revoke, but top-up happens through a separate invoice or quote endpoint that eventually triggers an internal admin credit after settlement.
Authentication is delegated to the standard Damage token flow. In practice, you
first obtain an access_token from /accounts/auth/ and then send it as:
Authorization: Bearer <access_token>
The server also accepts the token as an access_token query parameter or via
sessionid cookie, but the Bearer header is the cleanest option for scripts.
Authentication
Get an access token
curl -sS -X POST \ https://run.dev.damagebdd.com/accounts/auth/ \ -H 'content-type: application/json' \ -d '{ "username": "you@example.com", "password": "your-password" }'
Expected JSON response:
{
"status": "ok",
"access_token": "...",
"address": "ak_..."
}
Store the token for later:
export DAMAGE_BASE='https://run.dev.damagebdd.com' export DAMAGE_TOKEN='PASTE_ACCESS_TOKEN_HERE'
Python helper for authenticated requests
import requests BASE = "https://run.dev.damagebdd.com" TOKEN = "PASTE_ACCESS_TOKEN_HERE" session = requests.Session() session.headers.update({ "Authorization": f"Bearer {TOKEN}", "content-type": "application/json", "accept": "application/json", }) def post_json(path, payload): response = session.post(f"{BASE}{path}", json=payload, timeout=60) response.raise_for_status() if response.content: return response.json() return None
Endpoint: /api/nwc/mint
Creates a new NWC connection for the authenticated user. The handler generates a new client secret, derives the client pubkey, resolves the user ledger, and returns an NWC URI. If the user has no registered ledger yet, it may either set it up automatically or return setup intents, depending on execution mode and key availability.
Request body
relays: list of relay URLs. Optional.max_single_sat: per-payment ceiling in sats. Optional. Default10000.max_total_sat: total ledger ceiling in sats. Optional. Default100000.expires_height: chain height expiry. Optional. Default0.
curl example
curl -sS -X POST "$DAMAGE_BASE/api/nwc/mint" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d '{ "relays": ["wss://relay.damus.io"], "max_single_sat": 1000, "max_total_sat": 5000, "expires_height": 0 }'
Python example
mint = post_json("/api/nwc/mint", { "relays": ["wss://relay.damus.io"], "max_single_sat": 1000, "max_total_sat": 5000, "expires_height": 0, }) print(mint) client_pubkey = mint["client_pubkey"] nwc_uri = mint["nwc_uri"]
Typical success response
{
"status": "ok",
"owner": "ak_...",
"ledger_ct": "ct_...",
"ledger_mode": "server_signed",
"client_pubkey": "<64-hex>",
"secret_hex": "<32-byte-secret-hex>",
"nwc_uri": "nostr+walletconnect://<wallet_pubkey>?relay=wss://...&secret=<secret_hex>",
"wallet_pubkey": "<64-hex>",
"relay": "wss://relay.damus.io",
"intents": []
}
Notes
- The current code returns HTTP
200with a JSON body on success, not204. secret_hexis only returned at mint time, so persist it immediately if your client needs it.- In
user_signedmode,intentscontains a ledger registration intent instead of server-side execution. - In missing-ledger cases, the server may either auto-setup the ledger and still
return
status: "ok", or returnstatus: "needs_ledger_setup"with setup intents.
Endpoint: /api/nwc/ledger/balance
Looks up the ledger-backed balance state for a given client_pubkey under the
currently authenticated owner.
Request body
client_pubkey: required NWC client pubkey.
curl example
curl -sS -X POST "$DAMAGE_BASE/api/nwc/ledger/balance" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d '{ "client_pubkey": "YOUR_CLIENT_PUBKEY" }'
Python example
balance = post_json("/api/nwc/ledger/balance", { "client_pubkey": client_pubkey, }) print(balance)
Response shape
{
"status": "ok",
"owner": "ak_...",
"ledger_ct": "ct_...",
"client_pubkey": "<64-hex>",
"result": {
"return_type": "ok",
"return_value": "..."
}
}
The exact shape of result.return_value depends on the smart-contract entrypoint
serialization returned by ledger_call_user(..., "balance", ...).
Endpoint: /api/nwc/ledger/credit
Credits a ledger-backed NWC connection by a deterministic amount. This endpoint is explicitly node-admin-only and should be treated as a privileged operation. It is not the right endpoint for ordinary account self-service top-up.
Request body
client_pubkey: required NWC client pubkey.amount_sat: amount to credit in sats.ref: reference string.meta: metadata string, commonly JSON encoded text.
curl example for node admins
curl -sS -X POST "$DAMAGE_BASE/api/nwc/ledger/credit" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d '{ "client_pubkey": "YOUR_CLIENT_PUBKEY", "amount_sat": 2000, "ref": "bdd-credit-1", "meta": "{}" }'
Python example for node admins
credit = post_json("/api/nwc/ledger/credit", { "client_pubkey": client_pubkey, "amount_sat": 2000, "ref": "bdd-credit-1", "meta": "{}", }) print(credit)
Success response
{
"status": "ok",
"owner": "ak_...",
"ledger_ct": "ct_...",
"credited_sat": 2000,
"intents": []
}
Notes
- The handler converts
amount_satto millisatoshis before calling the ledger. - In
user_signedmode, the response may include credit intents instead of the server executing the mutation. - The code expects
role := in request state before allowing this endpoint. - For public user top-up, use a separate invoice or quote flow. Do not expose this endpoint to non-admin clients.
Endpoint: /api/nwc/revoke
Revokes a previously minted NWC connection identified by client_pubkey.
Request body
client_pubkey: required NWC client pubkey.
curl example
curl -sS -X POST "$DAMAGE_BASE/api/nwc/revoke" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d '{ "client_pubkey": "YOUR_CLIENT_PUBKEY" }'
Python example
revoke = post_json("/api/nwc/revoke", { "client_pubkey": client_pubkey, }) print(revoke)
Success response
{
"status": "ok",
"revoked": true,
"client_pubkey": "<64-hex>",
"ledger_ct": "ct_...",
"intents": []
}
In user_signed mode, intents may contain a revoke intent instead.
Recommended public top-up design
This section describes the cleaner public model for self-service funding. It documents the boundary you clarified:
- normal accounts do not call
/api/nwc/ledger/credit - node admins do
- clients top up through a separate payment challenge or invoice flow
- settlement triggers an internal admin credit
A practical public endpoint would look like:
/api/nwc/topup_invoice
with a request body like:
{
"client_pubkey": "<64-hex>",
"amount_sat": 2000
}
and a response like:
{
"status": "payment_required",
"client_pubkey": "<64-hex>",
"amount_sat": 2000,
"invoice": "lnbc...",
"payment_hash": "...",
"expires_at": "2026-04-02T12:34:56Z"
}
After settlement, the server would internally perform the same ledger credit logic with a deterministic reference such as the Lightning payment hash.
That gives you:
- self-service top-up for ordinary users
- no public arbitrary credit mutation
- idempotent settlement processing
- a clean place to return L402-style responses when balance is too low
End-to-end example: regular account flow
This example uses only the endpoints a normal authenticated account should rely on today.
curl workflow
export DAMAGE_BASE='https://run.dev.damagebdd.com' export DAMAGE_TOKEN='PASTE_ACCESS_TOKEN_HERE' MINT_JSON=$(curl -sS -X POST "$DAMAGE_BASE/api/nwc/mint" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d '{ "relays": ["wss://relay.damus.io"], "max_single_sat": 1000, "max_total_sat": 5000, "expires_height": 0 }') printf '%s\n' "$MINT_JSON" CLIENT_PUBKEY=$(printf '%s' "$MINT_JSON" | jq -r '.client_pubkey') curl -sS -X POST "$DAMAGE_BASE/api/nwc/ledger/balance" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d "{\"client_pubkey\":\"$CLIENT_PUBKEY\"}" curl -sS -X POST "$DAMAGE_BASE/api/nwc/revoke" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d "{\"client_pubkey\":\"$CLIENT_PUBKEY\"}"
Python workflow
mint = post_json("/api/nwc/mint", { "relays": ["wss://relay.damus.io"], "max_single_sat": 1000, "max_total_sat": 5000, "expires_height": 0, }) client_pubkey = mint["client_pubkey"] print("NWC URI:", mint["nwc_uri"]) balance_1 = post_json("/api/nwc/ledger/balance", { "client_pubkey": client_pubkey, }) print("Initial balance:", balance_1) revoke = post_json("/api/nwc/revoke", { "client_pubkey": client_pubkey, }) print("Revoke result:", revoke)
End-to-end example: node admin credit flow
This flow is for node admins only.
curl workflow
export DAMAGE_BASE='https://run.dev.damagebdd.com' export DAMAGE_TOKEN='PASTE_ADMIN_ACCESS_TOKEN_HERE' MINT_JSON=$(curl -sS -X POST "$DAMAGE_BASE/api/nwc/mint" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d '{ "relays": ["wss://relay.damus.io"], "max_single_sat": 1000, "max_total_sat": 5000, "expires_height": 0 }') CLIENT_PUBKEY=$(printf '%s' "$MINT_JSON" | jq -r '.client_pubkey') curl -sS -X POST "$DAMAGE_BASE/api/nwc/ledger/credit" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d "{\"client_pubkey\":\"$CLIENT_PUBKEY\",\"amount_sat\":2000,\"ref\":\"bdd-credit-1\",\"meta\":\"{}\"}" curl -sS -X POST "$DAMAGE_BASE/api/nwc/ledger/balance" \ -H "Authorization: Bearer $DAMAGE_TOKEN" \ -H 'content-type: application/json' \ -d "{\"client_pubkey\":\"$CLIENT_PUBKEY\"}"
Python workflow
mint = post_json("/api/nwc/mint", { "relays": ["wss://relay.damus.io"], "max_single_sat": 1000, "max_total_sat": 5000, "expires_height": 0, }) client_pubkey = mint["client_pubkey"] credit = post_json("/api/nwc/ledger/credit", { "client_pubkey": client_pubkey, "amount_sat": 2000, "ref": "bdd-credit-1", "meta": "{}", }) print("Credit result:", credit) balance_2 = post_json("/api/nwc/ledger/balance", { "client_pubkey": client_pubkey, }) print("Balance after credit:", balance_2)
Error handling
Common error envelopes from this handler include:
{
"status": "error",
"error": "NO_LEDGER",
"reason": "..."
}
Mint can also return a setup-oriented response:
{
"status": "needs_ledger_setup",
"reason": "...",
"account_registry_ct": "ct_...",
"client_pubkey": "<64-hex>",
"secret_hex": "...",
"nwc_uri": "nostr+walletconnect://...",
"wallet_pubkey": "...",
"relay": "wss://...",
"intents": [
{"type": "deploy_ledger", "...": "..."},
{"type": "upsert_registry", "...": "..."},
{"type": "ledger_register", "...": "..."}
]
}
For shell scripts, treat non-2xx HTTP codes as transport or application errors,
and also inspect the JSON status field because some logical failures are
expressed inside JSON payloads.
For non-admin callers, a credit attempt should be treated as a permission error, not as a balance top-up path.
Practical caveats
- Your original BDD scenario expected
204from/api/nwc/mint, but the handler shown here replies with200and a JSON body containing the minted values. Then I print the responseonly makes sense when the endpoint actually returns a body. The current mint handler does./api/nwc/ledger/creditshould only appear in admin scenarios.- Client self-service top-up should be modeled as a separate endpoint and settlement flow.
Source basis
This manual was derived from the current damage_nwc_http handler, the shared
Damage auth flow in damage_http, and the account auth endpoint in
damage_accounts.
