Skip to content
Merged
12 changes: 10 additions & 2 deletions crates/maxplayer-trade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ Earlier dated verification sections are historical snapshots, not current covera
never-submitted quotes that expired, are terminal `refused`, retryable explicitly with the same invoice. No passive recovery submits an
unsent quote; an explicitly failed pre-POST withdrawal cannot pay later on a serve tick.
Submitted ambiguity retains exact authorization and replays it until definitive.
- Read-only `status` and `balance` use SQLite snapshots without the writer lock. Keep
- Read-only `status` (readable summary; `--json` keeps the raw records byte-for-byte) and
`balance` use SQLite snapshots without the writer lock; `discover` reads only relays and
mint keysets (per-lot `expected_fees`, `unknown` if a mint is slow) with no lock, home or
journal access. While `serve` runs, `list`/`take`/`cancel` hand off to it over
`<home>/serve.sock` (0600, same uid, 16 KiB, one request per connection) and serve executes
them with the client's exact bounds as the single writer. `list`/`take --dry-run` preview
both legs' fees, gross amounts and max total debit with zero side effects. A swap waiting
>180 s on its counterparty shows `counterparty_unresponsive` (display only). Keep
serve alive from second lock through settlement. Exits: 0 success, 1 command error,
pre-submission refusal, or definitive terminal `unpaid_released`, 2 usage, 3 unresolved work, 4 terminal manual recovery. Non-final
withdrawals never exit 0; a submitted (past `quote_created`) non-terminal withdrawal always
Expand All @@ -42,7 +49,7 @@ sections below describe their dated runs, not current coverage totals.

## Implemented

- `list`, `discover`, `cancel`, `serve`, `take`, `recover`, `preflight`, `fund`, `balance`, `status`, `withdraw`.
- `list`, `discover`, `cancel`, `serve`, `take`, `recover`, `preflight`, `fund`, `balance`, `status`, `withdraw`, `receive`.
- Immutable signed 3410 lots (24 hours), hash-chained 3411 statuses, signature/content/tag
validation, fork/gap quarantine, no terminal reopening. Every configured relay is queried;
discovery unions their results. No EOSE is an error, not an empty market; partial relay
Expand Down Expand Up @@ -277,6 +284,7 @@ LOT="$(python3 -c 'import json,sys; print([json.loads(l)["lot_id"] for l in sys.
"$TRADE" --home "$RUN/maker" serve > "$RUN/maker.log" 2>&1 &
MAKER_PID=$!
"$TRADE" --home "$RUN/taker" discover
"$TRADE" --home "$RUN/taker" take "$LOT" --max-give 40 --min-receive 32 --dry-run
"$TRADE" --home "$RUN/taker" take "$LOT" --max-give 40 --min-receive 32
# Only stop the maker after both sides are complete; otherwise keep recovery running.
kill "$MAKER_PID"
Expand Down
78 changes: 39 additions & 39 deletions crates/maxplayer-trade/skills/maxplayer-trade/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,58 @@ description: Trade Cashu tokens for a human with the standalone maxplayer-trade

# Trade Cashu for the human

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

## 1. Establish custody and scope

Use the standalone binary, not Maxplayer jobs or the Maxplayer wallet/daemon. Read
[commands](references/commands.md) before any command and
[recovery](references/recovery.md) before handling an interruption.
Use the standalone binary, not Maxplayer jobs or wallet/daemon. Read
[commands](references/commands.md) before a command and
[recovery](references/recovery.md) for any interruption.

