Skip to content

fix(cli): say how to get a USD quote and what it must match - #61

Merged
thebrandonlucas merged 1 commit into
masterfrom
fix/cli-usd-quote-guidance
Oct 21, 2026
Merged

thebrandonlucas merged 1 commit into
masterfrom
fix/cli-usd-quote-guidance

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stack 1 of 6. Base: master. Next: feat/cli-next-step-after-accept.

Closes #39, closes #35, closes #38.

Problem

Behavior

Case Before After
USD payment, no --quote USD payment flows require an explicit quote_id A USD payment needs --quote QUOTE_ID (quote_id in --data), with a hint: a USD payment needs a wallet on a USD line of credit and a quote (quotes create, quotes get, --quote); a BTC-line wallet uses --currency btc
quotes create --unit sats --to btc sent; HTTP 400 mismatch_currency rejected before sending: --unit sats is already in the --to currency, btc; …, with a hint naming both directions: --unit sats --to usd to pay a BTC invoice from a USD wallet, --unit usd --to btc to receive a USD amount
HTTP 400 invalid_currency whose detail names a quoted amount API reason only adds a hint: the quote must convert to exactly the payment's amount, check it with quotes get, and quote an invoice's BTC amount --unit sats --to usd or a USD amount --unit usd --to btc
HTTP 400 invalid_currency, any other detail API reason only unchanged: no hint

The API returns invalid_currency for several mismatches that are not about a quote (a USD payment on a BTC line, a non-BTC Bolt11 or on-chain amount, max_fee), so the hint is given only when the detail contains quoted amount . The backend wraps that detail as Payment currency must be valid: …, so the check is contains, not starts_with. The --amount, --unit and --to help says which currency each one names. payments.md, troubleshooting.md (new invalid_currency entry), exit-codes.md and the changelog are updated.

Backend rules checked (backend origin/master 1630b77c):

  • request_a_quote accepts a quote only when the line of credit's currency equals the source or the target (crates/api/src/domain/quoting/routes.rs:613). When the two sides are in the same currency, the quote converts nothing, so the CLI rejects it. NewQuoteRequest documents the directions (:516–:521): a USD wallet paying a known BTC amount quotes that BTC amount to: usd; a USD receive quotes the USD amount to: btc.
  • validate_quote_amount needs the quote to convert to exactly the payment amount (crates/api/src/domain/payments/routes.rs:4059, :4068). Other invalid_currency sources: :3689, :3736, :3777, :3824, :3897, :4029, :4047.

Limits

Checks

