Skip to content
48 changes: 45 additions & 3 deletions crates/maxplayer-trade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ an active sat keyset, and clock skew at most 60 seconds. Preflight runs before e
`take`, funding quotes and lock submission; successful earlier preflight is not cached
as permission for a later lock. Test mints and loopback HTTP(S) fixtures remain supported.
HTTP is refused except on loopback, and redirects are disabled. There is no real-money
opt-in environment variable or allow-list flag. Each lock's gross amount and cumulative
funding per mint/home are capped at **100,000 sats**, including pending/lost funding intents.
opt-in environment variable or allow-list flag. Each lock's gross amount is capped at
**100,000 sats**. Funding intents plus charged receives (gross) share one 100,000-sat cap per mint per home, including pending/lost funding intents.
Each withdrawal invoice is also capped at **100,000 sats**, plus at most max(32 sats, ceil(invoice amount × 2%)) of
quoted fee reserve, bounded together with input fees by `--max-debit`; spendable funds must cover the invoice, input fees and required change.

Expand Down Expand Up @@ -624,13 +624,55 @@ A mint can still lie, withhold evidence, or process an already-delivered request
process-exit boundaries and one claim/refund race, not a full model checker.


## Receive (import a Cashu token)

`maxplayer-trade --home <home> receive <mint> --token-file <path>` (or the token on stdin;
never argv) imports one token, e.g. one written by `maxplayer-mint issue`. Code: `src/receive.rs`.

