End goal
Buyer and seller each configure one mint list (agicash-like): the set of mints that side is willing to transact on. The buyer pays a seller from any of the buyer's mints — if the lists don't overlap, the buyer's wallet swaps across (mint quote at the seller's mint → melt from a buyer-funded mint → fresh ecash at the seller's mint), which is the cross-mint hop slice.
Today's config and fence can't express that. Two concrete defects, both in the same predicate, hence one issue and one review.
Current state — evidence
home::mint_allowed (crates/mobee-core/src/home.rs:750-758) is the single real-mint fence, enforced inside open_wallet_at_mint_async (buyer_fund.rs:85-106) and by resolve_realized_mint (authorize_pay.rs:471):
pub fn mint_allowed(mint_url: &str, allow_real_mints: bool) -> bool {
if allow_real_mints {
mint_url.strip_prefix("https://").is_some_and(|host| !host.is_empty())
} else {
mint_url == DEFAULT_MINT_URL
}
}
1. Under the default posture the buyer has exactly one mint. With allow_real_mints == false, the fence admits only DEFAULT_MINT_URL, so no second mint's wallet can be opened at all. A buyer "mint list" of more than one entry is unrepresentable unless the real-money switch is on — i.e. multi-mint behaviour is structurally unreachable in the safe posture. The doc comment already reads "only the testnut/dev allow-list — today that is exactly DEFAULT_MINT_URL", so a list was the intent and the equality is the degenerate one-element case.
2. extra_mints is dead config. MobeeConfig::extra_mints is documented as the buyer wallet's additional allowed mints (home.rs:590-605) and wallet_ops.rs:167 composes "default mint_url first, then opt-in extra_mints (deduped)" — but mint_allowed never consults it. With the default allow_real_mints == false an extra mint is refused at wallet-open; with allow_real_mints == true the field is redundant because any https:// mint already passes. So the field cannot currently do the job its own docs describe.
Note also that the seller side already has its own list (accepted_mints, the accept policy) while the buyer side has extra_mints — two different shapes for what the end goal wants to be one symmetric concept per side.
Direction
Replace the equality with a real buyer mint list and keep the real-money switch orthogonal to it: the list says which mints this side transacts on; allow_real_mints says whether real-sats mints are permitted at all. Then extra_mints either becomes that list or is folded into it and removed, and the seller's accepted_mints is the mirror-image field.
Worth knowing while choosing defaults: test mints cannot pay each other's Lightning invoices, so a cross-mint hop between two fake mints is not exercisable end-to-end — the hop's live proof is a small real-sats trade. As of 2026-07-27 the only reachable public testnut probed was https://testnut.cashudevkit.org (cdk-mintd/0.17.0, NUT-04 + NUT-05 enabled for sat/bolt11); testnut.cashu.space and family are dead, which the repo already records (DEAD_TESTNUT_MINT_HOST, home.rs:67). So a multi-entry testnut list buys little on its own — the value is in expressing the real buyer/seller mint lists.
Teeth a fix should carry
- The default posture admits every list entry and refuses everything else, including plausible near-misses (
http:// scheme, empty host, a real-sats mint URL when the switch is off).
- An operator-named list entry is honoured at wallet-open, not just at config parse — i.e. the defect in (2) cannot come back.
- Buyer list and seller
accepted_mints gate on the same predicate, so the two sides cannot drift (the property the current doc comment claims).
- Turning on
allow_real_mints remains the only way to reach a real-sats mint, independent of list membership.
- Overlap vs no-overlap between the two lists is what selects direct payment vs the cross-mint hop; both branches asserted on the decision.
Related: the cross-mint hop slice (mint quote → melt → fresh ecash at the seller's mint) and #186 (hop fee-reserve reconciliation).
End goal
Buyer and seller each configure one mint list (agicash-like): the set of mints that side is willing to transact on. The buyer pays a seller from any of the buyer's mints — if the lists don't overlap, the buyer's wallet swaps across (mint quote at the seller's mint → melt from a buyer-funded mint → fresh ecash at the seller's mint), which is the cross-mint hop slice.
Today's config and fence can't express that. Two concrete defects, both in the same predicate, hence one issue and one review.
Current state — evidence
home::mint_allowed(crates/mobee-core/src/home.rs:750-758) is the single real-mint fence, enforced insideopen_wallet_at_mint_async(buyer_fund.rs:85-106) and byresolve_realized_mint(authorize_pay.rs:471):1. Under the default posture the buyer has exactly one mint. With
allow_real_mints == false, the fence admits onlyDEFAULT_MINT_URL, so no second mint's wallet can be opened at all. A buyer "mint list" of more than one entry is unrepresentable unless the real-money switch is on — i.e. multi-mint behaviour is structurally unreachable in the safe posture. The doc comment already reads "only the testnut/dev allow-list — today that is exactlyDEFAULT_MINT_URL", so a list was the intent and the equality is the degenerate one-element case.2.
extra_mintsis dead config.MobeeConfig::extra_mintsis documented as the buyer wallet's additional allowed mints (home.rs:590-605) andwallet_ops.rs:167composes "defaultmint_urlfirst, then opt-inextra_mints(deduped)" — butmint_allowednever consults it. With the defaultallow_real_mints == falsean extra mint is refused at wallet-open; withallow_real_mints == truethe field is redundant because anyhttps://mint already passes. So the field cannot currently do the job its own docs describe.Note also that the seller side already has its own list (
accepted_mints, the accept policy) while the buyer side hasextra_mints— two different shapes for what the end goal wants to be one symmetric concept per side.Direction
Replace the equality with a real buyer mint list and keep the real-money switch orthogonal to it: the list says which mints this side transacts on;
allow_real_mintssays whether real-sats mints are permitted at all. Thenextra_mintseither becomes that list or is folded into it and removed, and the seller'saccepted_mintsis the mirror-image field.Worth knowing while choosing defaults: test mints cannot pay each other's Lightning invoices, so a cross-mint hop between two fake mints is not exercisable end-to-end — the hop's live proof is a small real-sats trade. As of 2026-07-27 the only reachable public testnut probed was
https://testnut.cashudevkit.org(cdk-mintd/0.17.0, NUT-04 + NUT-05 enabled for sat/bolt11);testnut.cashu.spaceand family are dead, which the repo already records (DEAD_TESTNUT_MINT_HOST, home.rs:67). So a multi-entry testnut list buys little on its own — the value is in expressing the real buyer/seller mint lists.Teeth a fix should carry
http://scheme, empty host, a real-sats mint URL when the switch is off).accepted_mintsgate on the same predicate, so the two sides cannot drift (the property the current doc comment claims).allow_real_mintsremains the only way to reach a real-sats mint, independent of list membership.Related: the cross-mint hop slice (mint quote → melt → fresh ecash at the seller's mint) and #186 (hop fee-reserve reconciliation).