Choose **one private, persistent absolute home** for this human's trade wallet. Put
Choose **one private, persistent absolute home** for this human. Put
`--home <home>` before **every** command. Preserve that home, keys, databases and
journals across restarts. Never delete it while funds, reservations, unresolved
funding/withdrawals, or trades exist; retain it for historical recovery afterward.
Never use a fresh home to evade caps. One writer owns it. Read-only `status` and `balance` work while `serve` runs.
funding/withdrawals, or trades exist; keep it afterward for recovery.
Never use a fresh home to evade caps. Only one writer owns it: while `serve` runs,
`list`/`take`/`cancel` hand off to it, exact bounds; `status`, `balance`,
`discover` are lock-free. Don't stop serve to look.
**Do not stop serve between `second_locked` and `settling`**; a maker can lose its
claim window. Defer other writing commands until live obligations settle.
Never copy its seed.

Never print secrets, seeds, keys, tokens, proofs, preimages or raw journals, or
attach a home/database to chat. Report only public IDs, mint URLs, amounts, states
and redacted errors. Invoice delivery belongs in the human's private conversation.
Never print or attach secrets, seeds, keys, tokens, proofs, preimages, raw journals,
homes or databases. Report only public IDs, mints, amounts, states, redacted errors. Deliver invoices only in the human's private chat.
**Done:** home and authorized mints (URL or `nostr://<npub>`; sat unit) are known.

## 2. Check readiness and get explicit authorization

Run `balance <mint>` and `preflight <mint>` per mint. Passing preflight
proves neither honesty nor full compatibility. The CLI
also preflights before fund/list/take/withdraw/send: NUT-07/09/11/12/14, an active sat keyset, reachable
mint, and clock skew at most 60 seconds. Never bypass a failed check. Gross locks and each withdrawal invoice are capped at
mint, and clock skew at most 60 seconds. Never
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.
`--max-debit` to cap invoice amount + Cashu input fee + Lightning reserve. Fees can push a net lock over the cap.

**Before every real-money `fund`, `list`, `take`, `withdraw`, or `send`, present the
exact operation and get an explicit yes.** A broad “trade for me”, earlier approval
or a published listing does not approve a new operation. State:
**Before every real-money `fund`, `list`, `take`, `withdraw`, or `send`, present
the exact operation and get an explicit yes.** For `list`/`take`, first show the
same command's `--dry-run` fees, gross amounts and max debit (changes nothing).
A broad “trade for me”, earlier approval or published listing approves no 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.
- Public lot ID for a purchase; invoice amount/destination for a withdrawal.
- Worst-case normal trade lock budget: **about 75 minutes**, not a guaranteed release
time. Protocol locks are 60/15 minutes plus margins; outages or ambiguous evidence
- Worst-case normal lock budget: **about 75 minutes**, not a guaranteed release
time. Protocol locks are 60/15 min plus margins; outages or ambiguous evidence
can hold funds longer, indefinitely with a dishonest mint. Funding and withdrawal
have their own unresolved-payment risk, not a guaranteed 75-minute timeout.
have their own unresolved-payment risk.
- 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 and limits: [verification](references/verification.md). NUT-07 advertisement alone does not prove witness emission. The maker relies on
the refund deadline and successful recovery. `serve` must stay running while locks are live (an active `take` watches its trade).
NUT-14 claims remain valid after locktime. Nutshell refund 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 human's responsibility; there is no rating gate.