- Refused before any journal write: token mint not exactly `<mint>` after canonicalization,
multi-mint, non-sat, NUT-10 locked (P2PK/HTLC), > 128 or duplicate proofs, invalid incoming
DLEQ (when present), > 100,000 sats, or a receive that would exceed the shared cap below. Common preflight and NUT-07 UNSPENT are required before the journal write.
- One swap into fresh home-owned outputs; input proofs (with secrets), blinded outputs and
their secrets are journaled before the POST (the token text is not). A second output set
is never created. Ambiguity → NUT-09 restore of the same outputs and identical
replay only. A definitive NUT error with inputs UNSPENT is `refused`; SPENT inputs with nothing
of ours restorable is terminal `already_spent`. Only DLEQ-verified proofs are credited;
missing/invalid DLEQ on our outputs is terminal `quarantined` (exit 4).
- Cap: Funding intents plus charged receives (gross) share one 100,000-sat cap per mint per home; `refused`/`already_spent` receives do not count; `prepared`/`submitted`/`done`/`quarantined` ones do. A quarantined receive is not spendable but still occupies the cap.
- Exits: 0 `done` with its credited rows released; 1 pre-journal refusal, `refused` or
`already_spent`; 3 unresolved (`prepared`/`submitted`, `done` whose release is still
outstanding, or a journal write failed; run `recover`); 4 `quarantined`.
- Only a definitive NUT error (HTTP 400, JSON `code` + `detail`, code on the
`MintRefusal::DEFINITIVE` allowlist, a strict subset of cdk-common 0.17.2
`is_definitive_failure`: 11003 OutputsAlreadySigned and auth codes are deliberately left out)
can make a receive `refused`. `50000` (cdk's transient Internal/Database catch-all),
11002 TokenPending, 11003, 11004 OutputsPending, unknown codes, any other 400 and transport
refusals stay `submitted` and replay. Every absence check restores, checks NUT-07 and,
on SPENT, restores again before `already_spent`.
- `refused` is FINAL and journals its NUT code (`refusal_code`). Neither `recover` nor a
repeated `receive` replays it: against cdk 0.17.2 every allowlisted code is deterministic
for the same inputs and outputs (output-keyset codes 12001-12003, 11008, 11015, fee-based
11005 included), so no retryable code exists. The token was not imported, nothing is
charged to the cap, and it can be redeemed elsewhere. A record without a code (written
before this rule) is final too. `status`/`recover` lines carry `settled` and `refusal_code`.
- Credited proofs are stored RESERVED under the receive id and released only after `done`
is journaled, so `withdraw`/`send` cannot spend them earlier. Mint error detail text is
never printed; only the NUT code.
- Idempotent per Y-set (sha256 of the sorted proof Ys): repeats resume the existing attempt
and only ever replay its identical swap.
- The mint input fee is deducted; output reports `amount`, `fee`, `net`, `credited`, state and
attempt id only. `recover`/`serve` resume receives under the same 120 s item budget.
- `nostr://` mints: cdk 0.17.2 `MintUrl`/`Token` parse them (unit test
`cdk_token_parsing_accepts_nostr_mint_url`), but this crate's `Asset::new` admits only
http/https, so a nostr:// receive is refused until the nostr transport branch lands.

## Funding and withdrawals

This path is **not a production safety certification**. Public-mint results, if any, belong in
its separate run report; the historical test counts above predate this change.

- Each lock gross is capped at **100,000 sats**, at both planning and submission.
- Funding on every mint is capped at **100,000 sats cumulatively per mint per home**. The journal charges
- Funding on every mint is capped at **100,000 sats cumulatively per mint per home**: Funding intents plus charged receives (gross) share one 100,000-sat cap per mint per home; `refused`/`already_spent` receives do not count; `prepared`/`submitted`/`done`/`quarantined` ones do. The journal charges
an intent before creating the mint quote; pending, failed, issued and lost-reply intents
stay charged across restarts. A lost quote response is deliberately not auto-replaced.
- `fund` locks the quote to a private NUT-20 key and requires the mint to echo that
Expand Down
61 changes: 35 additions & 26 deletions crates/maxplayer-trade/skills/maxplayer-trade/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: maxplayer-trade
description: Trade Cashu tokens for a human with the standalone maxplayer-trade CLI, including real money. Use for “sell/list X of mint A for Y of mint B”, “buy that lot”, “find listings”, “fund my trade wallet”, “withdraw”, “check balance”, or “something got interrupted”.
description: Trade Cashu tokens for a human with the standalone maxplayer-trade CLI, including real money. Use for “sell/list X of mint A for Y of mint B”, “buy that lot”, “find listings”, “fund my trade wallet”, “receive this token”, “withdraw”, “check balance”, or “something got interrupted”.
---

# Trade Cashu for the human

> Round-five operating policy. Read the scoped evidence and
> verification limits in [verification](references/verification.md).
> Round-five operating policy. Scoped evidence and limits:
> [verification](references/verification.md).

## 1. Establish custody and scope

Expand Down Expand Up @@ -34,15 +34,17 @@ Use `balance <mint>` and `preflight <mint>` for each relevant mint. Passing pref
allows a mint, but does not establish honesty or complete compatibility. The CLI
also preflights before fund/list/take/withdraw: NUT-07/09/11/12/14, an active sat keyset, reachable
mint, and clock skew at most 60 seconds. Real mints need no opt-in setting. Never
bypass a failed check. Gross locks and cumulative funding per mint per home are
capped at **100,000 sats**, as is each withdrawal invoice. The withdrawal Lightning
bypass a failed check. Gross locks and each withdrawal invoice are capped at
**100,000 sats**. Funding intents plus charged receives (gross) share one
100,000-sat cap per mint per home; `refused`/`already_spent` receives do not
count; `prepared`/`submitted`/`done`/`quarantined` ones do. The withdrawal Lightning
fee reserve ceiling is **max(32 sats, 2% of invoice sats rounded up)**. Pass
`--max-debit` to cap invoice amount + Cashu input fee + Lightning reserve. Fees can make an otherwise
eligible net lock amount too large.

**Before every real-money `fund`, `list`, `take`, or `withdraw`, present the exact
operation and get an explicit yes.** A broad “trade for me”, previous trade approval,
or a published listing is not approval for a new operation. State:
operation and get an explicit yes.** A broad “trade for me”, earlier approval
or a published listing does not approve a new operation. State:

- Both mint URLs and direction (or the single funding/withdrawal mint and destination).
- Exact net amounts, maximum total debit, minimum receipt, and fee cap in sats.
Expand All @@ -53,15 +55,13 @@ or a published listing is not approval for a new operation. State:
have their own unresolved-payment risk, not a guaranteed 75-minute timeout.
- A mint can steal its tokens. If the counterparty vanishes, funds stay locked until
the refund deadline and successful recovery. `serve` must stay running while locks are live (the active `take` also watches its trade).
NUT-14 claims remain valid after locktime. Nutshell refund status: round 4 reports a **successful cashu.cz Nutshell/0.21.0
refund**; see the version-specific limits in [verification](references/verification.md). NUT-07 advertisement alone does not prove witness emission. The maker relies on
NUT-14 claims remain valid after locktime. Nutshell refund status and limits: [verification](references/verification.md). NUT-07 advertisement alone does not prove witness emission. The maker relies on
the chosen mint reporting the HTLC witness if the taker withholds its notice.
Mint choice is the buyer’s/seller’s responsibility; there is no rating or verification gate.
Mint choice is the trader's responsibility; there is no rating or verification gate.

Use the human's price and fee budget, never more permissive limits. If fees are
Use the human's price and fee budget, never looser limits. If fees are
unspecified, propose a numeric cap and wait for yes; the CLI default is not consent.
If a command cannot enforce the approved bounds, **do not run it**: explain the
missing enforcement and escalate. Do not substitute a guessed flag or a larger cap.
If a command cannot enforce the approved bounds, **do not run it**; escalate. Never guess a flag or raise a cap.
**Done:** an explicit yes binds this exact action, amounts, limits, and risks.

## 3. Execute the selected workflow
Expand All @@ -71,20 +71,30 @@ missing enforcement and escalate. Do not substitute a guessed flag or a larger c
After confirmation, `fund <mint> --amount <sats>`. On a real mint it prints a BOLT11
invoice and never auto-pays. Give that invoice privately to the human to pay from
**their own wallet**; the agent must never pay without separate explicit consent.
Do not treat printing an invoice as funded balance. Preserve the quote ID; after
An invoice is not funded balance. Preserve the quote ID; after
interruption use `recover`, and when needed resume that same funding quote with
`fund <mint> --amount <same-sats> --quote <quote-id>` after confirmation. Never create
or pay a replacement merely because a reply or issuance timed out.
**Done:** issuance is reconciled and `balance <mint>` confirms the result, or the
retained quote is reported as unresolved without another payment.
or pay a replacement because a reply or issuance timed out.
**Done:** issuance reconciled and `balance <mint>` confirms it, or the retained
quote is reported unresolved without another payment.

### Receive a token

Needs the human's yes (mint URL, amount); charges the shared cap (§2).
Save the token to a private file; run `receive <mint> --token-file <file>` (or
stdin). Never put a token in argv/chat/logs. Only plain sat proofs of that
mint; fee deducted. Exit 0 `done`; 1 `refused` (final: not imported;
redeem elsewhere) or `already_spent`; 3 unresolved/release pending:
`recover`, never re-import; 4 `quarantined`:
unspendable, still on the cap; escalate. **Done:** `done` and `balance <mint>`.

### Sell/list X of mint A for Y of mint B

After confirmation, `list --give-mint <A> --give <X> --want-mint <B> --want <Y>
--max-fees <cap>`. X and Y are net lot amounts, not interchangeable “bitcoin” assets.
The seller's maximum debit is X plus the approved outgoing fee cap. Record the lot
ID and keep `serve` running until complete or refunded. Listing authorizes serving
that fixed lot; it does not authorize further listings or price changes.
that fixed lot, not further listings or price changes.

`cancel <lot>` is allowed only with **no active quote/swap**. Cancellation never
revokes an authorized HTLC lock. On rejection, continue recovery; do not force-release
Expand All @@ -109,8 +119,7 @@ a timeout. **Done:** verify the terminal result and per-mint balances, or enter
Use only an invoice the human provides. Validate amount, destination, expiry, and
fee/debit bounds before confirmation. `withdraw <mint> --invoice <bolt11> --max-debit <approved-sats>` may spend
immediately; it is **not** a quote preview. Read the withdrawal limitations in the
command reference first. Non-terminal means payment and/or change remain unresolved,
not that payment failed. Never create a replacement invoice, withdrawal, or payment
command reference first. Non-terminal means unresolved, not failed. Never create a replacement invoice, withdrawal, or payment
while a submitted payment is unresolved. A pre-POST refusal cannot pay later via
recovery; only explicit same-invoice withdrawal can authorize a new attempt.
`quote_created` is never auto-submitted. A timeout after POST is not a failed payment. **Done:** `done` and reconciled change/balance are confirmed,
Expand All @@ -122,25 +131,25 @@ or report the retained authorization and recover it without paying again.
reservation is spendable. After **any interruption**, use `recover` with the same
home and relays before new work. It resumes existing authorizations and does not
admit new trade requests. It makes **one bounded pass**, at most **120 seconds per
funding/withdrawal/swap item**, without waiting for lock deadlines: exit **0** means
funding/withdrawal/receive/swap item**, without waiting for lock deadlines: exit **0** means
all terminal with no deferred work, **3** unresolved/deferred, **4** terminal manual recovery, **1** command error, pre-submission refusal, or definitive terminal `unpaid_released` (never proof a submitted withdrawal was refused; a submitted non-terminal withdrawal exits **3**),
**2** CLI usage error.
Timeout does not undo an RPC or release reservations. Restore `serve` while locks
Timeout neither undoes an RPC nor releases reservations. Restore `serve` while locks
are live; a single recovery pass is not a watcher. See the recovery reference for
quarantine and errors.
**Done:** classify every reported outcome using the recovery reference.

## 4. Verify and report

Check public swap/withdrawal state with `status` and balances with `balance`
without stopping the owner process. A zero exit, elapsed deadline, missing listing, or mint saying PAID is not
by itself reconciled success. Keep the home. Report spent/received amounts and fees
Check public state with `status` and balances with `balance` without stopping
the owner process. A zero exit, elapsed deadline, missing listing or PAID mint is not
reconciled success by itself. Keep the home. Report spent/received amounts and fees
only when known; distinguish complete, refunded, unresolved, and manual recovery.

On `refund_quarantined` or `claim_quarantined`, stop new money actions and escalate
to the human immediately. These are terminal **manual recovery**, not success or
spendable funds. Never credit/import unverifiable outputs, edit journal records,
replace attempts, or treat quarantine as refund permission. Persistent RPC failure,
missing witnesses, unexpected states, or accounting discrepancies also need a human;
missing witnesses, unexpected states or accounting gaps also need a human;
retain safe recovery for other obligations, without weakening any check.
**Done:** the human has an accurate outcome and any unresolved custody obligations.
Loading
Loading