{"openapi":"3.1.0","info":{"title":"bitSimp API","summary":"Anonymous P2P Bitcoin content marketplace — Lightning & Nostr APIs","description":"bitSimp is a Bitcoin-only anonymous peer-to-peer content marketplace. Creators post content behind a price wall denominated in satoshis; buyers pay with the Lightning Network.\n\nThis document describes the public HTTP API of bitsimp.com.\n\n# Authentication\n\nbitSimp is **passwordless**. Authentication is performed by proving ownership of a secp256k1 key pair, using one of two methods:\n\n1. **LNURL-auth (Lightning)** — ECDSA over secp256k1, DER-encoded signatures, 33-byte compressed public keys. See `/api/auth/lightning/lnurl-auth/*`.\n2. **NIP-07 (Nostr)** — BIP-340 Schnorr signatures over secp256k1 (NIP-01/NIP-42), 32-byte x-only public keys. See `/api/auth/nostr/nip-07/*`.\n\nBoth methods share the same challenge flow:\n\n1. `GET .../challenge` — obtain a fresh random 64-char hex challenge (5-minute TTL, single-use).\n2. Sign the challenge with your private key (LNURL: ECDSA DER; Nostr: Schnorr event with the challenge in `content` or a `[\"challenge\", …]` tag).\n3. `POST .../login` — one call that verifies the signature, upserts your account, and **sets an HttpOnly session cookie** (`[__Secure-]authjs.session-token`) in the response.\n\nAlternatively use the two-phase flow: `POST .../verify` then exchange the resulting `k1` through NextAuth sign-in (`/api/auth/callback/lightning`).\n\nFor headless clients, two pitfalls to avoid: the `lnurl` field in `/challenge` responses is only for interactive wallet QR flows — sign the hex `challenge` as-is and never bech32-decode the LNURL; and a `success: true` from `GET .../validate` only verifies the signature, it does **not** create a session — only `/login` (or the NextAuth sign-in) sets the session cookie.\n\n# Sessions\n\nAuthenticated endpoints read the session cookie `authjs.session-token` (prefixed `__Secure-` on HTTPS). Sessions last 30 days. The user identity is the **account id** (`session.user.name`), returned as `id` in responses.\n\n# Payments\n\nAll monetary amounts are integer **satoshis**. Purchases/tips/deposits are Lightning invoices created via OpenNode; see the authenticated API flows described in this document.\n\n# Errors\n\nMost endpoints return `{ \"error\": string }` (or `{ \"success\": false, \"error\": string }` for auth endpoints) with an appropriate 4xx/5xx status. Error messages are intentionally generic.\n","termsOfService":"https://bitsimp.com/about","contact":{"name":"bitSimp Support","url":"https://bitsimp.com/about"},"version":"1.0.0","license":{"name":"bitSimp","url":"https://bitsimp.com/about"}},"servers":[{"url":"https://bitsimp.com","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"security":[],"paths":{"/api/auth/lightning/lnurl-auth/challenge":{"get":{"tags":["Authentication — LNURL-auth (Lightning)"],"summary":"Issue an LNURL-auth challenge","description":"Generates a fresh random 32-byte hex k1 challenge and its corresponding LNURL string (`lightning:LNURL1...`, for QR-code scanning with any LNURL-auth compatible wallet). The challenge is storable in Redis for 5 minutes and is single-use.\n\nThe LNURL embeds the callback `GET /api/auth/lightning/lnurl-auth/validate?k1={k1}&tag=login`, which wallets call after the user approves the login.\n\nHeadless clients (CLI tools, scripts, LLM agents): use the plain hex `challenge` field directly as the message to sign and POST it to `/api/auth/lightning/lnurl-auth/login`. Do **not** bech32-decode the `lnurl` string — that indirection is only needed for interactive wallet QR flows (and decoding adds a checksum-failure risk that does not apply to the hex field).","operationId":"lnurlAuthChallenge","responses":{"200":{"description":"Challenge issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LnurlChallengeResponse"}}}}}}},"/api/auth/lightning/lnurl-auth/validate":{"get":{"tags":["Authentication — LNURL-auth (Lightning)"],"summary":"LNURL-auth wallet callback (LUD-04)","description":"Wallet-facing callback per LUD-04. Lightning wallets call this URL (embedded in the LNURL) after the user approves the login request, presenting the k1, the ECDSA DER signature and the compressed public key.\n\nOn success the pubkey is stored in Redis under `k1:{k1}` (10-minute TTL) so the browser polling loop can complete the NextAuth sign-in. Unlike `/verify`, this endpoint does not require a pre-issued Redis challenge — the k1 travels inside the LNURL itself.\n\n**This endpoint does NOT create a session** and sets no cookie: a `success: true` response only means the signature is valid and the pubkey is staged for the browser sign-in flow. Headless/agent clients must call `POST /api/auth/lightning/lnurl-auth/login` (or the two-phase `/verify` + NextAuth sign-in) to obtain a session cookie.","operationId":"lnurlAuthValidate","parameters":[{"name":"k1","in":"query","required":true,"description":"The 64-char hex k1 challenge (message that was signed).","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"key","in":"query","required":true,"description":"33-byte compressed secp256k1 public key as 66-char hex.","schema":{"type":"string","example":"03…"}},{"name":"sig","in":"query","required":true,"description":"DER-encoded ECDSA signature over the raw 32-byte k1, as hex.","schema":{"type":"string"}},{"name":"tag","in":"query","required":false,"description":"Fixed value `login`; required by LNURL-auth protocols.","schema":{"type":"string","enum":["login"]}}],"responses":{"200":{"description":"Signature valid — pubkey stored for sign-in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LnurlValidateResponse"}}}},"400":{"description":"Missing/invalid params or signature verification failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}}}}},"/api/auth/lightning/lnurl-auth/verify":{"post":{"tags":["Authentication — LNURL-auth (Lightning)"],"summary":"Verify an LNURL-auth signature (two-phase flow)","description":"API-facing equivalent of a wallet calling `/validate`: verify a local ECDSA DER signature over the challenge without a wallet. Requires a challenge previously issued by `/challenge`.\n\nOn success the challenge is consumed (deleted) and the pubkey is stored under `k1:{challenge}` (10-minute TTL), ready for the NextAuth sign-in (`POST /api/auth/callback/lightning`).","operationId":"lnurlAuthVerify","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LnurlVerifyRequest"}}}},"responses":{"200":{"description":"Signature verified — use `k1` to complete NextAuth sign-in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifySuccess"}}}},"400":{"description":"Missing params, unknown/expired challenge, or bad signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}}}}},"/api/auth/lightning/lnurl-auth/login":{"post":{"tags":["Authentication — LNURL-auth (Lightning)"],"summary":"LNURL-auth single-call login (headless / LLM agents)","description":"Single HTTP call for headless clients (CLI tools, scripts, LLM agents): verifies the ECDSA signature, upserts the account row (creates it on first login), and sets an encrypted HttpOnly session cookie in the response. Include the cookie in all subsequent requests.\n\nRequest body: `{ pubkey, challenge, sig }` — `pubkey` is the 33-byte compressed key; `challenge` must have been issued by `/challenge`; `sig` is the DER-encoded ECDSA signature over the raw 32-byte challenge.","operationId":"lnurlAuthLogin","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LnurlLoginRequest"}}}},"responses":{"200":{"description":"Authenticated — session cookie set","headers":{"Set-Cookie":{"description":"`[__Secure-]authjs.session-token=<encrypted JWT>; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000`","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginSuccess"}}}},"400":{"description":"Missing params, unknown/expired challenge, or bad signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"500":{"description":"Database failure or missing AUTH_SECRET","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}}}}},"/api/auth/nostr/nip-07/challenge":{"get":{"tags":["Authentication — NIP-07 (Nostr)"],"summary":"Issue a NIP-07 auth challenge","description":"Generates a fresh random 32-byte hex challenge for NIP-07 authentication and stores it in Redis for 5 minutes (single-use). The client embeds this challenge into a Nostr event (`content` or a `[\"challenge\", …]` tag) and signs it with the browser extension or a local Nostr key.","operationId":"nostrNip07Challenge","responses":{"200":{"description":"Challenge issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NostrChallengeResponse"}}}}}}},"/api/auth/nostr/nip-07/verify":{"post":{"tags":["Authentication — NIP-07 (Nostr)"],"summary":"Verify a signed Nostr event (NIP-01/NIP-42)","description":"Verifies a signed Nostr event (event ID = SHA-256 of serialized fields, BIP-340 Schnorr signature, and the challenge present in `content` or tags). On success the challenge is consumed and the event `pubkey` is stored under `k1:{challenge}` (10-minute TTL), ready for NextAuth sign-in.","operationId":"nostrNip07Verify","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NostrVerifyRequest"}}}},"responses":{"200":{"description":"Event verified — use `k1` to complete NextAuth sign-in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifySuccess"}}}},"400":{"description":"Missing params, unknown/expired challenge, or invalid event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}}}}},"/api/auth/nostr/nip-07/login":{"post":{"tags":["Authentication — NIP-07 (Nostr)"],"summary":"NIP-07 single-call login (headless / LLM agents)","description":"Single HTTP call for headless clients: verifies the Nostr event, checks `event.pubkey` matches the submitted `pubkey`, upserts the account row, and sets an encrypted HttpOnly session cookie in the response. Include the cookie in all subsequent requests.","operationId":"nostrNip07Login","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NostrLoginRequest"}}}},"responses":{"200":{"description":"Authenticated — session cookie set","headers":{"Set-Cookie":{"description":"`[__Secure-]authjs.session-token=<encrypted JWT>; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000`","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginSuccess"}}}},"400":{"description":"Missing params, unknown/expired challenge, or invalid event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"500":{"description":"Database failure or missing AUTH_SECRET","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}}}}},"/api/auth/session":{"get":{"tags":["Authentication — NextAuth","Sessions"],"summary":"Get current session","description":"NextAuth.js session endpoint. Returns the JWT-decoded session for the current `authjs.session-token` cookie, or `null` when unauthenticated.","operationId":"getSession","responses":{"200":{"description":"Session info (or null)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}}}}},"/api/auth/csrf":{"get":{"tags":["Authentication — NextAuth","Sessions"],"summary":"Get CSRF token","description":"NextAuth.js CSRF token endpoint — required before signing in via NextAuth flows.","operationId":"getCsrf","responses":{"200":{"description":"CSRF token object","content":{"application/json":{"schema":{"type":"object","properties":{"csrfToken":{"type":"string"}}}}}}}}},"/api/auth/signin":{"post":{"tags":["Authentication — NextAuth","Sessions"],"summary":"Sign in via NextAuth provider","description":"NextAuth sign-in endpoint. Used by the browser flow after a pubkey appears under `k1:{k1}` in Redis (via `/validate` or `/verify`). Body is form-encoded: `{ csrfToken, pubkey, k1, callbackUrl }`.","operationId":"nextAuthSignIn","responses":{"302":{"description":"Redirect to `callbackUrl` (session cookie set)"},"401":{"description":"k1 not found in Redis or pubkey mismatch"}}}},"/api/auth/signout":{"post":{"tags":["Authentication — NextAuth","Sessions"],"summary":"Sign out","description":"Clears the session cookie.","operationId":"nextAuthSignOut","responses":{"302":{"description":"Redirect to `callbackUrl` (session cookie cleared)"}}}},"/api/auth/callback/lightning":{"post":{"tags":["Authentication — NextAuth","Sessions"],"summary":"NextAuth lightning provider callback (two-phase flow)","description":"Completes sign-in after `/verify` when using the two-phase flow. Form-encoded: `{ csrfToken, pubkey, k1 }`. The NextAuth authorize callback reads `k1:{k1}` from Redis and creates sessions only when the stored pubkey matches.","operationId":"nextAuthCallbackLightning","responses":{"302":{"description":"Redirect to `callbackUrl` (session cookie set)"},"401":{"description":"k1 not found in Redis or pubkey mismatch"}}}},"/api/posts":{"get":{"tags":["Content"],"summary":"List posts (feed)","description":"Cursor-paginated feed of posts with optional filters. Anonymous browsing supported; the session (cookie) is used to compute per-post lock/unlock state and to include the caller's own archived posts.\n\nArchived posts are hidden from everyone except their owner. `isUnlocked` is always `true` when the post has no files, price is 0, the viewer owns the post, or the viewer has a `paid` purchase transaction.","operationId":"listPosts","parameters":[{"name":"author","in":"query","required":false,"description":"Filter by creator alias (resolved to the account id internally).","schema":{"type":"string"}},{"name":"price_min","in":"query","required":false,"description":"Minimum price in satoshis.","schema":{"type":"string"}},{"name":"price_max","in":"query","required":false,"description":"Maximum price in satoshis (0 = only free posts).","schema":{"type":"string"}},{"name":"includes_file_type","in":"query","required":false,"description":"Comma-separated bucket names: `images`, `videos`, `audio`, `other`, or `all`.","schema":{"type":"string","example":"images,videos"}},{"name":"purchased","in":"query","required":false,"description":"When `true`, only posts purchased by the authenticated user.","schema":{"type":"string","enum":["","true","false"]}},{"name":"unlocked","in":"query","required":false,"description":"When `true`, only posts already unlocked (free or purchased).","schema":{"type":"string","enum":["","true","false"]}},{"name":"locked","in":"query","required":false,"description":"When `true`, only locked/paywalled posts (not free, not purchased, not owned).","schema":{"type":"string","enum":["","true","false"]}},{"name":"cursor","in":"query","required":false,"description":"Created-at value of the last post from the previous page (cursor pagination).","schema":{"type":"string","example":"2026-08-01T12:00:00.000Z"}},{"name":"page_size","in":"query","required":false,"description":"Page size (default 5).","schema":{"type":"integer","minimum":1,"example":5}}],"responses":{"200":{"description":"Array of posts (files locked or unlocked per viewer state)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}}},"/api/posts/{postId}":{"get":{"tags":["Content"],"summary":"Get a single post","description":"Returns one post with access control. Locked posts (`isUnlocked=false`) return file entries with empty `objectName`/`data`/`blurDataURL` so content is never leaked; unlocked posts include full data. Archived posts return 404 unless the viewer is the owner.\n\nRequires a session only to see purchased/locked state: the underlying procedure is a `protectedProcedure` and returns 401 when no session cookie is present.","operationId":"getPost","security":[{"sessionCookie":[]}],"parameters":[{"name":"postId","in":"path","required":true,"description":"Post id (UUID).","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Post with files","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"401":{"description":"No valid session cookie","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Post not found (or archived and not yours)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/comments":{"get":{"tags":["Content"],"summary":"List comments for a post","description":"Offset-paginated comments for a post, newest first. Public read; posting comments is paid and handled by client/server actions with Lightning invoices.","operationId":"listComments","parameters":[{"name":"postId","in":"query","required":true,"description":"Post id to fetch comments for.","schema":{"type":"string","format":"uuid"}},{"name":"offset","in":"query","required":false,"description":"Number of comments to skip.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"limit","in":"query","required":false,"description":"Maximum number of comments to return (default 10).","schema":{"type":"integer","minimum":1,"default":10}}],"responses":{"200":{"description":"Array of comments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}}}}},"400":{"description":"Missing `postId`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me":{"get":{"tags":["Account"],"summary":"Current user account id, alias and balance","description":"Returns the authenticated user's account id (identity), npub, alias, and full available-balance breakdown. Requires a valid session cookie.\n\nBalance formula: `availableBalance = earnedAmount − routingFees − pwbSpendings − withdrawnAmount − withdrawalFees`.","operationId":"getMe","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"No valid session cookie","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/opennode/withdrawal":{"post":{"tags":["Webhooks"],"summary":"OpenNode withdrawal webhook (LNURL-withdraw)","description":"Webhook fired by OpenNode when a user completes an LNURL-withdraw. `multipart/form-data`. Integrity is enforced via HMAC-SHA256 of the transaction id using the OpenNode API key, compared with `hashed_order` using a timing-safe comparison. Requests without a valid hash are ignored.","operationId":"opennodeWithdrawalWebhook","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/WithdrawalWebhookPayload"}}}},"responses":{"200":{"description":"Webhook acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["OK"]},"success":{"type":"boolean"}},"required":["status","success"]}}}}}}},"/api/video":{"get":{"tags":["Video"],"summary":"next-video asset handler","description":"Library-managed endpoint owned by `next-video` — serves signed video asset metadata and handles Mux upload callbacks. Not part of the app's public business API.","operationId":"videoGet","responses":{"200":{"description":"Asset metadata or playback response"},"404":{"description":"Unknown asset"}}},"post":{"tags":["Video"],"summary":"next-video upload callback","description":"Library-managed upload/processing callback for video assets.","operationId":"videoPost","responses":{"200":{"description":"Callback acknowledged"}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"authjs.session-token","description":"Encrypted NextAuth JWT session cookie set by the `.../login` endpoints (or browser sign-in). On HTTPS the actual cookie name is `__Secure-authjs.session-token`. Send the cookie exactly as received."}},"schemas":{"LnurlChallengeResponse":{"type":"object","title":"LNURL challenge response","properties":{"challenge":{"type":"string","description":"64-char hex k1. Headless clients must hex-decode this value to the raw 32-byte message before signing it (LNURL-auth uses a DER-encoded ECDSA signature over those bytes, not over the ASCII hex string), then POST the hex `challenge` plus signature to `/api/auth/lightning/lnurl-auth/login`.","pattern":"^[0-9a-f]{64}$"},"lnurl":{"type":"string","description":"LNURL URI string for QR-code/wallet scanning: it starts with the `lightning:` scheme prefix and contains a bech32 LNURL payload after that prefix. Interactive wallets only — headless clients should ignore this field entirely and never try to bech32-decode the full URI string.","example":"lightning:LNURL1DP68GURN8GHJ7CNFW3EKJMTS9E3K7MF0V9CXJTMPW46XSTMVD9NKSARWD9HXWTMVDE6HYMPDV96HG6P0WESKC6TYV96X20MTXY7KZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPV9SKZCTPVYN8GCT884KX7EMFDCURSC59"}},"required":["challenge","lnurl"]},"NostrChallengeResponse":{"type":"object","title":"NIP-07 challenge response","properties":{"challenge":{"type":"string","description":"64-char hex challenge to embed in a Nostr event (content or tag).","pattern":"^[0-9a-f]{64}$"}},"required":["challenge"]},"LnurlVerifyRequest":{"type":"object","title":"LNURL signature verification request","properties":{"challenge":{"type":"string","pattern":"^[0-9a-f]{64}$"},"pubkey":{"type":"string","description":"33-byte compressed secp256k1 public key as 66-char hex."},"sig":{"type":"string","description":"DER-encoded ECDSA signature over the raw 32-byte challenge, as hex."}},"required":["challenge","pubkey","sig"]},"LnurlLoginRequest":{"allOf":[{"$ref":"#/components/schemas/LnurlVerifyRequest"}],"title":"LNURL single-call login request"},"NostrEvent":{"type":"object","title":"Nostr event (NIP-01)","description":"Standard Nostr event. To authenticate, `content` must equal the challenge, or `tags` must include `[\"challenge\", <challenge>]`. The event `id` is SHA-256 of `[0, pubkey, created_at, kind, tags, content]`, signed with BIP-340 Schnorr.","properties":{"id":{"type":"string","description":"32-byte event id as hex (SHA-256 of the serialized event)."},"pubkey":{"type":"string","description":"32-byte x-only public key as 64-char hex."},"created_at":{"type":"integer","description":"Unix timestamp in seconds.","example":1750000000},"kind":{"type":"integer","description":"Event kind (1 or 22242/NIP-42 style are accepted)."},"tags":{"type":"array","items":{"type":"array","items":{"type":"string"}},"example":[["challenge","0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"]]},"content":{"type":"string","description":"Free text — must contain the challenge for auth."},"sig":{"type":"string","description":"64-byte BIP-340 Schnorr signature as hex."}},"required":["id","pubkey","created_at","kind","tags","content","sig"]},"NostrVerifyRequest":{"type":"object","title":"NIP-07 verify request","properties":{"challenge":{"type":"string","pattern":"^[0-9a-f]{64}$"},"event":{"$ref":"#/components/schemas/NostrEvent"}},"required":["challenge","event"]},"NostrLoginRequest":{"type":"object","title":"NIP-07 single-call login request","properties":{"pubkey":{"type":"string","description":"32-byte x-only public key as 64-char hex; must match `event.pubkey`."},"challenge":{"type":"string","pattern":"^[0-9a-f]{64}$"},"event":{"$ref":"#/components/schemas/NostrEvent"}},"required":["pubkey","challenge","event"]},"VerifySuccess":{"type":"object","title":"Verification success (two-phase flow)","properties":{"success":{"type":"boolean","const":true},"k1":{"type":"string","description":"The challenge value — exchange it via NextAuth sign-in (`/api/auth/callback/lightning`)."}},"required":["success","k1"]},"LoginSuccess":{"type":"object","title":"Login success (single-call flow)","properties":{"success":{"type":"boolean","const":true},"id":{"type":"string","format":"uuid","description":"The authenticated user's account id (identity)."}},"required":["success","id"]},"LnurlValidateResponse":{"type":"object","title":"LNURL validate callback response","properties":{"status":{"type":"string","const":"OK"},"success":{"type":"boolean","const":true},"k1":{"type":"string"}},"required":["status","success","k1"]},"AuthError":{"type":"object","title":"Auth error","properties":{"success":{"type":"boolean","const":false},"error":{"type":"string","description":"Generic error message."}},"required":["success","error"]},"ErrorResponse":{"type":"object","title":"Generic error","properties":{"error":{"type":"string"}},"required":["error"]},"Session":{"type":"object","description":"NextAuth session or `null` when unauthenticated.","properties":{"user":{"type":"object","properties":{"name":{"type":"string","description":"The user's account id."}}},"expires":{"type":"string","format":"date-time"}}},"Post":{"type":"object","title":"Post","properties":{"id":{"type":"string","format":"uuid"},"created_by":{"type":"string","description":"Creator account id."},"created_at":{"type":"string","format":"date-time"},"title":{"type":"string"},"price":{"type":"integer","description":"Price in satoshis."},"is_archived":{"type":"boolean"},"archived_by":{"type":["string","null"],"description":"Admin account id if archived by an admin."},"body":{"type":["string","null"]},"isUnlocked":{"type":"boolean","description":"true when price=0, viewer owns it, it has no files, or a paid purchase exists."},"isOwner":{"type":"boolean"},"author":{"type":"string","description":"Author display name (alias > npub > id); npub and id values are trimmed `first5…last5`, so use `authorSlug` for links."},"authorSlug":{"type":"string","description":"Untrimmed canonical profile URL segment (alias > npub > id). Build profile links as `/{encodeURIComponent(authorSlug)}`."},"files":{"type":"array","description":"Locked posts return entries with empty `objectName`/`data`/`blurDataURL` so no content leaks.","items":{"$ref":"#/components/schemas/PostFile"}}},"required":["id","created_by","created_at","title","price","is_archived","archived_by","body","isUnlocked","isOwner","author","authorSlug","files"]},"PostFile":{"type":"object","title":"Post file","properties":{"bucketName":{"type":"string","enum":["images","videos","audio","other"]},"objectName":{"type":"string","description":"Empty when locked."},"data":{"type":"string","description":"Signed content URL; empty when locked."},"blurDataURL":{"type":"string","description":"Plaiceholder blur preview; empty when locked."},"public":{"type":"boolean"},"mux_asset_id":{"type":["string","null"]},"playbackId":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":true},"downloadUrl":{"type":"string"}}},"Comment":{"type":"object","title":"Comment","properties":{"id":{"type":"string","format":"uuid"},"body":{"type":"string"},"post_id":{"type":"string","format":"uuid"},"created_by":{"type":"string","description":"Commenter account id."},"created_at":{"type":"string","format":"date-time"},"reply_for":{"type":["string","null"]},"transaction_id":{"type":"string","description":"Paid Lightning transaction id."},"accountAlias":{"type":"string","description":"Commenter display name (alias > npub > id); npub and id values are trimmed `first5…last5`, so use `accountSlug` for links."},"accountSlug":{"type":"string","description":"Untrimmed canonical profile URL segment (alias > npub > id). Build commenter links as `/{encodeURIComponent(accountSlug)}`."}},"required":["id","body","post_id","created_by","created_at","reply_for","transaction_id","accountAlias","accountSlug"]},"AvailableBalance":{"type":"object","title":"Available balance breakdown","properties":{"earnedAmount":{"type":"integer","description":"Total sats earned from sales/tips/deposits."},"routingFees":{"type":"integer","description":"Total Lightning routing fees paid on earnings."},"pwbSpendings":{"type":"integer","description":"Total sats spent via pay-with-balance."},"withdrawnAmount":{"type":"integer","description":"Total sats withdrawn to a Lightning wallet."},"withdrawalFees":{"type":"integer","description":"Total withdrawal fees."},"availableBalance":{"type":"integer","description":"Net balance."}},"required":["earnedAmount","routingFees","pwbSpendings","withdrawnAmount","withdrawalFees","availableBalance"]},"MeResponse":{"type":"object","title":"Me response","properties":{"id":{"type":"string","format":"uuid","description":"User's account id (= session.user.name)."},"npub":{"type":["string","null"],"description":"User's Nostr key in NIP-19 npub1 form, when present."},"alias":{"type":["string","null"],"description":"Human-readable alias, when set."},"availableBalance":{"$ref":"#/components/schemas/AvailableBalance"}},"required":["id","npub","alias","availableBalance"]},"WithdrawalWebhookPayload":{"type":"object","title":"OpenNode withdrawal webhook payload (form-data)","properties":{"id":{"type":"string","description":"OpenNode transaction id."},"hashed_order":{"type":"string","description":"HMAC-SHA256(id, key=OPENNODE_API_KEY) — integrity proof."},"status":{"type":"string","description":"e.g. `confirmed`."},"amount":{"type":"string","description":"Withdrawn amount (satoshi units from OpenNode)."},"fee":{"type":"string","description":"Withdrawal fee."},"lnurl_withdrawal":{"type":"string","description":"JSON string: `{ \"id\": ..., \"external_id\": <user pubkey> }`."}},"required":["id","hashed_order","status","amount","fee","lnurl_withdrawal"]}}},"tags":[{"name":"Authentication — LNURL-auth (Lightning)","description":"Passwordless auth by proving ownership of a secp256k1 key pair (ECDSA, DER signatures, compressed 33-byte pubkeys). Marries the LNURL-auth protocol (LUD-04) with NextAuth sessions."},{"name":"Authentication — NIP-07 (Nostr)","description":"Passwordless auth with Nostr keys (BIP-340 Schnorr signatures, 32-byte x-only pubkeys, NIP-01/NIP-42 event formats)."},{"name":"Content","description":"Posts and comments — the marketplace content layer."},{"name":"Account","description":"The authenticated user's identity and balance."},{"name":"Webhooks","description":"External-service callbacks (OpenNode)."},{"name":"Video","description":"Library-managed video asset endpoints (next-video)."},{"name":"Sessions","description":"Standard NextAuth.js endpoints used by the browser sign-in flow."}]}