Expand All @@ -67,10 +69,10 @@ If a command cannot enforce the approved bounds, **do not run it**; escalate. Ne
### Fund my trade wallet

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.
An invoice is not funded balance. Preserve the quote ID; after
interruption use `recover`, and when needed resume that same funding quote with
invoice and never auto-pays. Give it privately to the human to pay from **their own wallet**; never pay it
without separate explicit consent.
An invoice is not funded balance. Keep the quote ID; after
interruption use `recover`, and if needed resume that quote with
`fund <mint> --amount <same-sats> --quote <quote-id>` after confirmation. Never create
or pay a replacement because a reply or issuance timed out.
**Done:** issuance reconciled and `balance <mint>` confirms it, or the retained
Expand All @@ -95,7 +97,7 @@ file privately, not its contents. Exit 3: `recover`; never resend. Until redeeme
### 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 of distinct mint assets.
--max-fees <cap>`. X and Y are net lot amounts; mints are not interchangeable.
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, not further listings or price changes.
Expand All @@ -106,44 +108,42 @@ inactive cancellation is reported; an unresolved lock is never called cancelled.

### Find listings / buy that lot

`discover`, inspect the exact mint pair and public lot ID, and match the human's
price. Discovery is not purchase consent. For “receive X A for at most Y B”,
`discover` (per-lot fees or `unknown`), inspect the mint pair
and lot ID, and match the human's price. Discovery approves nothing. For “X A for at most Y B”,
set `--min-receive X` and `--max-give Y` when Y is the total spending ceiling. If Y
was explicitly a net price plus a separately approved fee F, the total ceiling can
be Y + F; otherwise never silently add fees. Stricter limits are fine, looser
not. See the worked example in the command reference.
not. Worked example: command reference.

After confirmation, `take <lot> --max-give <total-cap> --min-receive <net-minimum>
--max-fees <cap>`. Keep it running through settlement; never repeat a take after
timeout. **Done:** verify the terminal result and per-mint balances, or enter recovery.
--max-fees <cap>`. Keep it (or serve) running through settlement. Never repeat a take
to “fix” a timeout. `counterparty_unresponsive` is only a warning; keep serving. **Done:** verify the terminal result and per-mint balances, or enter recovery.

### Withdraw

Use only the human's invoice; 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 is unresolved, not failed. Never create a replacement invoice, withdrawal, or payment
immediately; it is **not** a quote preview. Read the command reference's withdrawal limits first. Non-terminal is 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 post-POST timeout is not a failed payment. **Done:** `done` and reconciled change/balance are confirmed,
or report the retained authorization and recover it without paying again.

### Check balance / something got interrupted

`balance <mint>` is wallet balance, not total wealth or proof that every reservation is spendable. After **any interruption**, use `recover` with the same
`balance <mint>` is wallet balance, not total wealth or proof every 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 s per
item** (any money item; nostr trade swap: 140 s), not 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 neither undoes an RPC nor releases reservations. Restore `serve` while locks
are live; one pass is not a watcher. See the recovery reference for
quarantine and errors.
**Done:** classify every reported outcome using the recovery reference.
are live; one pass is not a watcher. Quarantine and errors: recovery reference.
**Done:** every outcome classified per the recovery reference.

## 4. Verify and report

