Skip to content
Draft
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
68 changes: 63 additions & 5 deletions specs/clink-debits.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,20 @@ Sent to the node service identified by the key and pointer.
}
```

3. **Info Request:**
```json
{
"info": true,
"pointer": "<pointer_id>" // 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`):**

Expand All @@ -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":"<lightning_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":"<lightning_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
Expand Down Expand Up @@ -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 ...
Expand All @@ -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": <n> }` |
| Auto-approval with no budget cap | `{ "res": "ok", "policy": "unrestricted", "available_sats": <n> }` |

### 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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Expand Down
59 changes: 53 additions & 6 deletions specs/clink-manage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<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
{
Expand Down Expand Up @@ -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": "<offer_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" }
Expand Down Expand Up @@ -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

Expand All @@ -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).

Expand Down