Claiming on-chain deposits
On-chain funds do not have to wait for 3 confirmations. With instant and expedited claims, a deposit can reach the wallet's balance while it is still in the mempool, or a block or two after it confirms. The SDK detects a deposit as soon as it is in the mempool, follows it through its confirmations, and claims it on its own as soon as the cost of doing so fits the fee limits you set.
A claim comes in three speeds. For the two faster ones the Spark Service Provider fronts the funds and charges an instant claim fee for doing so. The standard claim costs an ordinary on-chain fee.
| Claim | When funds arrive | Cost | Claimed automatically when |
|---|---|---|---|
| Instant | At 0 confirmations, while the deposit is still in the mempool. Offered for some deposits only. | Instant claim fee | The instant claim fee fits the deposit's max fee |
| Expedited | At 1 or 2 confirmations | Instant claim fee | The instant claim fee fits the deposit's max fee |
| Standard | At 3 confirmations on mainnet (1 on regtest) | On-chain fee only | The on-chain fee fits the deposit's max fee |
A deposit's max fee is the configured max claim fee, unless the deposit has been given a max fee of its own. Its own max fee then governs the instant and expedited claims, and the standard claim runs under the larger of the two.
What each speed would cost for a particular deposit can be checked before claiming it, by fetching a fee quote.
If the deposit's max fee is too low for any of the three, the deposit is not claimed automatically and should be claimed manually.
Developer note
The SDK attempts an instant or expedited claim whenever the provider offers one, and takes it when its fee fits the max claim fee. The default max claim fee of 1 sat/vbyte (about 99 sats) is below any instant claim fee, so with the default setting deposits are credited by the standard claim. Raise it to have deposits credited early.Setting a max claim fee
The max claim fee in the SDK configuration is the most the SDK will pay when it claims a deposit on its own. It takes the form of an absolute amount in sats, a rate in sats/vbyte, or the fastest recommended fee with a leeway, as described on the configuration page.
The max claim fee is one dial for two things. It caps the on-chain fee of a standard claim, and it caps the instant claim fee the provider may take for an instant or expedited claim. The value you choose therefore decides both how much on-chain fee the SDK will pay and whether deposits are claimed early at all.
To make automatic claims more likely, set the max claim fee to the fastest recommended rate at the time of the claim. This can result in higher fees.
// Create the default config
let mut config = default_config(Network::Mainnet);
config.api_key = Some("<breez api key>".to_string());
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config.max_deposit_claim_fee = Some(MaxFee::NetworkRecommended {
leeway_sat_per_vbyte: 1,
});
// Create the default config
var config = defaultConfig(network: Network.mainnet)
config.apiKey = "<breez api key>"
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config.maxDepositClaimFee = MaxFee.networkRecommended(leewaySatPerVbyte: 1)
// Create the default config
val config = defaultConfig(Network.MAINNET)
config.apiKey = "<breez api key>"
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config.maxDepositClaimFee = MaxFee.NetworkRecommended(leewaySatPerVbyte = 1u)
// Create the default config
var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with
{
apiKey = "<breez api key>"
};
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config = config with { maxDepositClaimFee = new MaxFee.NetworkRecommended(leewaySatPerVbyte: 1) };
// Create the default config
const config = defaultConfig('mainnet')
config.apiKey = '<breez api key>'
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config.maxDepositClaimFee = { type: 'networkRecommended', leewaySatPerVbyte: 1 }
// Create the default config
const config = defaultConfig(Network.Mainnet)
config.apiKey = '<breez api key>'
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config.maxDepositClaimFee = new MaxFee.NetworkRecommended({ leewaySatPerVbyte: BigInt(1) })
// Create the default config
var config = defaultConfig(network: Network.mainnet);
config = config.copyWith(apiKey: "<breez api key>");
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
config = config.copyWith(
maxDepositClaimFee:
MaxFee.networkRecommended(leewaySatPerVbyte: BigInt.from(1)));
# Create the default config
config = default_config(network=Network.MAINNET)
config.api_key = "<breez api key>"
# Set the maximum fee to the fastest network recommended fee at the time of claim
# with a leeway of 1 sats/vbyte
config.max_deposit_claim_fee = MaxFee.NETWORK_RECOMMENDED(leeway_sat_per_vbyte=1)
// Create the default config
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
apiKey := "<breez api key>"
config.ApiKey = &apiKey
// Set the maximum fee to the fastest network recommended fee at the time of claim
// with a leeway of 1 sats/vbyte
networkRecommendedInterface := breez_sdk_spark.MaxFee(
breez_sdk_spark.MaxFeeNetworkRecommended{LeewaySatPerVbyte: 1},
)
config.MaxDepositClaimFee = &networkRecommendedInterface
Even with a high max claim fee the SDK might still fail to claim a deposit on its own. When that happens it emits SdkEvent::UnclaimedDepositsSdkEvent.UNCLAIMED_DEPOSITSSdkEvent.unclaimedDepositsSdkEvent.UnclaimedDepositsSdkEvent.UnclaimedDepositsSdkEvent.UnclaimedDepositsSdkEvent.UnclaimedDepositsSdkEventUnclaimedDepositsSdkEvent.UnclaimedDeposits with the deposit's details, and the recommended approach is to claim it manually once the user has accepted the required fee. See Listening to events for how to subscribe.
Giving one deposit its own max fee
A max_feemax_feemaxFeemaxFeemaxFeemaxFeemaxFeeMaxFeeMaxFee passed to claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposit is recorded on that deposit and carried into the SDK's later automatic attempts on it. That is how one deposit is treated differently from the rest without changing the configuration for all of them.
- Raising it above the instant claim fee lets that single deposit be claimed early. The SDK keeps applying it on later sync passes, so the app does not have to keep calling
claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposituntil the claim lands. - Lowering it below the instant claim fee holds that one deposit back from an instant or expedited claim. It waits for the standard claim while the others keep claiming early under the configured max claim fee.
The standard claim runs under whichever is larger, the deposit's own max fee or the configured one. A raised max fee therefore applies to the standard claim too, should the early claim never happen, so raise it to what you are willing to pay for the deposit, not only for the early claim. A lowered one restricts only the early claim.
A few more rules to keep in mind:
- Calling
claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDepositwithout amax_feemax_feemaxFeemaxFeemaxFeemaxFeemaxFeeMaxFeeMaxFeeclaims under the configured max claim fee and clears any max fee standing on the deposit. - The max fee is recorded before the claim is attempted, so it stands whatever the attempt reports. What the attempt reports is covered under Handling claim outcomes.
- The recorded value is readable as
max_claim_feemax_claim_feemaxClaimFeemaxClaimFeemaxClaimFeemaxClaimFeemaxClaimFeeMaxClaimFeeMaxClaimFeeon each deposit fromlist_unclaimed_depositslist_unclaimed_depositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositsListUnclaimedDepositsListUnclaimedDeposits, unset while the configured max claim fee applies. - Deposits are not part of the synced wallet records, so a max fee set on one device stays on that device. The same deposit seen from another device runs under whatever that device has configured.
Seeing deposits before they confirm
A deposit is visible in the SDK from the moment it reaches the mempool, so an app can show it to the user, or claim it, before the first confirmation. The Spark operators only report a deposit once it has a confirmation, so the SDK also asks its chain service about the deposit addresses it has handed out. A deposit found this way arrives through SdkEvent::NewDepositsSdkEvent.NEW_DEPOSITSSdkEvent.newDepositsSdkEvent.NewDepositsSdkEvent.NewDepositsSdkEvent.NewDepositsSdkEvent.NewDepositsSdkEventNewDepositsSdkEvent.NewDeposits and appears in list_unclaimed_depositslist_unclaimed_depositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositsListUnclaimedDepositsListUnclaimedDeposits with is_matureis_matureisMatureisMatureisMatureisMatureisMatureIsMatureIsMature false, whether or not the SDK goes on to claim it automatically.
Each watched address costs one chain-service request per sync. Requesting a receive address starts a 24-hour window on it, and requesting it again restarts that window, so a wallet that is not expecting an on-chain payment settles at no requests at all. An address that has taken a deposit keeps being watched past its window until that deposit confirms.
Listing unclaimed deposits
list_unclaimed_depositslist_unclaimed_depositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositsListUnclaimedDepositsListUnclaimedDeposits returns every deposit the SDK is tracking:
- Pending deposits that have not yet reached the standard claim depth (
is_matureis_matureisMatureisMatureisMatureisMatureisMatureIsMatureIsMatureis false). These are claimed automatically once they reach the standard claim depth, or sooner if the max claim fee covers an instant or expedited claim. - Deposits with enough confirmations whose claim failed, with the specific failure reason.
- Deposits already claimed whose output the provider has not yet spent.
let request = ListUnclaimedDepositsRequest {};
let response = sdk.list_unclaimed_deposits(request).await?;
for deposit in response.deposits {
info!("Unclaimed deposit: {}:{}", deposit.txid, deposit.vout);
info!("Amount: {} sats", deposit.amount_sats);
if let Some(claim_error) = &deposit.claim_error {
match claim_error {
DepositClaimError::MaxDepositClaimFeeExceeded {
max_fee,
required_fee_sats,
required_fee_rate_sat_per_vbyte,
..
} => {
info!(
"Max claim fee exceeded. Max: {:?}, Required: {} sats or {} sats/vByte",
max_fee, required_fee_sats, required_fee_rate_sat_per_vbyte
);
}
DepositClaimError::MissingUtxo { .. } => {
info!("UTXO not found when claiming deposit");
}
DepositClaimError::DepositTooSmall { .. } => {
info!("Deposit too small to claim");
}
DepositClaimError::Generic { message } => {
info!("Claim failed: {}", message);
}
}
}
}
let request = ListUnclaimedDepositsRequest()
let response = try await sdk.listUnclaimedDeposits(request: request)
for deposit in response.deposits {
print("Unclaimed deposit: \(deposit.txid):\(deposit.vout)")
print("Amount: \(deposit.amountSats) sats")
if let claimError = deposit.claimError {
switch claimError {
case .maxDepositClaimFeeExceeded(
let tx, let vout, let maxFee, let requiredFeeSats, let requiredFeeRateSatPerVbyte):
let maxFeeStr: String
if let maxFee = maxFee {
switch maxFee {
case .fixed(let amount):
maxFeeStr = "\(amount) sats"
case .rate(let satPerVbyte):
maxFeeStr = "\(satPerVbyte) sats/vByte"
}
} else {
maxFeeStr = "none"
}
print(
"Max claim fee exceeded. Max: \(maxFeeStr), "
+ "Required: \(requiredFeeSats) sats or "
+ "\(requiredFeeRateSatPerVbyte) sats/vByte"
)
case .missingUtxo(let tx, let vout):
print("UTXO not found when claiming deposit")
case .depositTooSmall(let tx, let vout):
print("Deposit too small to claim")
case .generic(let message):
print("Claim failed: \(message)")
}
}
}
try {
val request = ListUnclaimedDepositsRequest
val response = sdk.listUnclaimedDeposits(request)
for (deposit in response.deposits) {
// Log.v("Breez", "Unclaimed deposit: ${deposit.txid}:${deposit.vout}")
// Log.v("Breez", "Amount: ${deposit.amountSats} sats")
deposit.claimError?.let { claimError ->
when (claimError) {
is DepositClaimError.MaxDepositClaimFeeExceeded -> {
val maxFee = claimError.maxFee
val maxFeeStr = when (maxFee) {
is Fee.Fixed -> "${maxFee.amount} sats"
is Fee.Rate -> "${maxFee.satPerVbyte} sats/vByte"
null -> "none"
}
// Log.v("Breez", "Max claim fee exceeded. Max: $maxFeeStr,
// Required: ${claimError.requiredFeeSats} sats or
// ${claimError.requiredFeeRateSatPerVbyte} sats/vByte")
}
is DepositClaimError.MissingUtxo -> {
// Log.v("Breez", "UTXO not found when claiming deposit")
}
is DepositClaimError.DepositTooSmall -> {
// Log.v("Breez", "Deposit too small to claim")
}
is DepositClaimError.Generic -> {
// Log.v("Breez", "Claim failed: ${claimError.message}")
}
}
}
}
} catch (e: Exception) {
// handle error
}
var request = new ListUnclaimedDepositsRequest();
var response = await sdk.ListUnclaimedDeposits(request: request);
foreach (var deposit in response.deposits)
{
Console.WriteLine($"Unclaimed deposit: {deposit.txid}:{deposit.vout}");
Console.WriteLine($"Amount: {deposit.amountSats} sats");
if (deposit.claimError != null)
{
if (deposit.claimError is DepositClaimError.MaxDepositClaimFeeExceeded exceeded)
{
var maxFeeStr = "none";
if (exceeded.maxFee != null)
{
if (exceeded.maxFee is Fee.Fixed fixedFee)
{
maxFeeStr = $"{fixedFee.amount} sats";
}
else if (exceeded.maxFee is Fee.Rate rateFee)
{
maxFeeStr = $"{rateFee.satPerVbyte} sats/vByte";
}
}
Console.WriteLine($"Claim failed: Fee exceeded. Max: {maxFeeStr}, " +
$"Required: {exceeded.requiredFeeSats} sats or " +
$"{exceeded.requiredFeeRateSatPerVbyte} sats/vByte");
}
else if (deposit.claimError is DepositClaimError.MissingUtxo)
{
Console.WriteLine("Claim failed: UTXO not found");
}
else if (deposit.claimError is DepositClaimError.DepositTooSmall)
{
Console.WriteLine("Claim failed: deposit too small to claim");
}
else if (deposit.claimError is DepositClaimError.Generic generic)
{
Console.WriteLine($"Claim failed: {generic.message}");
}
}
}
const request: ListUnclaimedDepositsRequest = {}
const response = await sdk.listUnclaimedDeposits(request)
for (const deposit of response.deposits) {
console.log(`Unclaimed deposit: ${deposit.txid}:${deposit.vout}`)
console.log(`Amount: ${deposit.amountSats} sats`)
if (deposit.claimError != null) {
switch (deposit.claimError.type) {
case 'maxDepositClaimFeeExceeded': {
let maxFeeStr = 'none'
if (deposit.claimError.maxFee != null) {
if (deposit.claimError.maxFee.type === 'fixed') {
maxFeeStr = `${deposit.claimError.maxFee.amount} sats`
} else if (deposit.claimError.maxFee.type === 'rate') {
maxFeeStr = `${deposit.claimError.maxFee.satPerVbyte} sats/vByte`
}
}
console.log(
`Max claim fee exceeded. Max: ${maxFeeStr}, ` +
`Required: ${deposit.claimError.requiredFeeSats} sats or ` +
`${deposit.claimError.requiredFeeRateSatPerVbyte} sats/vByte`
)
break
}
case 'missingUtxo':
console.log('UTXO not found when claiming deposit')
break
case 'depositTooSmall':
console.log('Deposit too small to claim')
break
case 'generic':
console.log(`Claim failed: ${deposit.claimError.message}`)
break
}
}
}
const request: ListUnclaimedDepositsRequest = {}
const response = await sdk.listUnclaimedDeposits(request)
for (const deposit of response.deposits) {
console.log(`Unclaimed deposit: ${deposit.txid}:${deposit.vout}`)
console.log(`Amount: ${deposit.amountSats} sats`)
if (deposit.claimError != null) {
if (deposit.claimError?.tag === DepositClaimError_Tags.MaxDepositClaimFeeExceeded) {
let maxFeeStr = 'none'
if (deposit.claimError.inner.maxFee != null) {
if (deposit.claimError.inner.maxFee.tag === Fee_Tags.Fixed) {
maxFeeStr = `${deposit.claimError.inner.maxFee.inner.amount} sats`
} else if (deposit.claimError.inner.maxFee.tag === Fee_Tags.Rate) {
maxFeeStr = `${deposit.claimError.inner.maxFee.inner.satPerVbyte} sats/vByte`
}
}
console.log(
`Max claim fee exceeded. Max: ${maxFeeStr},
Required: ${deposit.claimError.inner.requiredFeeSats} sats
or ${deposit.claimError.inner.requiredFeeRateSatPerVbyte} sats/vByte`
)
} else if (deposit.claimError?.tag === DepositClaimError_Tags.MissingUtxo) {
console.log('UTXO not found when claiming deposit')
} else if (deposit.claimError?.tag === DepositClaimError_Tags.DepositTooSmall) {
console.log('Deposit too small to claim')
} else if (deposit.claimError?.tag === DepositClaimError_Tags.Generic) {
console.log(`Claim failed: ${deposit.claimError.inner.message}`)
}
}
}
final request = ListUnclaimedDepositsRequest();
final response = await sdk.listUnclaimedDeposits(request: request);
for (DepositInfo deposit in response.deposits) {
print("Unclaimed deposit: ${deposit.txid}:${deposit.vout}");
print("Amount: ${deposit.amountSats} sats");
final claimError = deposit.claimError;
if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) {
final maxFeeStr = claimError.maxFee != null
? (claimError.maxFee is Fee_Fixed
? '${(claimError.maxFee as Fee_Fixed).amount} sats'
: '${(claimError.maxFee as Fee_Rate).satPerVbyte} sats/vByte')
: 'none';
print("Max claim fee exceeded. Max: $maxFeeStr, "
"Required: ${claimError.requiredFeeSats} sats or "
"${claimError.requiredFeeRateSatPerVbyte} sats/vByte");
} else if (claimError is DepositClaimError_MissingUtxo) {
print("UTXO not found when claiming deposit");
} else if (claimError is DepositClaimError_DepositTooSmall) {
print("Deposit too small to claim");
} else if (claimError is DepositClaimError_Generic) {
print("Claim failed: ${claimError.message}");
}
}
try:
request = ListUnclaimedDepositsRequest()
response = await sdk.list_unclaimed_deposits(request=request)
for deposit in response.deposits:
logging.info(f"Unclaimed deposit: {deposit.txid}:{deposit.vout}")
logging.info(f"Amount: {deposit.amount_sats} sats")
if deposit.claim_error:
if isinstance(
deposit.claim_error, DepositClaimError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDED
):
max_fee_str = "none"
if deposit.claim_error.max_fee is not None:
if isinstance(deposit.claim_error.max_fee, Fee.FIXED):
max_fee_str = f"{deposit.claim_error.max_fee.amount} sats"
elif isinstance(deposit.claim_error.max_fee, Fee.RATE):
max_fee_str = f"{deposit.claim_error.max_fee.sat_per_vbyte} sats/vByte"
logging.info(
f"Claim failed: Fee exceeded. Max: {max_fee_str}, "
f"Required: {deposit.claim_error.required_fee_sats} sats "
f"or {deposit.claim_error.required_fee_rate_sat_per_vbyte} sats/vByte"
)
elif isinstance(deposit.claim_error, DepositClaimError.MISSING_UTXO):
logging.info("Claim failed: UTXO not found")
elif isinstance(deposit.claim_error, DepositClaimError.DEPOSIT_TOO_SMALL):
logging.info("Claim failed: deposit too small to claim")
elif isinstance(deposit.claim_error, DepositClaimError.GENERIC):
logging.info(f"Claim failed: {deposit.claim_error.message}")
except Exception as error:
logging.error(error)
raise
request := breez_sdk_spark.ListUnclaimedDepositsRequest{}
response, err := sdk.ListUnclaimedDeposits(request)
if err != nil {
var sdkErr *breez_sdk_spark.SdkError
if errors.As(err, &sdkErr) {
// Handle SdkError - can inspect specific variants if needed
// e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc.
}
return err
}
for _, deposit := range response.Deposits {
log.Printf("Unclaimed Deposit: %v:%v", deposit.Txid, deposit.Vout)
log.Printf("Amount: %v sats", deposit.AmountSats)
if claimErr := *deposit.ClaimError; claimErr != nil {
switch claimErr := claimErr.(type) {
case breez_sdk_spark.DepositClaimErrorMaxDepositClaimFeeExceeded:
maxFeeStr := "none"
if claimErr.MaxFee != nil {
switch fee := (*claimErr.MaxFee).(type) {
case breez_sdk_spark.FeeFixed:
maxFeeStr = fmt.Sprintf("%v sats", fee.Amount)
case breez_sdk_spark.FeeRate:
maxFeeStr = fmt.Sprintf("%v sats/vByte", fee.SatPerVbyte)
}
}
log.Printf(
"Max claim fee exceeded. Max: %v, Required: %v sats or %v sats/vByte",
maxFeeStr,
claimErr.RequiredFeeSats,
claimErr.RequiredFeeRateSatPerVbyte,
)
case breez_sdk_spark.DepositClaimErrorMissingUtxo:
log.Print("UTXO not found when claiming deposit")
case breez_sdk_spark.DepositClaimErrorDepositTooSmall:
log.Print("Deposit too small to claim")
case breez_sdk_spark.DepositClaimErrorGeneric:
log.Printf("Claim failed: %v", claimErr.Message)
}
}
}
Deposits that are already claimed
A deposit taken by an instant or expedited claim carries InstantClaimStatus::SubmittedInstantClaimStatus.SUBMITTEDInstantClaimStatus.submittedInstantClaimStatus.SubmittedInstantClaimStatus.SubmittedInstantClaimStatus.SubmittedInstantClaimStatus.SubmittedInstantClaimStatusSubmittedInstantClaimStatus.Submitted in its instant_claim_statusinstant_claim_statusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusInstantClaimStatusInstantClaimStatus while the claim settles, and InstantClaimStatus::ClaimedInstantClaimStatus.CLAIMEDInstantClaimStatus.claimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatusClaimedInstantClaimStatus.Claimed once the amount is credited. It stays in the list until the provider spends the deposit output, some time after the credit. Treat InstantClaimStatus::ClaimedInstantClaimStatus.CLAIMEDInstantClaimStatus.claimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatusClaimedInstantClaimStatus.Claimed as settled and branch on it rather than showing the deposit as awaiting action. When the SDK claims automatically it emits SdkEvent::ClaimedDepositsSdkEvent.CLAIMED_DEPOSITSSdkEvent.claimedDepositsSdkEvent.ClaimedDepositsSdkEvent.ClaimedDepositsSdkEvent.ClaimedDepositsSdkEvent.ClaimedDepositsSdkEventClaimedDepositsSdkEvent.ClaimedDeposits at submission, so a deposit can appear both in that event and in this list.
A deposit claimed elsewhere, by another instance sharing the wallet or on another device, reaches InstantClaimStatus::ClaimedInstantClaimStatus.CLAIMEDInstantClaimStatus.claimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatusClaimedInstantClaimStatus.Claimed the next time the SDK tries to claim it and the provider reports it as already claimed. No SdkEvent::ClaimedDepositsSdkEvent.CLAIMED_DEPOSITSSdkEvent.claimedDepositsSdkEvent.ClaimedDepositsSdkEvent.ClaimedDepositsSdkEvent.ClaimedDepositsSdkEvent.ClaimedDepositsSdkEventClaimedDepositsSdkEvent.ClaimedDeposits event is emitted, because the claim was not made here. The credit still arrives as a payment, so follow it through list_paymentslist_paymentslistPaymentslistPaymentslistPaymentslistPaymentslistPaymentsListPaymentsListPayments or the payment events.
Instant & expedited claims
A deposit does not have to wait for 3 confirmations. The Spark Service Provider fronts the credited amount as soon as it is willing to carry the risk, which brings a deposit to the balance while it is still in the mempool (an instant claim) or after 1 or 2 confirmations (an expedited claim). Both kinds are reported through the instantinstantinstantinstantinstantinstantinstantInstantInstant quote and instant_claim_statusinstant_claim_statusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusInstantClaimStatusInstantClaimStatus.
What it costs, and when it is offered:
- The provider charges the instant claim fee for fronting the funds. It is roughly the on-chain cost of the provider's own claim plus a percentage of the deposit, so it grows with the deposit.
- A claim at 0 confirmations is offered for some deposits only. Whether a deposit is fronted at all, and at which depth, is decided by the provider per deposit. It is not a setting you control.
- The SDK re-attempts the early claim as confirmations arrive, so a deposit the provider will not yet front in the mempool is often claimed expedited a block or two later.
The SDK claims early on its own whenever the provider offers an early claim and its fee fits the max claim fee, so the max claim fee decides whether deposits are credited early. To credit a single deposit early instead, call claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposit with a higher max_feemax_feemaxFeemaxFeemaxFeemaxFeemaxFeeMaxFeeMaxFee, as described under Giving one deposit its own max fee.
Claiming a deposit manually
When a deposit is not claimed automatically because the max claim fee is too low, claim it with claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposit and a higher max_feemax_feemaxFeemaxFeemaxFeemaxFeemaxFeeMaxFeeMaxFee. The recommended approach is to show the user the required fee and ask for approval before claiming.
Claiming a deposit that already has a claim returns SdkError::DepositClaimInProgressSdkError.DEPOSIT_CLAIM_IN_PROGRESSSdkError.depositClaimInProgressSdkError.DepositClaimInProgressSdkError.DepositClaimInProgressSdkError.DepositClaimInProgressSdkError.DepositClaimInProgressSdkErrorDepositClaimInProgressSdkError.DepositClaimInProgress. That covers a claim still running, from a background attempt or another call, and one that has already credited the deposit and is waiting for the provider to spend the output. Neither is a failure to show the user, and neither needs anything from you. Check instant_claim_statusinstant_claim_statusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusInstantClaimStatusInstantClaimStatus to tell them apart.
if let Some(DepositClaimError::MaxDepositClaimFeeExceeded {
required_fee_sats, ..
}) = &deposit.claim_error
{
// Show UI to user with the required fee and get approval
let user_approved = true; // Replace with actual user approval logic
if user_approved {
let request = ClaimDepositRequest {
txid: deposit.txid.clone(),
vout: deposit.vout,
max_fee: Some(MaxFee::Fixed {
amount: *required_fee_sats,
}),
};
sdk.claim_deposit(request).await?;
}
}
if case .maxDepositClaimFeeExceeded(_, _, _, let requiredFeeSats, _) = deposit.claimError {
// Show UI to user with the required fee and get approval
let userApproved = true // Replace with actual user approval logic
if userApproved {
let claimRequest = ClaimDepositRequest(
txid: deposit.txid,
vout: deposit.vout,
maxFee: MaxFee.fixed(amount: requiredFeeSats)
)
try await sdk.claimDeposit(request: claimRequest)
}
}
try {
val claimError = deposit.claimError
if (claimError is DepositClaimError.MaxDepositClaimFeeExceeded) {
val requiredFee = claimError.requiredFeeSats
// Show UI to user with the required fee and get approval
val userApproved = true // Replace with actual user approval logic
if (userApproved) {
val claimRequest = ClaimDepositRequest(
txid = deposit.txid,
vout = deposit.vout,
maxFee = MaxFee.Fixed(requiredFee)
)
sdk.claimDeposit(claimRequest)
}
}
} catch (e: Exception) {
// handle error
}
if (deposit.claimError is DepositClaimError.MaxDepositClaimFeeExceeded exceeded)
{
var requiredFee = exceeded.requiredFeeSats;
// Show UI to user with the required fee and get approval
var userApproved = true; // Replace with actual user approval logic
if (userApproved)
{
var claimRequest = new ClaimDepositRequest(
txid: deposit.txid,
vout: deposit.vout,
maxFee: new MaxFee.Fixed(amount: requiredFee)
);
await sdk.ClaimDeposit(request: claimRequest);
}
}
if (deposit.claimError?.type === 'maxDepositClaimFeeExceeded') {
const requiredFee = deposit.claimError.requiredFeeSats
// Show UI to user with the required fee and get approval
const userApproved = true // Replace with actual user approval logic
if (userApproved) {
const claimRequest: ClaimDepositRequest = {
txid: deposit.txid,
vout: deposit.vout,
maxFee: { type: 'fixed', amount: requiredFee }
}
await sdk.claimDeposit(claimRequest)
}
}
if (deposit.claimError?.tag === DepositClaimError_Tags.MaxDepositClaimFeeExceeded) {
const requiredFee = deposit.claimError.inner.requiredFeeSats
// Show UI to user with the required fee and get approval
const userApproved = true // Replace with actual user approval logic
if (userApproved) {
const claimRequest: ClaimDepositRequest = {
txid: deposit.txid,
vout: deposit.vout,
maxFee: new MaxFee.Fixed({ amount: requiredFee })
}
await sdk.claimDeposit(claimRequest)
}
}
final claimError = deposit.claimError;
if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) {
final requiredFee = claimError.requiredFeeSats;
// Show UI to user with the required fee and get approval
bool userApproved = true; // Replace with actual user approval logic
if (userApproved) {
final claimRequest = ClaimDepositRequest(
txid: deposit.txid,
vout: deposit.vout,
maxFee: MaxFee.fixed(amount: requiredFee),
);
await sdk.claimDeposit(request: claimRequest);
}
}
try:
if isinstance(
deposit.claim_error, DepositClaimError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDED
):
required_fee = deposit.claim_error.required_fee_sats
# Show UI to user with the required fee and get approval
user_approved = True # Replace with actual user approval logic
if user_approved:
claim_request = ClaimDepositRequest(
txid=deposit.txid,
vout=deposit.vout,
max_fee=Fee.FIXED(amount=required_fee),
)
await sdk.claim_deposit(request=claim_request)
except Exception as error:
logging.error(error)
raise
if claimErr := *deposit.ClaimError; claimErr != nil {
if exceeded, ok := claimErr.(breez_sdk_spark.DepositClaimErrorMaxDepositClaimFeeExceeded); ok {
requiredFee := exceeded.RequiredFeeSats
// Show UI to user with the required fee and get approval
userApproved := true // Replace with actual user approval logic
if userApproved {
maxFee := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeFixed{Amount: requiredFee})
claimRequest := breez_sdk_spark.ClaimDepositRequest{
Txid: deposit.Txid,
Vout: deposit.Vout,
MaxFee: &maxFee,
}
_, err := sdk.ClaimDeposit(claimRequest)
if err != nil {
var sdkErr *breez_sdk_spark.SdkError
if errors.As(err, &sdkErr) {
// Handle SdkError - can inspect specific variants if needed
// e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc.
}
return err
}
}
}
}
Writing your own claim logic
For advanced use cases you may want to write your own claim logic instead of relying on the SDK's automatic process. This gives you complete control over when and how deposits are claimed.
To disable automatic claims, unset the max claim fee. Then use the methods on this page to claim deposits manually according to your business logic. Common scenarios include:
- Dynamic fee adjustment: Adjust claiming fees based on market conditions or priority
- Conditional claiming: Only claim deposits that meet certain criteria (amount thresholds, time windows, etc.)
- Integration with external systems: Coordinate claims with other business processes
The recommended fees API is useful for determining appropriate fee levels for claiming deposits. For example, you can claim a deposit only if the required fee rate is less than the fastest recommended fee (or any other).
if let Some(DepositClaimError::MaxDepositClaimFeeExceeded {
required_fee_rate_sat_per_vbyte,
..
}) = &deposit.claim_error
{
let recommended_fees = sdk.recommended_fees().await?;
if *required_fee_rate_sat_per_vbyte <= recommended_fees.fastest_fee {
let request = ClaimDepositRequest {
txid: deposit.txid.clone(),
vout: deposit.vout,
max_fee: Some(MaxFee::Rate {
sat_per_vbyte: *required_fee_rate_sat_per_vbyte,
}),
};
sdk.claim_deposit(request).await?;
}
}
if case .maxDepositClaimFeeExceeded(_, _, _, _, let requiredFeeRateSatPerVbyte) =
deposit.claimError
{
let recommendedFees = try await sdk.recommendedFees()
if requiredFeeRateSatPerVbyte <= recommendedFees.fastestFee {
let claimRequest = ClaimDepositRequest(
txid: deposit.txid,
vout: deposit.vout,
maxFee: MaxFee.rate(satPerVbyte: requiredFeeRateSatPerVbyte)
)
try await sdk.claimDeposit(request: claimRequest)
}
}
try {
val claimError = deposit.claimError
if (claimError is DepositClaimError.MaxDepositClaimFeeExceeded) {
val requiredFeeRate = claimError.requiredFeeRateSatPerVbyte
val recommendedFees = sdk.recommendedFees()
if (requiredFeeRate <= recommendedFees.fastestFee) {
val claimRequest = ClaimDepositRequest(
txid = deposit.txid,
vout = deposit.vout,
maxFee = MaxFee.Rate(requiredFeeRate)
)
sdk.claimDeposit(claimRequest)
}
}
} catch (e: Exception) {
// handle error
}
if (deposit.claimError is DepositClaimError.MaxDepositClaimFeeExceeded exceeded)
{
var requiredFeeRate = exceeded.requiredFeeRateSatPerVbyte;
var recommendedFees = await sdk.RecommendedFees();
if (requiredFeeRate <= recommendedFees.fastestFee)
{
var claimRequest = new ClaimDepositRequest(
txid: deposit.txid,
vout: deposit.vout,
maxFee: new MaxFee.Rate(satPerVbyte: requiredFeeRate)
);
await sdk.ClaimDeposit(request: claimRequest);
}
}
if (deposit.claimError?.type === 'maxDepositClaimFeeExceeded') {
const requiredFeeRate = deposit.claimError.requiredFeeRateSatPerVbyte
const recommendedFees = await sdk.recommendedFees()
if (requiredFeeRate <= recommendedFees.fastestFee) {
const claimRequest: ClaimDepositRequest = {
txid: deposit.txid,
vout: deposit.vout,
maxFee: { type: 'rate', satPerVbyte: requiredFeeRate }
}
await sdk.claimDeposit(claimRequest)
}
}
if (deposit.claimError?.tag === DepositClaimError_Tags.MaxDepositClaimFeeExceeded) {
const requiredFeeRate = deposit.claimError.inner.requiredFeeRateSatPerVbyte
const recommendedFees = await sdk.recommendedFees()
if (requiredFeeRate <= recommendedFees.fastestFee) {
const claimRequest: ClaimDepositRequest = {
txid: deposit.txid,
vout: deposit.vout,
maxFee: new MaxFee.Rate({ satPerVbyte: requiredFeeRate })
}
await sdk.claimDeposit(claimRequest)
}
}
final claimError = deposit.claimError;
if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) {
final requiredFeeRate = claimError.requiredFeeRateSatPerVbyte;
final recommendedFees = await sdk.recommendedFees();
if (requiredFeeRate <= recommendedFees.fastestFee) {
final claimRequest = ClaimDepositRequest(
txid: deposit.txid,
vout: deposit.vout,
maxFee: MaxFee.rate(satPerVbyte: requiredFeeRate),
);
await sdk.claimDeposit(request: claimRequest);
}
}
try:
if isinstance(
deposit.claim_error, DepositClaimError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDED
):
required_fee_rate = deposit.claim_error.required_fee_rate_sat_per_vbyte
recommended_fees = await sdk.recommended_fees()
if required_fee_rate <= recommended_fees.fastest_fee:
claim_request = ClaimDepositRequest(
txid=deposit.txid,
vout=deposit.vout,
max_fee=MaxFee.RATE(sat_per_vbyte=required_fee_rate),
)
await sdk.claim_deposit(request=claim_request)
except Exception as error:
logging.error(error)
raise
if claimErr := *deposit.ClaimError; claimErr != nil {
if exceeded, ok := claimErr.(breez_sdk_spark.DepositClaimErrorMaxDepositClaimFeeExceeded); ok {
requiredFeeRate := exceeded.RequiredFeeRateSatPerVbyte
recommendedFees, err := sdk.RecommendedFees()
if err != nil {
return err
}
if requiredFeeRate <= recommendedFees.FastestFee {
maxFee := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeRate{SatPerVbyte: requiredFeeRate})
claimRequest := breez_sdk_spark.ClaimDepositRequest{
Txid: deposit.Txid,
Vout: deposit.Vout,
MaxFee: &maxFee,
}
_, err := sdk.ClaimDeposit(claimRequest)
if err != nil {
var sdkErr *breez_sdk_spark.SdkError
if errors.As(err, &sdkErr) {
// Handle SdkError - can inspect specific variants if needed
// e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc.
}
return err
}
}
}
}
Pricing a claim with fee quotes
fetch_claim_deposit_quotefetch_claim_deposit_quotefetchClaimDepositQuotefetchClaimDepositQuotefetchClaimDepositQuotefetchClaimDepositQuotefetchClaimDepositQuoteFetchClaimDepositQuoteFetchClaimDepositQuote prices both ways of claiming a deposit, so an app can offer the choice rather than deciding for the user. It returns the deposit's current confirmationsconfirmationsconfirmationsconfirmationsconfirmationsconfirmationsconfirmationsConfirmationsConfirmations alongside two quotes: instantinstantinstantinstantinstantinstantinstantInstantInstant for the instant or expedited claim, and maturematurematurematurematurematurematureMatureMature for the standard claim. Each quote carries the fee and confirmations_requiredconfirmations_requiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredConfirmationsRequiredConfirmationsRequired.
Reading the quotes:
confirmations_requiredconfirmations_requiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredConfirmationsRequiredConfirmationsRequiredis the depth the deposit becomes claimable at, not a count of blocks still to wait. Subtract the deposit's current confirmations to get the wait: an early claim claimable at 1 confirmation, on a deposit with 0, is available a block from now.- On the
instantinstantinstantinstantinstantinstantinstantInstantInstantquote,confirmations_requiredconfirmations_requiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredconfirmationsRequiredConfirmationsRequiredConfirmationsRequiredalso tells instant from expedited: 0 is an instant claim, 1 or 2 is an expedited one. - The
instantinstantinstantinstantinstantinstantinstantInstantInstantquote is absent when the provider will not front this particular deposit. It is also absent when claiming early would not actually be earlier: once the deposit has reached the standard claim depth, or when the provider would only credit at that same depth, waiting is both cheaper and no slower, so there is no choice left to offer. It is absent, too, when the provider could not be reached for a quote, which is the one case worth retrying. An absent quote means no early claim is offered for this deposit right now, not that early claiming is unavailable. - The
instantinstantinstantinstantinstantinstantinstantInstantInstantquote is priced whether or not the configured max claim fee would allow it, so the fee it shows is the provider's price rather than what the configured limit permits. - The
maturematurematurematurematurematurematureMatureMaturequote is always present, but may be flaggedis_estimateis_estimateisEstimateisEstimateisEstimateisEstimateisEstimateIsEstimateIsEstimatewhen the provider will not quote a deposit this early. The fee is then derived from current on-chain fees and the final one may differ.
Acting on the instantinstantinstantinstantinstantinstantinstantInstantInstant quote yourself means passing a max_feemax_feemaxFeemaxFeemaxFeemaxFeemaxFeeMaxFeeMaxFee to claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposit of at least the quoted fee_satsfee_satsfeeSatsfeeSatsfeeSatsfeeSatsfeeSatsFeeSatsFeeSats. With a lower one the call returns ClaimDepositOutcome::DeferredClaimDepositOutcome.DEFERREDClaimDepositOutcome.deferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcomeDeferredClaimDepositOutcome.Deferred with ClaimDeferredReason::MaxFeeExceededClaimDeferredReason.MAX_FEE_EXCEEDEDClaimDeferredReason.maxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReasonMaxFeeExceededClaimDeferredReason.MaxFeeExceeded, carrying what the early claim would have cost, and the deposit waits for the standard claim unless the max fee is raised (see Handling claim outcomes).
What to offer follows from the quote and the configured max claim fee. Check the middle column first: where the SDK claims by itself, a dialog defaulting to the standard claim shows the user one outcome and delivers another.
| Quote state | What the SDK will do | What to present |
|---|---|---|
No instantinstantinstantinstantinstantinstantinstantInstantInstant quote | Standard claim | The standard claim only |
instantinstantinstantinstantinstantinstantinstantInstantInstant quote, depth not yet reached | Claim early once the depth arrives, if the fee fits the max claim fee | The standard claim, with the early claim shown as available in N blocks |
instantinstantinstantinstantinstantinstantinstantInstantInstant quote at a reachable depth, fee above the max claim fee | Wait for the standard claim | Both, the early claim requiring an explicit higher max fee |
instantinstantinstantinstantinstantinstantinstantInstantInstant quote at a reachable depth, fee within the max claim fee | Claim early by itself | Default to the early claim, or offer no choice |
let quote = sdk
.fetch_claim_deposit_quote(FetchClaimDepositQuoteRequest {
txid: deposit.txid.clone(),
vout: deposit.vout,
})
.await?;
// The standard claim, and how many blocks away it is.
let blocks_to_wait = quote
.mature
.confirmations_required
.saturating_sub(quote.confirmations);
info!(
"Wait {} blocks and pay {} sats",
blocks_to_wait, quote.mature.fee_sats
);
// An instant or expedited claim, when the provider offers one.
if let Some(instant) = "e.instant {
let blocks_to_wait = instant
.confirmations_required
.saturating_sub(quote.confirmations);
info!(
"Or wait {} blocks and pay {} sats",
blocks_to_wait, instant.fee_sats
);
}
let request = FetchClaimDepositQuoteRequest(txid: deposit.txid, vout: deposit.vout)
let quote = try await sdk.fetchClaimDepositQuote(request: request)
// The standard claim, and how many blocks away it is.
var blocksToWait: UInt32 = 0
if quote.mature.confirmationsRequired > quote.confirmations {
blocksToWait = quote.mature.confirmationsRequired - quote.confirmations
}
print("Wait \(blocksToWait) blocks and pay \(quote.mature.feeSats) sats")
// An instant or expedited claim, when the provider offers one.
if let instant = quote.instant {
var instantBlocks: UInt32 = 0
if instant.confirmationsRequired > quote.confirmations {
instantBlocks = instant.confirmationsRequired - quote.confirmations
}
print("Or wait \(instantBlocks) blocks and pay \(instant.feeSats) sats")
}
try {
val quote = sdk.fetchClaimDepositQuote(
FetchClaimDepositQuoteRequest(
txid = deposit.txid,
vout = deposit.vout
)
)
// UInt subtraction wraps, so clamp the wait at zero.
fun blocksToWait(required: UInt) =
if (required > quote.confirmations) required - quote.confirmations else 0u
// The standard claim, and how many blocks away it is.
val matureWait = blocksToWait(quote.mature.confirmationsRequired)
// Log.v("Breez", "Wait $matureWait blocks and pay ${quote.mature.feeSats} sats")
// An instant or expedited claim, when the provider offers one.
quote.instant?.let { instant ->
val instantWait = blocksToWait(instant.confirmationsRequired)
// Log.v("Breez", "Or wait $instantWait blocks and pay ${instant.feeSats} sats")
}
} catch (e: Exception) {
// handle error
}
var request = new FetchClaimDepositQuoteRequest(
txid: deposit.txid,
vout: deposit.vout
);
var quote = await sdk.FetchClaimDepositQuote(request: request);
// The standard claim, and how many blocks away it is.
var blocksToWait = quote.mature.confirmationsRequired > quote.confirmations
? quote.mature.confirmationsRequired - quote.confirmations
: 0U;
Console.WriteLine($"Wait {blocksToWait} blocks and pay {quote.mature.feeSats} sats");
// An instant or expedited claim, when the provider offers one.
if (quote.instant is ClaimDepositQuote instant)
{
var instantBlocksToWait = instant.confirmationsRequired > quote.confirmations
? instant.confirmationsRequired - quote.confirmations
: 0U;
Console.WriteLine($"Or wait {instantBlocksToWait} blocks and " +
$"pay {instant.feeSats} sats");
}
const quote = await sdk.fetchClaimDepositQuote({
txid: deposit.txid,
vout: deposit.vout
})
// The standard claim, and how many blocks away it is.
const blocksToWait = Math.max(
0,
quote.mature.confirmationsRequired - quote.confirmations
)
console.log(`Wait ${blocksToWait} blocks and pay ${quote.mature.feeSats} sats`)
// An instant or expedited claim, when the provider offers one.
if (quote.instant != null) {
const instantBlocksToWait = Math.max(
0,
quote.instant.confirmationsRequired - quote.confirmations
)
console.log(
`Or wait ${instantBlocksToWait} blocks and ` +
`pay ${quote.instant.feeSats} sats`
)
}
const quote = await sdk.fetchClaimDepositQuote({
txid: deposit.txid,
vout: deposit.vout
})
// The standard claim, and how many blocks away it is.
const blocksToWait = Math.max(0, quote.mature.confirmationsRequired - quote.confirmations)
console.log(`Wait ${blocksToWait} blocks and pay ${quote.mature.feeSats} sats`)
// An instant or expedited claim, when the provider offers one.
const instant = quote.instant
if (instant != null) {
const instantBlocks = Math.max(0, instant.confirmationsRequired - quote.confirmations)
console.log(`Or wait ${instantBlocks} blocks and pay ${instant.feeSats} sats`)
}
final request = FetchClaimDepositQuoteRequest(
txid: deposit.txid,
vout: deposit.vout,
);
final quote = await sdk.fetchClaimDepositQuote(request: request);
// The standard claim, and how many blocks away it is.
final confirmations = quote.confirmations;
final matureBlocks = quote.mature.confirmationsRequired > confirmations
? quote.mature.confirmationsRequired - confirmations
: 0;
print("Wait $matureBlocks blocks and pay ${quote.mature.feeSats} sats");
// An instant or expedited claim, when the provider offers one.
final instant = quote.instant;
if (instant != null) {
final instantBlocks = instant.confirmationsRequired > confirmations
? instant.confirmationsRequired - confirmations
: 0;
print("Or wait $instantBlocks blocks and pay ${instant.feeSats} sats");
}
try:
request = FetchClaimDepositQuoteRequest(txid=deposit.txid, vout=deposit.vout)
quote = await sdk.fetch_claim_deposit_quote(request=request)
# The standard claim, and how many blocks away it is.
blocks_to_wait = max(
0, quote.mature.confirmations_required - quote.confirmations
)
logging.info(
f"Wait {blocks_to_wait} blocks and pay {quote.mature.fee_sats} sats"
)
# An instant or expedited claim, when the provider offers one.
if quote.instant is not None:
blocks_to_wait = max(
0, quote.instant.confirmations_required - quote.confirmations
)
logging.info(
f"Or wait {blocks_to_wait} blocks and pay {quote.instant.fee_sats} sats"
)
except Exception as error:
logging.error(error)
raise
quote, err := sdk.FetchClaimDepositQuote(breez_sdk_spark.FetchClaimDepositQuoteRequest{
Txid: deposit.Txid,
Vout: deposit.Vout,
})
if err != nil {
return err
}
// The standard claim, and how many blocks away it is.
blocksToWait := uint32(0)
if quote.Mature.ConfirmationsRequired > quote.Confirmations {
blocksToWait = quote.Mature.ConfirmationsRequired - quote.Confirmations
}
log.Printf("Wait %v blocks and pay %v sats", blocksToWait, quote.Mature.FeeSats)
// An instant or expedited claim, when the provider offers one.
if quote.Instant != nil {
instantBlocks := uint32(0)
if quote.Instant.ConfirmationsRequired > quote.Confirmations {
instantBlocks = quote.Instant.ConfirmationsRequired - quote.Confirmations
}
log.Printf("Or wait %v blocks and pay %v sats", instantBlocks, quote.Instant.FeeSats)
}
Handling claim outcomes
claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposit reports what it did as outcomeoutcomeoutcomeoutcomeoutcomeoutcomeoutcomeOutcomeOutcome, which is worth handling in full:
ClaimDepositOutcome::SettledClaimDepositOutcome.SETTLEDClaimDepositOutcome.settledClaimDepositOutcome.SettledClaimDepositOutcome.SettledClaimDepositOutcome.SettledClaimDepositOutcome.SettledClaimDepositOutcomeSettledClaimDepositOutcome.Settled: a standard claim that settled, carrying the payment it produced.ClaimDepositOutcome::SubmittedClaimDepositOutcome.SUBMITTEDClaimDepositOutcome.submittedClaimDepositOutcome.SubmittedClaimDepositOutcome.SubmittedClaimDepositOutcome.SubmittedClaimDepositOutcome.SubmittedClaimDepositOutcomeSubmittedClaimDepositOutcome.Submitted: an instant or expedited claim is settling asynchronously. Watch for the payment vialist_paymentslist_paymentslistPaymentslistPaymentslistPaymentslistPaymentslistPaymentsListPaymentsListPaymentsor the payment events.ClaimDepositOutcome::DeferredClaimDepositOutcome.DEFERREDClaimDepositOutcome.deferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcomeDeferredClaimDepositOutcome.Deferred: nothing was claimed yet, and no further call is needed.
Which outcome occurs follows from the deposit's depth and the max fee rather than from anything you ask for. A max_feemax_feemaxFeemaxFeemaxFeemaxFeemaxFeeMaxFeeMaxFee below what an early claim costs returns ClaimDepositOutcome::DeferredClaimDepositOutcome.DEFERREDClaimDepositOutcome.deferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcome.DeferredClaimDepositOutcomeDeferredClaimDepositOutcome.Deferred rather than failing. A deposit that has already reached the standard claim depth and whose claim exceeds the max fee is a different matter and returns SdkError::MaxDepositClaimFeeExceededSdkError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDEDSdkError.maxDepositClaimFeeExceededSdkError.MaxDepositClaimFeeExceededSdkError.MaxDepositClaimFeeExceededSdkError.MaxDepositClaimFeeExceededSdkError.MaxDepositClaimFeeExceededSdkErrorMaxDepositClaimFeeExceededSdkError.MaxDepositClaimFeeExceeded, because nothing will claim it until the max fee rises or on-chain fees fall.
A deposit worth too little to claim returns SdkError::DepositTooSmallSdkError.DEPOSIT_TOO_SMALLSdkError.depositTooSmallSdkError.DepositTooSmallSdkError.DepositTooSmallSdkError.DepositTooSmallSdkError.DepositTooSmallSdkErrorDepositTooSmallSdkError.DepositTooSmall, from both claim_depositclaim_depositclaimDepositclaimDepositclaimDepositclaimDepositclaimDepositClaimDepositClaimDeposit and fetch_claim_deposit_quotefetch_claim_deposit_quotefetchClaimDepositQuotefetchClaimDepositQuotefetchClaimDepositQuotefetchClaimDepositQuotefetchClaimDepositQuoteFetchClaimDepositQuoteFetchClaimDepositQuote, and automatic claims record it as DepositClaimError::DepositTooSmallDepositClaimError.DEPOSIT_TOO_SMALLDepositClaimError.depositTooSmallDepositClaimError.DepositTooSmallDepositClaimError.DepositTooSmallDepositClaimError.DepositTooSmallDepositClaimError.DepositTooSmallDepositClaimErrorDepositTooSmallDepositClaimError.DepositTooSmall in claim_errorclaim_errorclaimErrorclaimErrorclaimErrorclaimErrorclaimErrorClaimErrorClaimError. This happens when the amount left after the claim fee would be below the dust limit. No max fee changes this, but the deposit can become claimable once on-chain fees fall.
Whether a deferred deposit actually waits for the standard claim depends on its reasonreasonreasonreasonreasonreasonreasonReasonReason. The SDK re-attempts an early claim as the deposit gains confirmations, so a claim declined at a depth the provider will not yet front is often claimed early a block or two later.
ClaimDeferredReason::NoEarlyClaimAvailableClaimDeferredReason.NO_EARLY_CLAIM_AVAILABLEClaimDeferredReason.noEarlyClaimAvailableClaimDeferredReason.NoEarlyClaimAvailableClaimDeferredReason.NoEarlyClaimAvailableClaimDeferredReason.NoEarlyClaimAvailableClaimDeferredReason.NoEarlyClaimAvailableClaimDeferredReasonNoEarlyClaimAvailableClaimDeferredReason.NoEarlyClaimAvailableusually clears with the next confirmation.ClaimDeferredReason::MaxFeeExceededClaimDeferredReason.MAX_FEE_EXCEEDEDClaimDeferredReason.maxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReason.MaxFeeExceededClaimDeferredReasonMaxFeeExceededClaimDeferredReason.MaxFeeExceededdoes not clear on its own. The deposit waits for the standard claim unless the max fee is raised.ClaimDeferredReason::ProviderDeclinedClaimDeferredReason.PROVIDER_DECLINEDClaimDeferredReason.providerDeclinedClaimDeferredReason.ProviderDeclinedClaimDeferredReason.ProviderDeclinedClaimDeferredReason.ProviderDeclinedClaimDeferredReason.ProviderDeclinedClaimDeferredReasonProviderDeclinedClaimDeferredReason.ProviderDeclinedmeans the provider refused or could not be reached, which another confirmation does not address.
Only the first is a wait you can put a time on, so showing a user "claimed in about 30 minutes" for the others would be wrong.
Refunding deposits
When a deposit cannot be claimed you can refund it to an external Bitcoin address. This creates a transaction that sends the amount, minus transaction fees, to the destination address.
A deposit that has already been claimed is not a candidate: its instant_claim_statusinstant_claim_statusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusinstantClaimStatusInstantClaimStatusInstantClaimStatus is InstantClaimStatus::ClaimedInstantClaimStatus.CLAIMEDInstantClaimStatus.claimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatus.ClaimedInstantClaimStatusClaimedInstantClaimStatus.Claimed, so check that before offering a refund.
The recommended fees API is useful for choosing a fee for the refund transaction.
A deposit can only be refunded once it has enough confirmations. Calling refund_depositrefund_depositrefundDepositrefundDepositrefundDepositrefundDepositrefundDepositRefundDepositRefundDeposit earlier fails, reporting the deposit as unknown while it is unconfirmed and as having too few confirmations for a block or so after that. Nothing is signed or stored when this happens, so retry after a few more blocks.
let txid = "your_deposit_txid".to_string();
let vout = 0;
let destination_address = "bc1qexample...".to_string(); // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
let recommended_fees = sdk.recommended_fees().await?;
let fee = Fee::Rate {
sat_per_vbyte: recommended_fees.half_hour_fee,
};
// or using a fixed amount
//let fee = Fee::Fixed { amount: 500 };
//
let request = RefundDepositRequest {
txid,
vout,
destination_address,
fee,
};
let response = sdk.refund_deposit(request).await?;
info!("Refund transaction created:");
info!("Transaction ID: {}", response.tx_id);
info!("Transaction hex: {}", response.tx_hex);
let txid = "your_deposit_txid"
let vout: UInt32 = 0
let destinationAddress = "bc1qexample..." // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
let recommendedFees = try await sdk.recommendedFees()
let fee = Fee.rate(satPerVbyte: recommendedFees.halfHourFee)
// or using a fixed amount
//let fee = Fee.fixed(amount: 500) // 500 sats
//
let request = RefundDepositRequest(
txid: txid,
vout: vout,
destinationAddress: destinationAddress,
fee: fee
)
let response = try await sdk.refundDeposit(request: request)
print("Refund transaction created:")
print("Transaction ID: \(response.txId)")
print("Transaction hex: \(response.txHex)")
try {
val txid = "your_deposit_txid"
val vout = 0u
val destinationAddress = "bc1qexample..." // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
val recommendedFees = sdk.recommendedFees()
val fee = Fee.Rate(recommendedFees.halfHourFee)
// or using a fixed amount
//val fee = Fee.Fixed(500u)
//
val request = RefundDepositRequest(
txid = txid,
vout = vout,
destinationAddress = destinationAddress,
fee = fee
)
val response = sdk.refundDeposit(request)
// Log.v("Breez", "Refund transaction created:")
// Log.v("Breez", "Transaction ID: ${response.txId}")
// Log.v("Breez", "Transaction hex: ${response.txHex}")
} catch (e: Exception) {
// handle error
}
var txid = "your_deposit_txid";
var vout = 0U;
var destinationAddress = "bc1qexample..."; // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
var recommendedFees = await sdk.RecommendedFees();
var fee = new Fee.Rate(satPerVbyte: recommendedFees.halfHourFee);
// or using a fixed amount
//var fee = new Fee.Fixed(amount: 500);
//
var request = new RefundDepositRequest(
txid: txid,
vout: vout,
destinationAddress: destinationAddress,
fee: fee
);
var response = await sdk.RefundDeposit(request: request);
Console.WriteLine("Refund transaction created:");
Console.WriteLine($"Transaction ID: {response.txId}");
Console.WriteLine($"Transaction hex: {response.txHex}");
const txid = 'your_deposit_txid'
const vout = 0
const destinationAddress = 'bc1qexample...' // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
const recommendedFees = await sdk.recommendedFees()
const fee: Fee = { type: 'rate', satPerVbyte: recommendedFees.halfHourFee }
// or using a fixed amount
// const fee: Fee = { type: 'fixed', amount: 500 }
//
const request: RefundDepositRequest = {
txid,
vout,
destinationAddress,
fee
}
const response = await sdk.refundDeposit(request)
console.log('Refund transaction created:')
console.log('Transaction ID:', response.txId)
console.log('Transaction hex:', response.txHex)
const txid = 'your_deposit_txid'
const vout = 0
const destinationAddress = 'bc1qexample...' // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
const recommendedFees = await sdk.recommendedFees()
const fee = new Fee.Rate({ satPerVbyte: recommendedFees.halfHourFee })
// or using a fixed amount
// const fee = new Fee.Fixed({ amount: BigInt(500) })
//
const request: RefundDepositRequest = {
txid,
vout,
destinationAddress,
fee
}
const response = await sdk.refundDeposit(request)
console.log('Refund transaction created:')
console.log('Transaction ID:', response.txId)
console.log('Transaction hex:', response.txHex)
String txid = "your_deposit_txid";
int vout = 0;
String destinationAddress = "bc1qexample..."; // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
final recommendedFees = await sdk.recommendedFees();
Fee fee = Fee.rate(satPerVbyte: recommendedFees.halfHourFee);
// or using a fixed amount
//Fee fee = Fee.fixed(amount: BigInt.from(500));
//
final request = RefundDepositRequest(
txid: txid,
vout: vout,
destinationAddress: destinationAddress,
fee: fee,
);
final response = await sdk.refundDeposit(request: request);
print("Refund transaction created:");
print("Transaction ID: ${response.txId}");
print("Transaction hex: ${response.txHex}");
try:
txid = "your_deposit_txid"
vout = 0
destination_address = "bc1qexample..." # Your Bitcoin address
# Set the fee for the refund transaction using the half-hour feerate
recommended_fees = await sdk.recommended_fees()
fee = Fee.RATE(sat_per_vbyte=recommended_fees.half_hour_fee)
# or using a fixed amount
#fee = Fee.FIXED(amount=500)
#
request = RefundDepositRequest(
txid=txid, vout=vout, destination_address=destination_address, fee=fee
)
response = await sdk.refund_deposit(request=request)
logging.info("Refund transaction created:")
logging.info(f"Transaction ID: {response.tx_id}")
logging.info(f"Transaction hex: {response.tx_hex}")
except Exception as error:
logging.error(error)
raise
txid := "<your_deposit_txid>"
vout := uint32(0)
destinationAddress := "bc1qexample..." // Your Bitcoin address
// Set the fee for the refund transaction using the half-hour feerate
recommendedFees, err := sdk.RecommendedFees()
if err != nil {
return err
}
fee := breez_sdk_spark.Fee(breez_sdk_spark.FeeRate{SatPerVbyte: recommendedFees.HalfHourFee})
// or using a fixed amount
//fee := breez_sdk_spark.Fee(breez_sdk_spark.FeeFixed{Amount: 500})
//
request := breez_sdk_spark.RefundDepositRequest{
Txid: txid,
Vout: vout,
DestinationAddress: destinationAddress,
Fee: fee,
}
response, err := sdk.RefundDeposit(request)
if err != nil {
var sdkErr *breez_sdk_spark.SdkError
if errors.As(err, &sdkErr) {
// Handle SdkError - can inspect specific variants if needed
// e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc.
}
return err
}
log.Print("Refund transaction created:")
log.Printf("Transaction ID: %v", response.TxId)
log.Printf("Transaction hex: %v", response.TxHex)
Developer note
The total fee must cover at least 1 sat/vB of the refund transaction so it can be relayed by the Bitcoin network. The exact minimum depends on the size of the transaction, which varies with the destination address type: around 99 sats to a native segwit address and 111 sats to a taproot one. If the fee is lower, the refund request is rejected and the error states the required minimum.Tracking a refund
refund_staterefund_staterefundStaterefundStaterefundStaterefundStaterefundStateRefundStateRefundState on DepositInfoDepositInfoDepositInfoDepositInfoDepositInfoDepositInfoDepositInfoDepositInfoDepositInfo reports how far the refund has got:
RefundState::BroadcastPendingRefundState.BROADCAST_PENDINGRefundState.broadcastPendingRefundState.BroadcastPendingRefundState.BroadcastPendingRefundState.BroadcastPendingRefundState.BroadcastPendingRefundStateBroadcastPendingRefundState.BroadcastPending: the refund is signed and stored but has not been seen on the network. The SDK rebroadcasts it on every sync until the deposit is spent, so a refund that failed to send because of a temporary network problem recovers on its own.RefundState::BroadcastRefundState.BROADCASTRefundState.broadcastRefundState.BroadcastRefundState.BroadcastRefundState.BroadcastRefundState.BroadcastRefundStateBroadcastRefundState.Broadcast: the network has accepted the refund and it is waiting to confirm. The deposit disappears fromlist_unclaimed_depositslist_unclaimed_depositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositslistUnclaimedDepositsListUnclaimedDepositsListUnclaimedDepositsonce it does.
A refund created near the 1 sat/vB minimum can stay at RefundState::BroadcastPendingRefundState.BROADCAST_PENDINGRefundState.broadcastPendingRefundState.BroadcastPendingRefundState.BroadcastPendingRefundState.BroadcastPendingRefundState.BroadcastPendingRefundStateBroadcastPendingRefundState.BroadcastPending indefinitely if the network's minimum relay fee later rises above what it pays. Rebroadcasting cannot fix this, because the network keeps refusing the same transaction. Read last_errorlast_errorlastErrorlastErrorlastErrorlastErrorlastErrorLastErrorLastError for the reason the network gave, then call refund_depositrefund_depositrefundDepositrefundDepositrefundDepositrefundDepositrefundDepositRefundDepositRefundDeposit again at a higher fee to replace it.
Replacing a refund that is already on the network costs more than the original fee, because the replacement also pays to relay its own size. When the fee offered is too low, the call is rejected and the error states the minimum required.
Recommended fees
Get Bitcoin fee estimates for different confirmation targets to help determine appropriate fee levels for claiming or refunding deposits.
let response = sdk.recommended_fees().await?;
info!("Fastest fee: {} sats/vByte", response.fastest_fee);
info!("Half-hour fee: {} sats/vByte", response.half_hour_fee);
info!("Hour fee: {} sats/vByte", response.hour_fee);
info!("Economy fee: {} sats/vByte", response.economy_fee);
info!("Minimum fee: {} sats/vByte", response.minimum_fee);
let response = try await sdk.recommendedFees()
print("Fastest fee: \(response.fastestFee) sats/vByte")
print("Half-hour fee: \(response.halfHourFee) sats/vByte")
print("Hour fee: \(response.hourFee) sats/vByte")
print("Economy fee: \(response.economyFee) sats/vByte")
print("Minimum fee: \(response.minimumFee) sats/vByte")
val response = sdk.recommendedFees()
println("Fastest fee: ${response.fastestFee} sats/vByte")
println("Half-hour fee: ${response.halfHourFee} sats/vByte")
println("Hour fee: ${response.hourFee} sats/vByte")
println("Economy fee: ${response.economyFee} sats/vByte")
println("Minimum fee: ${response.minimumFee} sats/vByte")
var response = await sdk.RecommendedFees();
Console.WriteLine($"Fastest fee: {response.fastestFee} sats/vByte");
Console.WriteLine($"Half-hour fee: {response.halfHourFee} sats/vByte");
Console.WriteLine($"Hour fee: {response.hourFee} sats/vByte");
Console.WriteLine($"Economy fee: {response.economyFee} sats/vByte");
Console.WriteLine($"Minimum fee: {response.minimumFee} sats/vByte");
}
const response = await sdk.recommendedFees()
console.log('Fastest fee:', response.fastestFee, 'sats/vByte')
console.log('Half-hour fee:', response.halfHourFee, 'sats/vByte')
console.log('Hour fee:', response.hourFee, 'sats/vByte')
console.log('Economy fee:', response.economyFee, 'sats/vByte')
console.log('Minimum fee:', response.minimumFee, 'sats/vByte')
const response = await sdk.recommendedFees()
console.log('Fastest fee:', response.fastestFee, 'sats/vByte')
console.log('Half-hour fee:', response.halfHourFee, 'sats/vByte')
console.log('Hour fee:', response.hourFee, 'sats/vByte')
console.log('Economy fee:', response.economyFee, 'sats/vByte')
console.log('Minimum fee:', response.minimumFee, 'sats/vByte')
final response = await sdk.recommendedFees();
print("Fastest fee: ${response.fastestFee} sats/vByte");
print("Half-hour fee: ${response.halfHourFee} sats/vByte");
print("Hour fee: ${response.hourFee} sats/vByte");
print("Economy fee: ${response.economyFee} sats/vByte");
print("Minimum fee: ${response.minimumFee} sats/vByte");
response = await sdk.recommended_fees()
logging.info(f"Fastest fee: {response.fastest_fee} sats/vByte")
logging.info(f"Half-hour fee: {response.half_hour_fee} sats/vByte")
logging.info(f"Hour fee: {response.hour_fee} sats/vByte")
logging.info(f"Economy fee: {response.economy_fee} sats/vByte")
logging.info(f"Minimum fee: {response.minimum_fee} sats/vByte")
response, err := sdk.RecommendedFees()
if err != nil {
var sdkErr *breez_sdk_spark.SdkError
if errors.As(err, &sdkErr) {
// Handle SdkError - can inspect specific variants if needed
// e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc.
}
return err
}
log.Printf("Fastest fee: %v sats/vByte", response.FastestFee)
log.Printf("Half-hour fee: %v sats/vByte", response.HalfHourFee)
log.Printf("Hour fee: %v sats/vByte", response.HourFee)
log.Printf("Economy fee: %v sats/vByte", response.EconomyFee)
log.Printf("Minimum fee: %v sats/vByte", response.MinimumFee)