From 0933c7c1e65f5aef9962bdd722a08f37c7cd92ba Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 29 Jul 2026 01:34:40 -0400 Subject: [PATCH] nsats for manage and debit --- specs/clink-debits.md | 68 +++++++++++++++++++++++++++++++++++++++---- specs/clink-manage.md | 59 +++++++++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/specs/clink-debits.md b/specs/clink-debits.md index 9fdf640..11c4b04 100644 --- a/specs/clink-debits.md +++ b/specs/clink-debits.md @@ -157,10 +157,20 @@ Sent to the node service identified by the key and pointer. } ``` +3. **Info Request:** + ```json + { + "info": true, + "pointer": "" // Optional, from ndebit TLV 2 + } + ``` + Queries the current debit relationship for this requestor (spend allowance / policy). See [Debit Info (`info: true`)](#debit-info-info-true) below. Like `bolt11` or `frequency`, this is a request-shape flag — not a manage-style `action`. + **Notes on Request Payload:** - The node service MAY require `amount_sats` even for direct payments to process rules without decoding the invoice, but MUST verify the invoice amount upon payment. - For budget requests, omitting `frequency` implies a one-time budget. -- A request with no `bolt11`, no `amount_sats`, and no `frequency` is implicitly a request for unrestricted access linked to the `pointer` (or the node service pubkey if `pointer` is absent), subject to node service policy and user approval. +- A request with no `info`, no `bolt11`, no `amount_sats`, and no `frequency` is implicitly a request for unrestricted access linked to the `pointer` (or the node service pubkey if `pointer` is absent), subject to node service policy and user approval. +- A request with `info: true` is an info query only. It MUST NOT be interpreted as a payment, budget, or unrestricted-access request. If `info` is present it MUST be `true`; other values are invalid (GFY code `6`). **Session Identifiers (`k1`):** @@ -180,8 +190,9 @@ Sent by the node service upon completing or rejecting a debit request. The kind 1. **ACK Payment Success:** Upon successful payment of a direct debit request, the node service sends a success response. The event itself, being signed by the node service and referencing the original request via an `e` tag, serves as a verifiable acknowledgment. The payload distinguishes between a standard Lightning payment and an internal settlement. - - For a **standard Lightning payment**, the NIP-44 encrypted `content` MUST be: `{"res":"ok","preimage":""}`. - - For an **internal settlement**, the NIP-44 encrypted `content` MUST be: `{"res":"ok"}`. The absence of a preimage indicates an internal transaction. + - For a **standard Lightning payment**, the NIP-44 encrypted `content` MUST include `"res":"ok"` and `"preimage":""`. + - For an **internal settlement**, the NIP-44 encrypted `content` MUST include `"res":"ok"` and MUST omit `preimage`. The absence of a preimage indicates an internal transaction. + - The node service SHOULD also include `policy` and, when applicable, `available_sats` as defined for info responses, as a refresh hint after spend. Clients that need a current allowance MUST still be able to poll via `info: true`; do not rely on payment responses alone. The overall event structure is the same for both cases, only the encrypted `content` differs: ```json @@ -209,7 +220,13 @@ Sent by the node service upon completing or rejecting a debit request. The kind } ``` -3. **GFY (General Failure to Yield) Response:** +3. **ACK (Info Success):** + Response to an `info: true` request. See [Debit Info (`info: true`)](#debit-info-info-true). Example encrypted payload: + ```json + {"res":"ok","policy":"budget","available_sats":42123} + ``` + +4. **GFY (General Failure to Yield) Response:** ```json { // ... similar structure ... @@ -218,6 +235,44 @@ Sent by the node service upon completing or rejecting a debit request. The kind } ``` +## Debit Info (`info: true`) + +CLINK Debits are an authorization overlay, not a wallet account API. A debit connection does not necessarily have a balance of its own: the node service may require interactive (manual) approval per payment, grant a rolling auto-approval budget, or grant unrestricted auto-approval against underlying spendable funds. + +`info: true` asks: **how much may this requestor spend right now under this debit relationship?** It does **not** expose a full account ledger balance. + +### Response fields + +Success payloads MUST include `"res":"ok"` and `"policy"`. + +| Field | Required | Description | +|-------|----------|-------------| +| `policy` | MUST | One of `"manual"`, `"budget"`, `"unrestricted"` | +| `available_sats` | when quantitative auto-spend is known | Non-negative integer sats this requestor can spend **now** under auto-approval | + +**`policy` meanings:** + +- `"manual"`: No lasting auto-approval for this requestor (unknown, or only one-shot approvals so far). The node service MUST omit `available_sats`. Clients MUST NOT invent `0` or treat a missing `available_sats` as an empty wallet; payments may still succeed after interactive approval. +- `"budget"`: Auto-approval is capped by a frequency budget. `available_sats` MUST be the remaining amount in the current window (including any fee policy the service applies when enforcing that budget). +- `"unrestricted"`: Auto-approval with no budget cap. `available_sats` MUST be the amount the service will auto-pay for this requestor now (typically underlying spendable funds, when the service exposes that). + +### Relationship mapping + +Debit relationships are not always lasting grants. A node service may approve a single payment without establishing ongoing auto-approval, grant budgeted or unrestricted auto-approval, or deny/ban a requestor. + +| Relationship | Info response | +|--------------|---------------| +| No lasting auto-approval | `{ "res": "ok", "policy": "manual" }` | +| Requestor denied / banned | GFY code `1` | +| Auto-approval with a frequency budget | `{ "res": "ok", "policy": "budget", "available_sats": }` | +| Auto-approval with no budget cap | `{ "res": "ok", "policy": "unrestricted", "available_sats": }` | + +### Info handling rules + +- Info MUST NOT prompt the user or otherwise initiate an authorization / approval flow (unlike direct payment, budget, or unrestricted-access requests). +- Info does **not** require a prior lasting grant; absence of lasting auto-approval is reported as `policy: "manual"`. +- Rolling budgets MAY change `available_sats` over time even without new payments. Clients that care about freshness SHOULD poll `info: true` before a spend when the last known `available_sats` is below the intended amount, and SHOULD NOT rely solely on values piggybacked on prior payment responses (multi-client use can also stale cached values). + ## GFY (General Failure to Yield) Handling When a request cannot be fulfilled, the node service MAY respond with a GFY error code. @@ -301,8 +356,9 @@ Implementations MUST include this tag in both request and response events and SH * It authenticates the request (e.g., checks if the app pubkey is known/allowed). * It evaluates the request against user rules or prompts the user for approval. 5. **Response**: Node service sends a kind `21002` response event to the requestor pubkey. - * **Success (Direct Payment)**: Includes `{"res":"ok", "preimage":"..."}`. + * **Success (Direct Payment)**: Includes `{"res":"ok", "preimage":"..."}` (and SHOULD include `policy` / `available_sats` when meaningful). * **Success (Budget Approval)**: Includes `{"res":"ok"}`. + * **Success (Info)**: Includes `{"res":"ok", "policy":"..."}` and `available_sats` when applicable. * **Failure**: Includes `{"res":"GFY", ...}`. 6. **Application Handling**: Application receives and processes the response. @@ -335,6 +391,8 @@ Implementations MUST include this tag in both request and response events and SH - Implement robust budget tracking (amount, frequency resets). - Consider adding fee reserves to budgets based on policy. - Implement automatic approval/denial based on user-defined rules (e.g., allow app X up to Y sats per month). +- Answer `info: true` without initiating user approval prompts; report `policy` and `available_sats` as specified. +- Include `policy` / `available_sats` on successful payment responses when meaningful. ### Wallet Client (UI) diff --git a/specs/clink-manage.md b/specs/clink-manage.md index 146b0d7..722c182 100644 --- a/specs/clink-manage.md +++ b/specs/clink-manage.md @@ -79,6 +79,17 @@ Allows an app to create, update, and delete offers on the user's wallet server. } ``` The wallet server is responsible for filtering the list to only include offers created by the requesting app. Future versions of this specification may introduce pagination and additional filtering capabilities. +- **Offer Stats** + ```json + { + "resource": "offer", + "action": "stats", + "offer": { + "id": "" + } + } + ``` + Returns aggregates for paid activity on this offer only (not account balance, not debit allowance). Authorization is the same as `get`: the requestor must be permitted to manage that offer. See success payload below. - **Delete Offer** ```json { @@ -115,6 +126,22 @@ The offer object is managed by the wallet server. It includes a unique `id` (gen ``` The `details` field contains an array of full offer objects. +- **Success (`stats`)** + ```json + { + "res": "ok", + "resource": "offer", + "details": { + "id": "", + "received_sats": 125000, + "paid_count": 42 + } + } + ``` + - `received_sats` (MUST): Sum of paid amounts (sats) received via this offer. + - `paid_count` (MUST): Number of paid invoices for this offer (allows clients to detect new payments without a history dump). + - Responses MUST NOT include per-invoice or payment-history arrays. Clients that need invoice history SHOULD use wallet-native interfaces outside CLINK Manage. + - **Success (`delete`)** ```json { "res": "ok", "resource": "offer" } @@ -185,22 +212,41 @@ When a request cannot be fulfilled, the wallet service MAY respond with a GFY er ``` #### Authorization & Ownership -- The wallet server MUST track which app created each offer and MUST reject modification or deletion requests from other apps unless explicitly permitted by the user. +- The wallet server MUST record which app pubkey created each offer. +- A manage grant does **not** confer access to all of the user's offers. The authorized app MAY only `list`, `get`, `update`, `delete`, and `stats` offers **it created** (bound to that app pubkey). It MUST NOT see or mutate offers created by other apps or by the user outside this grant. - Offer IDs MUST be unique per wallet server. The wallet server is responsible for enforcing uniqueness. #### Updatable Fields - Only existing fields on an offer may be included in the `fields` object for an update. The server MUST NOT add new fields to an offer via this action. #### Authorization Flow -- User shares their `nmanage1...` pointer with the app. -- App sends a Kind 21003 request to the wallet server. -- Wallet server prompts user for approval (or applies rules). -- On approval, the server creates/updates/deletes the offer and responds. + +CLINK Manage has **no separate access-request action**. Unlike [CLINK Debits](clink-debits.md) (which use budget / unrestricted-access request payloads to establish spend permission), manage grants are established by the first resource action from an app. + +There is also no spend permission in manage: approval whitelists the app to create offers and to manage **only those offers it creates** — not to debit funds, and not to administer the user's other offers. + +1. User shares their `nmanage1...` pointer with the app. +2. App sends a kind `21003` request with a normal resource action (commonly `create` or `list`). +3. If the app pubkey is not yet authorized for that pointer, the wallet server prompts the user for approval (or applies rules) — this first action **is** the access request. +4. On approval, the wallet server remembers the grant, performs the pending action, and responds. +5. Later actions from the same authorized app proceed without re-prompting (until the user revokes or bans the grant), still scoped to offers created by that app. +6. On denial / ban, the wallet server responds with GFY code `1`. + +Apps that only need to discover offers they already created can use `list` as the first action so approval is not tied to creating a new offer. #### Security & Rules - All requests are signed and auditable. - Wallet server can enforce rules (e.g., only allow certain apps, require user approval, limit offer creation rate, etc.). -- Apps should not be able to modify or delete offers they did not create, unless explicitly permitted. +- Apps MUST NOT be able to `get`, `update`, `delete`, or `stats` offers they did not create, unless the user explicitly permits otherwise (out of scope for the default grant model). + +#### Migration guidance (wallet / node) + +Creator-scoped grants mean a new marketplace app cannot manage offers another app created. That is intentional. When a user moves catalogs between apps, prefer **wallet-native reassignment** over widening `list` scope: + +- Wallet UIs SHOULD present offers grouped by managing app pubkey (the key bound at create time). +- The user SHOULD be able to move selected offers (or a whole group) to another authorized manage app, rebinding management to that app’s pubkey. +- After reassignment, the destination app’s `list` / `get` / `update` / `delete` / `stats` behave normally; the previous app loses access to those offers. +- Do **not** use an unscoped `list` (“all user offers”) as the migration mechanism — that weakens isolation between apps. ## Extensibility @@ -226,6 +272,7 @@ Implementations MUST include this tag in both request and response events and SH - `delete`: This action is idempotent. A client sending multiple `delete` requests for the same `id` will result in the same final state (the offer not existing). If the offer `id` already does not exist, the server SHOULD return a success (`res: "ok"`) response. - `get`: This action is idempotent. If the offer `id` does not exist, return a GFY `6: Invalid Request` error. - `list`: This action is idempotent. + - `stats`: This action is idempotent. If the offer `id` does not exist, return a GFY `6: Invalid Request` error. Aggregates reflect paid activity at response time and MAY change as new payments settle. - **Request Expiration**: To prevent replay attacks, wallet servers MUST enforce a maximum time delta between the server's clock and the event's `created_at` timestamp. It is recommended to follow the pattern in CLINK Debits by returning a GFY `3: Expired Request` error for events outside this delta (e.g., > 30 seconds, a standard used by existing implementations).