On head 641003e, rebased onto master 905cf4e:

  • cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (108 unit, 91 integration), and python3 scripts/check-coverage.py pass. All ran in nix develop.
  • New error paths have tests: the same-currency quote, for both sats/btc and cents/usd, with its hint; the local USD quote check with its hint, as a unit test and through the CLI; the invalid_currency hint for both quoted-amount details, and no hint for a non-quote invalid_currency detail or a missing detail.
  • CRAP: run on the stack tip (feat(cli): replace a saved login when the same user signs in again #67, c66d2a3), which contains this commit: 0 of 555 measured functions at or above 20. The highest is wait_for_payment at 19.60, unchanged from master. This head adds no branches since 63bbdb3 (it moves a constant and changes hint text), so it was not run separately.
  • tee8z review fixes: one hint form, --unit usd --to btc, in the hint, payments.md, and the test; payments.md scopes the local rejection to --unit and --to; a comment on the "quoted amount " match names the backend messages it depends on; USD_PAYMENT_HINT is a private const in input.rs.
  • Live staging quote behavior was not tested.

@thebrandonlucas thebrandonlucas changed the title fix/cli usd quote guidance fix(cli): say how to get a USD quote and what it must match Oct 20, 2026
@thebrandonlucas
thebrandonlucas marked this pull request as ready for review October 20, 2026 17:59

@thebrandonlucas thebrandonlucas left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of c0692e3 against master b25c05d (Aurelius). Backend references are voltagecloud/backend origin/master 1630b77c.

1. High: the same-currency hint tells a USD wallet to quote in the wrong direction for sends (src/input.rs:548)
The hint says "To pay or receive with a USD wallet, quote the USD amount: --unit usd --to btc." The backend says the opposite for paying: "For a USD wallet paying a known BTC amount, use that BTC amount and set to to USD" (crates/api/src/domain/quoting/routes.rs:520, and the btc_to_usd_quote example at :557: "Reuse the exact BTC amount in the payment request"). A send's amount must be BTC (payments/routes.rs:3777, try_as_msats gives "Only BTC is supported for Bolt11 payments"). validate_quote_amount (payments/routes.rs:4056) then converts the quoted amount into the payment's currency and requires an exact match. A USD→BTC quote almost never converts to the invoice's exact sats. A BTC→USD quote of the invoice amount matches trivially.
Fix: "To receive into a USD wallet, quote the USD amount: --unit usd --to btc. To pay from one, quote the invoice's BTC amount: --unit sats --to usd." Add the send direction to the payments guide next to the receive example at docs/guides/payments.md:34.

2. High: the sub-cent claim follows from the wrong quote direction and is unverified (docs/guides/payments.md:46, docs/guides/troubleshooting.md:106, src/api.rs:403)
"A quote in cents cannot match an invoice worth less than one cent" comes from issue #38's reproduction, which used --amount 1 --unit cents --to btc. The documented send quote is --amount N --unit sats --to usd, whose amount is the invoice's own sats, so the exact-match check passes. It is unknown whether the backend accepts a sats→usd quote that rounds to 0 cents or debits it correctly. Until that is checked on staging, telling users that a USD wallet can never pay a sub-cent invoice is a guess presented as a rule. "Exactly the payment's amount" is also only practically reachable with the BTC→USD direction, which none of the new text names.
Fix: In these three places, say to quote the invoice's sats with --to usd. Drop the sub-cent sentence, or verify it on staging with a 1 sat invoice and a --unit sats --to usd quote first. Then decide whether #38 is closed or only referenced.

3. Medium: the invalid_currency hint assumes a quote mismatch, but the API uses that type for at least 6 other causes (src/api.rs:357, src/api.rs:402)
The doc comment says "among other mismatches", but the hint always says the quote must match the amount. The backend also returns invalid_currency for:

  • a USD amount on a Bolt11, on-chain or BIP21 send (payments/routes.rs:3777, :3824, :3897)
  • a USD receive on a BTC line (:3689)
  • a quote that does not involve the line's currency or the payment's currency (:4029, :4047)
  • a non-asset currency for taproot (:3736)

In each of these, "check what the quote converts to" sends the user the wrong way.
Fix: Either match only the mismatch detail (it starts with quoted amount / cannot validate quoted amount) and give no hint otherwise, or word the hint so it holds for every cause. Add a test with one non-quote invalid_currency detail, for example "USD payment currency must be on a USD line of credit".

4. Low: the local USD check's hint contradicts itself for a BTC wallet (src/input.rs:672)
is_usd() fires on currency: usd or a USD amount, not on the wallet's line. With --currency usd on a BTC-line wallet, the CLI says "needs --quote", and QUOTE_HINT then says "A wallet on a BTC line of credit needs no quote." The backend rejects that receive anyway with invalid_currency (payments/routes.rs:3689), so a quote does not help.
Fix: Append to this hint only: "On a BTC line of credit, use a BTC amount and drop --currency usd." Or use a local hint that says the same.

5. Nit: "The line of credit must be in one of the two" is always true (src/cli.rs:488, docs/guides/payments.md:44)
Currency here is only btc or usd. The backend rule (quoting/routes.rs:613) that matters is that the line's currency equals --to or the --unit currency. Say "in the line of credit's currency" once, or drop the clause.

Tests for new paths: same-currency rejection (both pairs), the invalid_currency hint, and the local quote hint (integration) are all covered. Nothing is missing beyond the non-quote invalid_currency case in finding 3. QUOTE_HINT reuse is good.

Checks on c0692e3 (verified with git rev-parse HEAD in the worktree):

  • cargo fmt --check: pass
  • cargo clippy --locked --all-targets -- -D warnings: pass
  • cargo machete --with-metadata (in nix develop): pass
  • cargo test --locked: pass, 108 unit and 91 integration (run by the CRAP script)
  • scripts/check-coverage.py: pass
  • CRAP report: 0 of 517 measured functions at or above 20. Highest are wait_for_payment at 19.60 (unchanged) and check_payload at 17.00 (100% covered).

Not run: a staging run of either quote direction for a sub-cent invoice (no credentials in this session), and CodeRabbit status.

@thebrandonlucas
thebrandonlucas added this pull request to stack #63 October 20, 2026 18:02

@thebrandonlucas thebrandonlucas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cicero review of c0692e3 (base master b25c05d)

Finding 1: the invalid_currency hint gives quote advice for errors that have nothing to do with a quote

src/api.rs:403 (also docs/guides/troubleshooting.md:104-106, docs/reference/exit-codes.md:16, CHANGELOG.md:26)

hint() attaches the "a quote must convert to exactly the payment's amount" hint to every invalid_currency. In backend master (1630b77c) the API returns invalid_currency for many reasons. Only two of them are a quote/amount mismatch (crates/api/src/domain/payments/routes.rs:4059, :4068). The others:

  • routes.rs:3689: "USD payment currency must be on a USD line of credit"
  • routes.rs:3777, :3824, :3897: "Only BTC is supported for Bolt11/OnChain/BIP21 payments"
  • routes.rs:3784, :3855, :3904: "Only BTC is supported for max_fee in ..."
  • routes.rs:685, :723, :755: internal storage
  • line_of_credit/error.rs:160: line-of-credit create and update

Failure scenario: someone pays from a BTC-line wallet with a USD amount in --data, or passes --max-fee in cents. They get the API's real reason, followed by a hint telling them to check voltage quotes get QUOTE_ID and use a BTC line of credit. That is the wrong next step, and in the first case it is backwards. The doc comment at src/api.rs:357 already says "among other mismatches", but the code does not check which one it got.

Fix: give the hint only when the detail shows a quote mismatch. For example, use ApiErrorType::InvalidCurrency if self.detail.as_deref().is_some_and(|d| d.starts_with("quoted amount ") || d.starts_with("cannot validate quoted amount ")), and return None otherwise. Add a test asserting no hint for, say, "Only BTC is supported for max_fee in Bolt11 payments"; that early return is currently untested. Narrow the troubleshooting section, the exit-codes sentence and the CHANGELOG line to say "when the API says the quoted amount does not match".

Checked, no finding

  • quotes create same-currency rejection (src/input.rs:542): this matches the backend rule in quoting/routes.rs:613 (the line of credit must equal the source or the --to currency). Both sats→btc and cents→usd are tested. --unit usd --to btc in the hint is a valid combination.
  • Local USD-without-quote check (src/input.rs:670): QUOTE_HINT is reused and not duplicated. The new message and hint are asserted in tests/cli_integration.rs:2666.
  • The sub-cent claim in payments.md:46 matches validate_quote_amount, which needs an exact converted match.

Checks run on c0692e3

  • Verification gate: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata (run in nix develop), cargo test --locked (108 unit, 91 integration) and scripts/check-coverage.py all pass. The tree is clean afterwards.
  • CRAP report (scripts/crap-report.sh): 0 functions at or above 20. The highest is wait_for_payment (src/api.rs:1119) at 19.60, the same as on master. check_payload is at 17.00 with 100% coverage.
  • Skills: voltage-pr-review and its Rust backend style checklist. The repo AGENTS.md "Before you request review" checks.
  • Not run: a staging check of the real invalid_currency and mismatch_currency responses. The backend claims above come from reading the source at 1630b77c, not from a live request.

@thebrandonlucas thebrandonlucas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cicero re-review of a149b80

My finding is fixed. invalid_currency gets the hint only for a quoted amount detail. contains is right because payments/error.rs:118 wraps the detail. Both quote details and two non-quote cases are tested. The docs and CHANGELOG are narrowed to match. I also checked the new quote directions against NewQuoteRequest (quoting/routes.rs:516-522), and they match. Correction to my first review: I called --unit usd --to btc a valid hint for paying. Aurelius was right that it is the wrong direction for a send.

Finding (low): one sentence still says to reuse the USD amount

docs/guides/troubleshooting.md:100 (the quote_required section, next to the text this PR adds) still says "pass --quote QUOTE_ID and the same USD amount to the payment". That is right for a receive. For a send, the payment keeps the invoice's BTC amount: the btc_to_usd_quote example says "Reuse the exact BTC amount in the payment request". A reader following it would get the invalid_currency this PR explains. Fix: "...and the same amount you quoted (the invoice's BTC amount to pay, the USD amount to receive)", or link to the new paragraph in payments.md.

