- Rust 99%
- Nix 0.6%
- Shell 0.3%
- Just 0.1%
|
Some checks failed
Checks / format (push) Successful in 1m0s
Checks / format (pull_request) Successful in 1m3s
Checks / dependencies (pull_request) Successful in 1m9s
Checks / cdk-compatible (pull_request) Failing after 12m47s
Checks / dependencies (push) Successful in 1m19s
Checks / checks (push) Failing after 19m59s
Checks / checks (pull_request) Failing after 23m50s
Checks / cdk-compatible (push) Successful in 16m23s
Cap new Cashu reservations globally and per intent, preserving existing retries. Rotate recovery through a durable cursor with bounded attempts and elapsed time. Checkpoint pre-melt cleanup before deleting isolated source wallet storage, then remove settlement ownership and unused hold records atomically. Add an idempotent cleanup hook for caller-owned wallets. Preserve started melts and uncollected destination quotes, and bound orphaned receiving-wallet quotes. Cover admission races, restart, cleanup interruption, provider storage, manual hold reuse, quote capacity, and collection of late funds without receipts. |
||
|---|---|---|
| .github/workflows | ||
| crates/payments | ||
| scripts | ||
| tests/regtest | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| deny.toml | ||
| DEVELOPMENT.md | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| LICENSE | ||
| NOTICE.md | ||
| README.md | ||
| rust-toolchain.toml | ||
satsettle
A Rust library that creates one durable payment intent with Cashu, L402, and Lightning x402 offers. Choose automatic settlement or manual claim/reject for exclusive purchases such as usernames. Each buyer gets a separate intent; your application reserves the resource before claiming the winning payment.
This is an embedded, single-process library. It includes CDK wallets, an LDK Server client, protocol codecs, and persistent recovery journals. Applications provide HTTP handlers (if needed), authentication, rate limits, scheduling, and fulfillment. Manual capture starts authenticated LDK event listeners; the application still schedules recovery and makes allocation decisions.
Use it
The initial version is consumed as a path or Git dependency. Publishing to crates.io requires replacing the pinned Git LDK Server dependency with a compatible published release.
[dependencies]
satsettle = { path = "../402_payments/crates/payments" }
use satsettle::{CreateIntent, Engine, IdempotencyKey, PaymentProof};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let payments = Engine::open(".state/payments").await?;
payments.recover().await?;
// Generate for a new checkout; save it with the order and reuse it on retries.
let key = IdempotencyKey::generate();
let intent = payments.create_intent(CreateIntent {
idempotency_key: key,
reference: "order-123".into(),
amount_sats: 10,
// Supply and retain an absolute deadline; don't change it on retries.
expires_at: 2_000_000_000,
cashu_mint: Some("https://your-mint.example".into()),
capture: satsettle::CaptureMode::Automatic,
cashu_settlement: None,
lightning: None,
}).await?;
// Present intent.offers to the payer, then accept their selected proof.
let receipt = payments.submit_payment(
&intent.id,
PaymentProof::Cashu("cashuB...".into()),
).await?;
// Fulfill by intent.id in your application's own durable transaction.
# Ok(())
# }
The compilable checkout example shows all three
methods, HTTP request binding, and separate Cashu payouts. LDK Server creates
the shared invoice; incoming Lightning funds stay on that node. Cashu payments
with cashu_settlement: None are redeemed into the configured mint's wallet and
may be paid out to a NUT-18 request. Direct Cashu settlement is described below.
The library does not convert existing LDK balances to Cashu or convert balances
as part of the payout API.
Both intents and payouts require IdempotencyKey, a UUID-backed type. Use
IdempotencyKey::generate() for a new operation, saved_text.parse::<IdempotencyKey>()?
to restore one, or IdempotencyKey::from(existing_uuid) to wrap a UUID. Keep
application order IDs in reference. UUIDs serialize as lowercase, hyphenated
strings; parsing a different UUID spelling does not create a different key.
Manual capture: one username, two buyers
Set CreateIntent.capture = CaptureMode::Manual. Give each buyer a separate
intent and stable key. A shared reference such as alice@xyz identifies the
application resource; it does not enforce exclusivity inside this library.
The compilable username example shows
an application-owned durable reservation and crash recovery.
- For Lightning, configure
lightningnormally. Its L402/x402 offers use a hold invoice. For Cashu, configurecashu_settlementwithLdk(node)and callauthorize_cashu(intent_id, token). The original proofs are melted without a preliminary swap; the source mint holds them pending. - Call
authorizations()to read funded holds in durable observation order. AnAuthorizationis not a receipt.sequencedefines which arrival this engine recorded first; simultaneous payments are ordered by the store. - Atomically reserve the username in your application database, recording
the winning intent and authorization IDs. Then call
claim(authorization.id). - Assign the username only after receiving a receipt or observing
Paid. Callreject(losing_intent.id)for the other buyer.
claim() durably selects one authorization per intent before revealing its
preimage. Repeating that ID is safe. Unavailable or task cancellation can mean
that funds were already claimed: retain the application reservation and retry
that same ID or run recover(). The intent stays Claiming until the node proves
success or failure. Only Failed is evidence that the selected claim failed;
it is safe for the application to release that reservation. A Rejected intent
that never started a claim can also release its reservation. There is no automatic
switch to another buyer. Persist application reservations and reconcile them on
startup, including reservations made just before a crash preceding claim().
reject() is irreversible and cannot undo a claim decision. It also rejects
late arrivals. Rejected means the service will never claim that intent; it does
not mean the losing Cashu proofs are already spendable. Their source mint must
process the failed Lightning payment first. Continue recover() and inspect
pending_settlements() until the source reports the original proofs unspent.
Rejected submissions are never remelted. Return the original token to the buyer
if needed; no replacement token or swap is required. Each hold uses a fresh
preimage; duplicate payments to the same invoice are failed.
Manual capture supports directly paid Lightning and direct Cashu melts to an
LDK node under your control. Wallet-receive Cashu and trusted-mint destinations
are rejected in this mode because they do not expose a manual claim decision.
An L402/x402 payer cannot provide its preimage before the claim; the application
must process node authorizations independently of that payer's pending HTTP
request. Use submit_payment() for automatic intents and authorize_cashu()
for manual Cashu submissions.
The node macaroon needs Bolt11ReceiveForHash, SubscribeEvents,
GetPaymentDetails, ListPayments, Bolt11ClaimForId, Bolt11FailForId, and
GetNodeInfo access (node:read is required for deadline checks). Listeners
start before invoices are exposed and reconnect after disconnection. LDK's
claimable events are not replayed. A separate reconciler scans paginated node
records at startup, after reconnection, and every five seconds to discover pending
holds owned by this store. Each batch reads at most 32 pages; larger histories
resume from the saved cursor in the next batch and take longer to reconcile.
Recovered authorizations use observation order, not node arrival order. When an
event deadline is missing, checking the authorization persists one conservative
bound of the node's current height plus 2016 blocks (LDK's maximum incoming HTLC
lock). Repeated observations and restarts never extend it; a later event may
shorten it. Reconciliation never claims a hold automatically. Persisted decisions
recover across restarts. Claim deadlines are block heights; holds can expire
before an application decides. At or past the deadline, a still-pending node
record is treated as failed, excluding it from authorizations and resolving a
selected claim as Failed. An authenticated successful record takes precedence.
If the node height cannot be read, the result remains uncertain. Keep holds
short and run allocation/recovery promptly. close().await stops listeners
before releasing storage; dropping the engine also aborts listeners.
Intent status progresses through Authorized and Claiming to Paid, or to
Rejected/Failed. Undecided expired holds are rejected by recovery. Recovery
reports also expose authorized, claims_pending, and rejections_pending.
Direct Cashu settlement
Set cashu_settlement to melt the payer's original proofs directly to a trusted
destination, without first receiving/swapping them:
use satsettle::{CashuSettlement, Network, SettlementDestination};
let settlement = CashuSettlement {
destination: SettlementDestination::CashuMint {
mint: "https://your-trusted-mint.example".into(),
network: Network::Bitcoin,
},
max_fee_sats: 3,
};
// Or: destination: SettlementDestination::Ldk(your_ldk_config)
// Then set CreateIntent.cashu_settlement = Some(settlement).
See the direct settlement example
for creation, submission, recovery handling and change. It is a compilable handler
sketch; its main does not make payments. This mode does not require HTTP request
binding or enabling L402/x402.
| Destination | Completion condition |
|---|---|
Ldk(config) |
An authenticated ListPayments lookup finds a successful inbound payment with the invoice's hash and the full received amount. |
CashuMint { mint, network } |
The destination invoice is paid and CDK has verified and durably stored the newly issued tokens. A completed local issuance transaction is required. |
The source mint's PAID state or preimage alone never completes the intent. LDK
credentials need both Bolt11Receive and ListPayments permissions. Tokens
issued at the trusted mint become available through cashu_balance() and the
normal payout API.
With direct settlement enabled, cashu_mint: None accepts compatible HTTPS
source mints on port 443. Source wallets use CDK's standard mint connector and
HTTP transport with HTTP polling. Direct melts use CDK's skip_swap option to
spend the original proofs. cashu_mint: Some(url) restricts the source to that
configured mint; loopback HTTP is supported for development.
The library does not filter resolved IP addresses, pin DNS, or impose a response body size limit. Applications accepting payer-selected mint URLs must enforce network access restrictions through their deployment or an outbound proxy. The source must support BOLT11 melts of sat-denominated, unlocked Cashu v4 tokens. A reachable mint is not a guarantee of liquidity or payment.
The Cashu offer requests amount_sats + max_fee_sats. Before sending, the library
checks that the source's input fee plus Lightning fee reserve fits that budget
and that the token covers both fees and the full purchase amount. It never tops
up from another wallet. The cap limits the quoted fees; an untrusted mint can
still withhold change. Excess and unused fee reserve remain source-mint ecash.
submit_payment() returns a receipt only after destination confirmation.
An interrupted or uncertain attempt returns Error::Unavailable; an otherwise
unpaid intent reports IntentStatus::Settling. Inspect pending_settlements()
and call recover() at startup and periodically. Recovery retains the same
invoice, quote, and input proofs. It can finish from destination evidence even
when the source is offline. Pending melts are not resubmitted; a definitively
unpaid, compensated preparation may retry the same quote and proofs. Never
generate a replacement payment merely because a call timed out. An expired or
unresolvable attempt that may have sent remains pending for reconciliation.
Source proofs live in isolated per-attempt CDK wallets, never in the receiving
wallet's spendable balance. After settlement, settlement_change(receipt.id)
recovers and exports any available change as PaymentProof::Cashu. It returns
the same bearer token on retries, without another swap. Deduplicate its delivery
or redemption by receipt ID. None means no change is currently recovered;
later recovery may find it. Only newly derived change proofs are exported, never
the original payer proofs. There is no automatic refund or conversion of change.
This is a fresh storage format (schema 3). Start with a new data directory; earlier schemas are rejected, and no migration or legacy-key compatibility is provided. Existing data is never deleted automatically.
Mint-backed L402 receiving
Use LightningReceiver::CashuMint to accept automatic L402 payments into a
trusted Cashu wallet without running an LDK node:
use satsettle::{LightningReceiver, Network};
let receiver = LightningReceiver::CashuMint {
mint: "https://your-trusted-mint.example".into(),
network: Network::Bitcoin,
};
// Set LightningOptions.receiver = receiver and protocols = LightningProtocols::L402.
// Compute the request binding with LightningOptions::http_binding as usual.
See the mint L402 example for complete
configuration. LDK callers use LightningReceiver::Ldk(config) or config.into();
existing serialized LDK settings remain readable. A mint receiver requires
CaptureMode::Automatic and LightningProtocols::L402. Manual capture and x402
are rejected before creating a quote. Managed trusted wallets are the default;
with supplied wallets, configure a wallet for this mint explicitly.
The payer receives an invoice and a request-bound macaroon. Quote identifiers
remain private in the storage backend. A receipt requires a valid
macaroon:preimage credential and verified ecash issuance committed in the
receiving wallet. A mint's PAID or ISSUED response is insufficient. Retry the
same intent and proof after Unavailable; the quote and receipt are idempotent.
recover() can collect a paid quote to safeguard funds even when the payer has
not authenticated. It does not mark that intent paid or issue a receipt until a
valid payer credential has been verified. If verification succeeded before an
interrupted collection, recovery can finish that receipt after a restart.
Collected ecash is available through cashu_balance() and the payout API.
The payer must obtain the Lightning preimage. A Lightning wallet normally returns it after paying, but a Cashu mint used to melt tokens may not expose it. Such a payer can pay the invoice yet remain unable to authenticate with L402. Use the direct Cashu settlement flow for Cashu payers without preimage support. Changing the receiver to LDK does not fix this payer-side limitation, and mint payment status never substitutes for credentials.
Quote creation continues to its durable checkpoint after caller cancellation.
After a crash, recovery can bind the original quote already stored by CDK. If a
quote response never reached durable local storage, its identity is unknown:
Satsettle returns Unavailable and does not create a replacement invoice. An
unbound request blocks new quote creation at that mint until it is reconciled;
retain both the coordination records and the original wallet for recovery.
Automatic payment semantics
- A caller idempotency key plus immutable terms creates one intent. Repeating
those terms returns the original offers; changing them returns
Conflict. The intent ID is a deterministic SHA256 of the canonical UUID string, not an authorization secret. - Both Lightning protocols prove the same payment hash. Submitting either or both returns the same receipt. The first accepted proof determines its method. The invoice must be the exact invoice issued for this intent, even if another intent has the same price and request binding.
- Cashu tokens are bearer payments. The library binds a canonical token identity to an intent before redemption or melting and rejects its use for another intent. The Cashu payment-request ID is informational, not a cryptographic token binding.
- A distinct second payment produces a receipt with
additional = true. Refunds or credits are an application decision. Additional receipts do not mean the application should fulfill the intent again. Receipt.applied_satsis the amount applied to the purchase, not a wallet accounting statement. Cashu prices are net of fees. Wallet-receive mode retains excess value. Direct settlement exposes source change separately. Neither mode automatically refunds excess or accumulates partial payments.- Intent expiry stops new Cashu submissions and new offers. A Cashu receive or direct settlement journaled before expiry can still recover. Already-issued Lightning invoices retain their own protocol expiry and 60-second grace, so a payer can submit a valid proof even after the application deadline. The application must decide how to fulfill such a late-paid order.
- Protocol proofs must still validate when resubmitted. Once an L402 credential
or invoice expires, read durable paid state with
status()instead. A paid intent never becomes unpaid merely because its offer expired.
Payment, fulfillment, and payout are separate. The library does not guarantee
exactly-once application side effects. Use intent.id as the deduplication key
in an application transaction or transactional outbox. Authorize every API call
to the intent and keep payment material out of backend logs.
Recovery and payouts
Cashu submission journals retain the token privately before contacting the mint.
CDK performs its own wallet recovery; the intent journal then records the receipt.
Retrying after a crash between those commits does not redeem or credit twice.
Run recover() at startup and periodically. Invalid pending tokens are discarded;
temporary mint failures remain pending. New Cashu submissions are limited to
1,024 unresolved attempts globally and eight per intent, atomically in storage;
existing-token retries remain available at capacity. Admission returns
Error::Unavailable when full. Cleanup still in progress consumes capacity.
Each recovery call rotates through at most eight Cashu submissions with a shared
30-second budget; its counts describe the work attempted in that call. The cursor
survives restarts, so an offline early attempt cannot monopolize recovery. Unknown
outcomes are never evicted to make room. Recovery also prunes expired L402 keys.
A rejection before any melt starts durably schedules cleanup of the settlement, destination ownership, and isolated source wallet, including its seed and SQLite files. Interrupted cleanup resumes during recovery. Observed manual holds must be rejected before their records are removed. Started melts and collected change retain their recovery state. A rejected attempt targeting a Cashu mint also retains its bounded cleanup reservation while the destination quote is uncollected: the source has seen its invoice and could pay it later. Recovery collects such late funds without granting a payment receipt, then releases the reservation. Unpaid quotes are not evicted by age. Each receiving wallet additionally caps uncollected quotes at 1,024, including orphaned responses after a crash, so repeated interrupted quote creation cannot bypass admission limits. For automatic LDK intents, recovery does not discover paid invoices when the payer never submits a proof; LDK still owns those funds. Mint-backed L402 recovery safeguards paid ecash separately from payer authentication, as described above. Recovery does not run payout schedules.
payout(PayoutRequest) delivers to a Cashu payment request using the same wallet
lock as receives. Supply a stable idempotency key and a total fee cap. Successful
retries return Delivered without sending again. Delivery means a transport
acknowledgment, not proof that the destination redeemed the tokens.
Before a send, the library journals the payout request, then persists the CDK
operation ID before confirming. A crash, timeout, fee failure, or lost response
can leave the caller's key Unresolved. That key is deliberately never retried
by creating a fresh payment. Inspect pending_payouts(); use reclaim_payout()
only when you intend to invalidate the unredeemed tokens at the destination.
An unresolved caller key remains a conservative historical marker, even after
CDK has recovered or compensated the operation. After checking the outcome,
the application may explicitly choose a new payout key. Do not blindly generate
a new key after a failure.
Payouts support CDK's HTTP POST and encrypted Nostr NIP-17 request transports.
The application can implement threshold or interval scheduling by checking
cashu_balance() and supplying a distinct, persisted key for each intended send.
Caller-managed trusted wallets
Pass existing CDK wallets when the application owns their seed, database and configuration:
use satsettle::{Engine, EngineOptions};
use std::sync::Arc;
# async fn open(wallet: Arc<cdk::Wallet>) -> Result<Engine, satsettle::Error> {
let options = EngineOptions::default().with_trusted_wallets([wallet]);
let engine = Engine::open_with_options(".state/payments", options).await?;
# Ok(engine)
# }
Each wallet routes its own mint URL and must use sats. Duplicate mints and
unconfigured trusted mints are errors; there is no fallback to a new managed
wallet. Supply one wallet per trusted mint, or an empty list to disable managed
trusted wallets. Receiving, trusted-mint settlement, balance and payout APIs all
use this routing. Engine::open retains the managed-wallet default.
Satsettle still persists its coordination and receive-evidence journals, but creates no seed or wallet database for a supplied wallet. Reopen with the same mapping, durable wallet state and seed, and back up both the caller's wallet storage and Satsettle's directory together. Existing managed wallet directories cannot be switched to unrelated supplied wallets; this API is not a migration.
Keep mutation and recovery exclusive to this Engine for its entire lifetime.
An Arc<Wallet> does not make external CDK operations or another Engine safe to
run concurrently. Caller-owned databases, seeds and permissions are the caller's
responsibility. Direct-settlement source wallets remain isolated; these trusted
routes never receive arbitrary source proofs.
Caller-managed source wallets
EngineOptions::with_source_wallet_provider(Arc<dyn SourceWalletProvider>)
delegates direct-settlement wallet construction to the caller. The asynchronous
wallet() method receives a SourceWalletRequest with an opaque stable attempt
ID, source mint, and unit, and returns an Arc<cdk::Wallet>. Retries, recovery and
change export use the same attempt ID. The default remains a persistent SQLite
wallet with a separate seed for every attempt.
A provider must resolve each attempt to its own durable database namespace and
seed across restarts. Neither a new Arc nor another connection to the same
database proves isolation. The Engine checks mint/unit and rejects known reuse
of a wallet database handle across attempts or with a supplied trusted wallet;
the provider must prevent aliases through other handles or processes. Keep
mutation/recovery exclusive to the Engine and retain the state while melts,
original-proof releases, or change recovery remain unresolved. Satsettle does
not directly delete provider storage or create a fallback source database.
Implement the required asynchronous discard() hook to remove rejected pre-melt
attempts, including their seed and database namespace. It must be idempotent and
return success for missing state; failed cleanup stays pending and counts against
capacity. Wallet setup and cleanup calls are serialized and survive cancellation.
An in-memory wallet alone cannot recover a crash safely.
Source URL validation still runs before provider lookup: unconfigured source
mints require HTTPS on port 443. Deployment egress restrictions must block
internal services, and any caller-configured connector must preserve that
policy and its redirect restrictions. The default uses CDK's standard HTTP
client; Satsettle does not add a custom transport. Provider injection retains
the direct skip_swap melt, durable rejection decisions, original-proof
release, and separation from trusted balances and payouts.
Caller-managed coordination storage
EngineOptions::with_store(Arc<dyn storage::PaymentStore>) supplies all payment
coordination state. Engine::open(directory) retains the embedded redb default;
storage::RedbStore::open(directory) also exposes that implementation explicitly.
A supplied backend owns intents, receipts, authorizations, decisions, settlement
and payout records, per-mint CashuJournals and the LightningJournal containing
L402 keys and replay barriers, and private mint-invoice checkpoints. The Engine
opens no fallback redb database.
CDK wallet databases and seeds remain separate: configure trusted wallets and a
source-wallet provider as needed, or retain the managed SQLite wallets.
Each storage method is an atomic domain operation. Success must mean durable commit, including related indexes. A cancelled caller can lose an acknowledgment; subsequent reads must wait for its write to finish, and retries must preserve immutable terms, receipt ownership and capture decisions. Store records include bearer tokens, preimages and signing keys: encrypt or restrict access as appropriate and back them up with the wallets. Preserve backend state and routing on restart.
An application backend may commit its resource reservation and the equivalent of
decide_claim(authorization_id) in one database transaction. It can then call
Engine::claim with that authorization to perform or recover the network effects.
The durable decision must be visible through the backend before any claim RPC.
A database transaction cannot include a Lightning RPC or a mint payment atomically;
keep reservation fulfillment idempotent and recover uncertain network outcomes.
Sharing a database does not authorize concurrent Engines or external wallet use.
The application owns database configuration, migrations and worker coordination. Satsettle ships the embedded adapter and storage traits; it does not provide a PostgreSQL adapter, leases or automatic failover. Keep one Engine owner for each store and its wallets. Before transferring ownership, stop the old owner and ensure its in-flight wallet operations have finished. Distributed failover needs application-provided fencing that covers wallet writes as well as journal writes; a shared database or an expiring lease alone does not provide that guarantee.
Backend implementers can enable the storage-contract-tests Cargo feature and
call storage::contracts::run(factory). The async factory receives a namespace;
start each namespace empty and reopen the same durable state on repeated calls.
The suite checks concurrent receipt and decision ownership, immutable terms,
settlement revisions, uncertain payouts, receipt evidence, credentials and restart
recovery. Add backend-specific crash, cancellation and transaction isolation tests.
The embedded adapter retains its existing layout and adds a versioned table for
mint-invoice checkpoints. Custom backends must implement the mint-invoice CAS
and receipt prerequisites as well as the existing storage contracts.
Storage and deployment
One Engine owns a backend and its wallets; share it using Arc. With the embedded
backend, opening the same data directory twice fails.
Use a local filesystem with reliable locking and fsync. Application operations are serialized
in v0.1, including network I/O; event listeners persist observations concurrently.
Put bounded concurrency, timeouts and rate limits
around the public boundary. Dropping an in-flight future does not undo payments.
The store uses redb with immediate durability and versioned tables. Managed CDK wallets use SQLite. Managed directories are mode 0700 and files 0600; unsafe existing permissions, symlinks, and missing wallet seeds fail closed. Back up the entire directory while the engine is stopped, including wallet seeds, SQLite state, L402 root keys, hold preimages, authorizations, capture decisions, pending token material, intent receipts, and payout journals. Do not delete journals to reset a payment. v0.1 does not prune intents or receipts or automatically migrate Geata data; monitor disk use and start with a new directory.
Errors and public Debug implementations omit bearer material. Applications
must also suppress secret-bearing dependency logs, particularly CDK wallet and
macaroon debug logs. Do not enable blanket dependency trace logging on payment
services. A private directory is access control, not encryption at rest.
Supported scope
- Rust 1.95.0, edition 2024; Linux x86_64 and aarch64 Nix outputs. The Linux host platform is tested in CI. Other platforms are not promised in this version.
- Cashu sats from one configured mint per intent, using CDK 0.18.0. V4
cashuBtokens need DLEQ proofs and no spending conditions; 16 KiB maximum. - L402 with request-bound V2 macaroons and LDK Server invoices on mainnet,
testnet3, signet, and regtest. Manual capture supports all four networks.
X402andBothare rejected for signet/regtest before issuing an invoice. - x402 v2
exact/lnbtcover BOLT11, mainnet or testnet3. This is not general EVM/USDC x402 support. The HTTP binding helper retains Geata'shttp:1orgeata:http:2profile; clients need to support the selected profile. These binding codecs do not install HTTP handlers. - LDK Server client pinned to
21a903ccb89496285c58a55ffdce42a9f0353a05, with authenticated TLS and method-specific macaroon permissions described above. - Automatic mint-backed L402 receiving through trusted Cashu wallets. The payer must supply a valid preimage; manual capture and x402 require LDK.
See DEVELOPMENT.md for Nix, Cargo, tests and architecture, and NOTICE.md for the Geata source attribution.