Background
In the agicash-team #feedback-team "auth limits testing" thread (1505825679269298267), bobthree reported 2026-05-18 that topping up >1k sats from a Spark wallet to an auth-limited gift-card mint (1k sats / 1m window) showed a generic toast instead of the mint's quota-exceeded detail. jbojcic confirmed the network tab returned a proper 33001 Mint quota exceeded payload from the mint, with structured data: {limit, used, unit, window_secs, retry_after} — the UI was masking it.
CDK PR #31 (merged) rephrased the mint-side 33001 detail to "Amount exceeds remaining daily limit of ₿X." — so on master today, the mint returns an already-friendly error.message. The wallet just isn't surfacing it.
What's wrong
33001 is not in app/lib/cashu/error-codes.ts (the CashuErrorCodes enum stops at 31004 on master), and the mint-quote call sites have no error handling:
app/features/receive/cashu-receive-quote-core.ts:268 — await wallet.createLockedMintQuote(...) (the receive side of /Add / gift-card top-up)
app/features/send/cashu-send-quote-service.ts:150 — await wallet.createMeltQuoteBolt11(...) (the send side)
A MintOperationError from cashu-ts propagates raw. It is not a DomainError, so at app/features/transfer/transfer-input.tsx:83-93 the instanceof DomainError check fails and the user gets the destructive fallback toast "Failed to create transfer. Please try again." The mint's friendly error.message goes to console.error only.
Proposed fix
Map MintOperationError → DomainError at the cashu-quote boundary, so the mint-supplied message reaches the existing toast convention. Starting with 33001:
- Add
MINT_QUOTA_EXCEEDED = 33001 to app/lib/cashu/error-codes.ts.
- At the quote-creation call sites (
cashu-receive-quote-core.ts:268, cashu-send-quote-service.ts:150), catch MintOperationError, switch on error.code, and throw new DomainError(error.message) for recognized codes. Unknown codes can either pass through as DomainError(error.message) (mint message is usually decent) or fall to generic — design choice for the PR.
Nuance: the pinned cashu-ts@3.6.1 strips the structured data: {limit, used, unit, window_secs, retry_after} field before throwing — only error.message is reachable today. The cashu-ts v4 upgrade in PR #1108 restores data, after which a richer formatter (e.g. retry-after countdown) becomes possible. Not blocking — surfacing error.message is the immediate win.
Already drafted
.claude/worktrees/pr1-quota-error/ has a draft pass of this change (adds MINT_QUOTA_EXCEEDED, a quote-errors.ts formatter, wiring at both quote services). May be the starting point for the PR.
Minor follow-up (out of scope)
canSendToLightning at app/features/accounts/account.ts:102-110 returns a bare boolean for four distinct reject reasons (!isTransactional, isTestMint, !isOnline, NUT-05 disabled). The throw site at transfer-service.ts:88-92 formats them all as " cannot send Lightning payments". For gift-card / offer-purpose mints (the path now assumed for all auth-limited mints), the reject is correct but the copy reads as the named mint being broken. Reason-returning predicate + per-reason copy is a small cleanup; not blocking, not in this issue's scope.
Background
In the agicash-team #feedback-team "auth limits testing" thread (1505825679269298267), bobthree reported 2026-05-18 that topping up >1k sats from a Spark wallet to an auth-limited gift-card mint (1k sats / 1m window) showed a generic toast instead of the mint's quota-exceeded detail. jbojcic confirmed the network tab returned a proper
33001 Mint quota exceededpayload from the mint, with structureddata: {limit, used, unit, window_secs, retry_after}— the UI was masking it.CDK PR #31 (merged) rephrased the mint-side
33001detail to "Amount exceeds remaining daily limit of ₿X." — so on master today, the mint returns an already-friendlyerror.message. The wallet just isn't surfacing it.What's wrong
33001is not inapp/lib/cashu/error-codes.ts(theCashuErrorCodesenum stops at31004on master), and the mint-quote call sites have no error handling:app/features/receive/cashu-receive-quote-core.ts:268—await wallet.createLockedMintQuote(...)(the receive side of/Add/ gift-card top-up)app/features/send/cashu-send-quote-service.ts:150—await wallet.createMeltQuoteBolt11(...)(the send side)A
MintOperationErrorfrom cashu-ts propagates raw. It is not aDomainError, so atapp/features/transfer/transfer-input.tsx:83-93theinstanceof DomainErrorcheck fails and the user gets the destructive fallback toast "Failed to create transfer. Please try again." The mint's friendlyerror.messagegoes toconsole.erroronly.Proposed fix
Map
MintOperationError→DomainErrorat the cashu-quote boundary, so the mint-supplied message reaches the existing toast convention. Starting with33001:MINT_QUOTA_EXCEEDED = 33001toapp/lib/cashu/error-codes.ts.cashu-receive-quote-core.ts:268,cashu-send-quote-service.ts:150), catchMintOperationError, switch onerror.code, andthrow new DomainError(error.message)for recognized codes. Unknown codes can either pass through asDomainError(error.message)(mint message is usually decent) or fall to generic — design choice for the PR.Nuance: the pinned
cashu-ts@3.6.1strips the structureddata: {limit, used, unit, window_secs, retry_after}field before throwing — onlyerror.messageis reachable today. The cashu-ts v4 upgrade in PR #1108 restoresdata, after which a richer formatter (e.g. retry-after countdown) becomes possible. Not blocking — surfacingerror.messageis the immediate win.Already drafted
.claude/worktrees/pr1-quota-error/has a draft pass of this change (addsMINT_QUOTA_EXCEEDED, aquote-errors.tsformatter, wiring at both quote services). May be the starting point for the PR.Minor follow-up (out of scope)
canSendToLightningatapp/features/accounts/account.ts:102-110returns a bare boolean for four distinct reject reasons (!isTransactional,isTestMint,!isOnline, NUT-05disabled). The throw site attransfer-service.ts:88-92formats them all as " cannot send Lightning payments". For gift-card / offer-purpose mints (the path now assumed for all auth-limited mints), the reject is correct but the copy reads as the named mint being broken. Reason-returning predicate + per-reason copy is a small cleanup; not blocking, not in this issue's scope.