Checks on a149b80

  • Gate in nix develop: fmt, clippy, machete, tests (108 unit, 91 integration) and coverage pass. The tree is clean afterwards.
  • CRAP: 0 of 517 measured functions at or above 20. The highest is wait_for_payment at 19.60.
  • Not run: a staging check of invalid_currency responses.

@thebrandonlucas
thebrandonlucas force-pushed the fix/cli-usd-quote-guidance branch from a149b80 to 1fd4f30 Compare October 20, 2026 18:17

@thebrandonlucas thebrandonlucas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 1fd4f30 (Aurelius). My 5 findings from c0692e3 are fixed. I checked the quote directions against backend 1630b77c (NewQuoteRequest, quoting/routes.rs:516) and the two quoted amount details (payments/routes.rs:4060, :4069).

Low: docs/guides/payments.md:46 says "To receive a USD amount, quote it with --unit cents --to btc, as above." The example above it (:38) uses --amount 10 --unit usd --to btc. Both units are valid, but "as above" points to a different flag. Fix: write --unit usd --to btc here, to match the example, or drop "as above".

Checks:

  • Gate on a149b80 (nix develop): fmt, clippy, machete, tests (108 unit, 91 integration) and coverage pass.
  • CRAP on a149b80: 0 functions at or above 20. The highest is wait_for_payment at 19.60, then check_payload and endpoint at 17.00.
  • 1fd4f30 changes only troubleshooting.md from a149b80. The commit is signed G blu@blu.cx, with no trailers.
  • Not run: a staging check of invalid_currency.