Check `status` and `balance` without stopping the owner process. A zero exit, elapsed deadline, missing listing or PAID mint is not
Check `status` (`--json` raw) and `balance` without stopping serve. 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ Canonical command signatures (placeholders are not literal values):
```text
preflight <mint>
balance <mint>
status [--check-sends]
status [--json] [--check-sends]
fund <mint> --amount <sats> [--quote <quote-id>]
receive <mint> [--token-file <path>]
send (<mint> --amount <sats> --out <path> | --reclaim <send-id>) [--max-fees <sats>]
list --give-mint <mint> --give <net-sats> --want-mint <mint> --want <net-sats> --max-fees <sats>
list --give-mint <mint> --give <net-sats> --want-mint <mint> --want <net-sats> --max-fees <sats> [--dry-run]
discover
serve
cancel <lot>
take <lot> --max-give <total-sats> --min-receive <net-sats> --max-fees <sats>
take <lot> --max-give <total-sats> --min-receive <net-sats> --max-fees <sats> [--dry-run]
withdraw <mint> --invoice <bolt11> [--max-debit <sats>]
recover
```
Expand All @@ -37,7 +37,8 @@ Use `-h` / `--help` at the root or on any application subcommand. The built-in
`help [COMMAND]` also shows root or command help. There is no version option.
`--home <HOME>` is required for operational commands; `--relay <RELAY>` is
repeatable. All command-specific flags in the signatures above are required except
`--quote`, `--max-debit`, `--token-file` (stdin when omitted), `--check-sends`, and `--max-fees` (default 16 on list/take; on send it is required unless the mint's input fee is 0). `send` takes either `<mint> --amount --out` or `--reclaim <send-id>`. Positional mint arguments are
`--quote`, `--max-debit`, `--token-file` (stdin when omitted), `--dry-run`, `--json`, `--check-sends`,
and `--max-fees` (default 16 on list/take; on send it is required unless the mint's input fee is 0). `send` takes either `<mint> --amount --out` or `--reclaim <send-id>`. Positional mint arguments are
canonical URLs, or `nostr://<npub>` for a Nostr-reachable mint (hex or uppercase input is
canonicalized to the lowercase npub). `fund` and `withdraw` refuse `nostr://` mints before
any journal entry: the Maxplayer credits mint serves no NUT-04/05/20; `receive` is how credits
Expand Down Expand Up @@ -83,9 +84,45 @@ common trading capabilities. Diagnostic preflight output goes to stderr. No opt-
passing advertisement checks is not independent refund interoperability evidence.
`balance` and `status` are lock-free read-only SQLite snapshots, available during
`serve`, without submission of earlier authorizations; neither makes network calls
except `status --check-sends` (read-only NUT-07 per sent token).
Market commands (including `discover` and `cancel`) first recover existing
authorizations; do not treat them as read-only on a home with pending obligations.
except `status --check-sends` (read-only NUT-07 per sent token). `status`
prints one readable line per lot/swap/funding/withdrawal/receive, unresolved items
first, any `counterparty_unresponsive` warning, and the exit-code meanings;
`status --json` prints the unchanged raw public records for scripts.

`discover` is lock-free and read-only too: it reads relays only, with a throwaway
relay identity, never runs recovery, never takes the home lock, never creates or
touches the home or journal, and works while `serve` or `take` runs. Each listing
carries `expected_fees` per leg (`net`, `ppk`, `gross`, `claim_fee`) from the
mints' published keysets, or `"unknown"` when a mint did not answer within ~5 s
(discover never blocks on a mint). The taker's own lock fee depends on its inputs:
use `take --dry-run`. Relay union, EOSE and `timeout_or_partial` reporting are unchanged.

## Fee preview: `--dry-run`

`list ... --dry-run` and `take ... --dry-run` run the normal preflight, then print one
JSON object: `you_give` (keyset, ppk, net, gross, `lock_fee`, `claim_fee`, `debit`,
inputs that would be selected), `you_receive` (incoming leg's gross/claim fee, funded
by the counterparty), `max_total_debit` and `fees_total`. The numbers use the same
arithmetic and input selection as the real command against the current wallet and
the mints' published keysets. Nothing is journaled, reserved, locked, created or
published, and no quote request reaches the maker. Exit 0 when the real command
would pass the same bounds; exit 1 with the reason (e.g. `maximum give cap`,
`fee cap exceeded`, insufficient balance, or `cannot solve bounded fee-inclusive
split` for an amount with no exact fee-inclusive gross) otherwise. Run it before
asking for approval, then run the identical command without `--dry-run`. Balances
can change in between; the real command enforces your bounds again.

## Hand-off to a running serve

While `serve` owns the home, `list`, `take` and `cancel` hand their request to it
over `<home>/serve.sock` (mode 0600, same user only) and print serve's result with
the usual output and exit codes. serve executes it with exactly the bounds given;
it never fills in defaults. Use the same `--relay` set as serve or the request is
refused. A handed-off `take` prints its swap id and that serve is watching it, then
streams states until settlement; if the command is interrupted, serve keeps driving
the swap (check `status`). Without a running serve these commands behave as before.
`recover` and the money commands still need the home lock; stop serve first.
`cancel` first recovers existing authorizations when it runs without serve.
`serve` continuously accepts trades and recovers obligations; keep it running while
locks are live. `recover` is a bounded single pass, not a replacement watcher;
see [recovery](recovery.md) for exit codes and states.
Expand Down
Loading
Loading