@thebrandonlucas
thebrandonlucas force-pushed the fix/cli-usd-quote-guidance branch from 1fd4f30 to 63bbdb3 Compare October 20, 2026 18:24

@thebrandonlucas thebrandonlucas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 63bbdb3 (Aurelius). My low finding is fixed: payments.md:46 now says --unit usd --to btc, which matches the example at :38. No findings remain. The only change since 1fd4f30 is that line. Signed G blu@blu.cx, with no trailers.

@thebrandonlucas thebrandonlucas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 63bbdb3. This is not an approval. My findings are fixed. The change since 1fd4f30 is docs only: payments.md:46 now uses --unit usd --to btc, which matches the example at :38. I did not rerun the gate on this head, because no src/ or tests/ changed.

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 63bbdb3. CI is green. I ran the CRAP report on this head: 0 of 517 functions at or above 20, highest wait_for_payment at 19.60. No blocking issues, only nits:

  • The receive hints in src/api.rs (QUOTE_MISMATCH_HINT) and src/input.rs (same-currency rejection) say --unit cents --to btc. docs/guides/payments.md says --unit usd --to btc, which matches its example. Both work, but readers compare them. Pick one, and update the hint assertion in the input.rs tests.
  • docs/guides/payments.md says "The CLI rejects a quote whose --unit is already in the --to currency". That holds only on the flag path; a complete --data body still goes to the API and gets mismatch_currency. Consider "With --unit and --to, the CLI rejects…".
  • ApiErrorBody::hint matches detail.contains("quoted amount "). A short comment naming the backend message it depends on would help whoever changes either side next.
  • The description reports the checks on a149b80. Say that only docs changed since then, or name the head.

Comment thread src/api.rs Resolved
tee8z
tee8z previously approved these changes Oct 21, 2026

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved 63bbdb3 against master. The quote directions, local rejections, and API error hints match the reviewed contract. Remaining hint-placement and wording suggestions are nonblocking.

Validation on this unchanged head: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked, and python3 scripts/check-coverage.py passed. CRAP: 517 measured functions, none at or above 20; highest wait_for_payment 19.60. 137 test functions were unmeasured.

A USD payment without a quote now names --quote QUOTE_ID, says it
needs a wallet on a USD line of credit, and says how to create the
quote, instead of naming the API field quote_id. quotes create rejects
a --unit already in the --to currency before sending it, since the
quote would convert nothing, and its hint names both directions: the
invoice's sats to usd to pay, a USD amount to btc to receive. An
invalid_currency rejection of a quoted amount says the quote must
convert to exactly the payment's amount and how to quote it. Other
invalid_currency rejections get no hint.

Closes #39
Closes #35
Closes #38
@thebrandonlucas

Copy link
Copy Markdown
Contributor Author

Addressed tee8z review 5590694929 in 641003e (rebased onto master 905cf4e):

  • One receive form, --unit usd --to btc, in the hint, docs/guides/payments.md and the input.rs test.
  • payments.md now says "With --unit and --to, the CLI rejects…", and notes that a complete --data body goes to the API as is.
  • A comment on the "quoted amount " match in ApiErrorBody::hint names the backend messages it depends on.
  • USD_PAYMENT_HINT moved into input.rs as a private const.
  • The description reports the checks on the new head.

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved 641003e against master 905cf4e. This head is the rebase of 63bbdb3, plus fixes for my nits:

  • One receive form, --unit usd --to btc, in QUOTE_MISMATCH_HINT, in the same-currency rejection, in its test, and in payments.md.
  • payments.md limits the local rejection to --unit and --to, and says that a --data body goes to the API as is.
  • A comment on the "quoted amount " match names both backend messages that it depends on.
  • USD_PAYMENT_HINT is now a private const in input.rs.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (108 unit, 91 integration) and python3 scripts/check-coverage.py passed. CRAP: 517 measured functions, none at or above 20, highest wait_for_payment at 19.60; 137 functions were unmeasured. When I approved, CI on x86_64 macOS was still pending, and the other jobs had passed.

@thebrandonlucas
thebrandonlucas merged commit 1a5665b into master Oct 21, 2026
7 checks passed
@thebrandonlucas
thebrandonlucas deleted the fix/cli-usd-quote-guidance branch October 21, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants