Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stable balance

The stable balance feature enables users to automatically convert received Bitcoin to a stable token, protecting against Bitcoin price volatility. This is ideal for users who want to receive Bitcoin payments but prefer to hold their value in a stable asset like a USD-pegged stablecoin.

How it works

When stable balance is configured and activated, the SDK automatically monitors your sats balance. When your sats balance exceeds the configured threshold, the SDK automatically converts the excess sats to the active stable token using token conversions.

This creates a seamless experience where:

  1. You can receive payments in any format (Lightning, Spark, on-chain deposits)
  2. The SDK automatically converts any received sats to your chosen stable token
  3. Your balance remains stable in value, denominated in the stable token

Configuration

To enable stable balance, configure the stable balance config when initializing the SDK with the following options:

  • Tokens - A list of available stable tokens, each with a display label and token identifier. Labels must be unique and are used to activate a specific token at runtime.
  • Default Active Label - Optional label of the token to activate by default. If unset, stable balance starts deactivated and can be activated at runtime via user settings.
  • Threshold Sats - Optional minimum sats balance to trigger auto-conversion. Defaults to the conversion limit minimum if not specified.
  • Maximum Slippage - Optional maximum slippage in basis points. Defaults to 50 bps (0.5%).
Rust
let mut config = default_config(Network::Mainnet);

// Enable stable balance with auto-conversion to a specific token
config.stable_balance_config = Some(StableBalanceConfig {
    tokens: vec![StableBalanceToken {
        label: "USDB".to_string(),
        token_identifier: "<token_identifier>".to_string(),
    }],
    default_active_label: Some("USDB".to_string()),
    threshold_sats: Some(10_000),
    max_slippage_bps: Some(100),
});
Swift
var config = defaultConfig(network: Network.mainnet)

// Enable stable balance with auto-conversion to a specific token
config.stableBalanceConfig = StableBalanceConfig(
    tokens: [StableBalanceToken(
        label: "USDB",
        tokenIdentifier: "<token_identifier>"
    )],
    defaultActiveLabel: "USDB",
    thresholdSats: 10_000,
    maxSlippageBps: 100
)
Kotlin
val config = defaultConfig(Network.MAINNET)

// Enable stable balance with auto-conversion to a specific token
config.stableBalanceConfig = StableBalanceConfig(
    tokens = listOf(StableBalanceToken(
        label = "USDB",
        tokenIdentifier = "<token_identifier>",
    )),
    defaultActiveLabel = "USDB",
    thresholdSats = 10_000u,
    maxSlippageBps = 100u,
)
C#
var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with
{
    // Enable stable balance with auto-conversion to a specific token
    stableBalanceConfig = new StableBalanceConfig(
        tokens: new StableBalanceToken[] {
            new StableBalanceToken(
                label: "USDB",
                tokenIdentifier: "<token_identifier>"
            )
        },
        defaultActiveLabel: "USDB",
        thresholdSats: 10000,
        maxSlippageBps: 100
    )
};
Javascript
const config = defaultConfig('mainnet')

// Enable stable balance with auto-conversion to a specific token
config.stableBalanceConfig = {
  tokens: [{
    label: 'USDB',
    tokenIdentifier: '<token_identifier>'
  }],
  defaultActiveLabel: 'USDB',
  thresholdSats: 10_000,
  maxSlippageBps: 100
}
React Native
const config = defaultConfig(Network.Mainnet)

// Enable stable balance with auto-conversion to a specific token
config.stableBalanceConfig = {
  tokens: [{
    label: 'USDB',
    tokenIdentifier: '<token_identifier>'
  }],
  defaultActiveLabel: 'USDB',
  thresholdSats: BigInt(10_000),
  maxSlippageBps: 100
}
Flutter
var config = defaultConfig(network: Network.mainnet).copyWith(
    // Enable stable balance with auto-conversion to a specific token
    stableBalanceConfig: StableBalanceConfig(
        tokens: [StableBalanceToken(
          label: "USDB",
          tokenIdentifier: "<token_identifier>",
        )],
        defaultActiveLabel: "USDB",
        thresholdSats: BigInt.from(10000),
        maxSlippageBps: 100,
        ));
Python
config = default_config(network=Network.MAINNET)

# Enable stable balance with auto-conversion to a specific token
config.stable_balance_config = StableBalanceConfig(
    tokens=[StableBalanceToken(
        label="USDB",
        token_identifier="<token_identifier>",
    )],
    default_active_label="USDB",
    threshold_sats=10_000,
    max_slippage_bps=100,
)
Go
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)

// Enable stable balance with auto-conversion to a specific token
thresholdSats := uint64(10_000)
maxSlippageBps := uint32(100)
defaultActiveLabel := "USDB"
stableBalanceConfig := breez_sdk_spark.StableBalanceConfig{
	Tokens: []breez_sdk_spark.StableBalanceToken{
		{Label: "USDB", TokenIdentifier: "<token_identifier>"},
	},
	DefaultActiveLabel: &defaultActiveLabel,
	ThresholdSats:       &thresholdSats,
	MaxSlippageBps:      &maxSlippageBps,
}
config.StableBalanceConfig = &stableBalanceConfig

Developer note

If the configured threshold sats is lower than the minimum amount required by the conversion protocol, the protocol minimum will be used instead. This ensures conversions always meet the minimum requirements.

Switching stable balance mode

You can activate, switch, or deactivate stable balance at runtime using the user settings API. This allows users to choose when to enable stable balance and which token to use.

Activating stable balance

To activate stable balance, set the active label to one of the labels defined in your #{{name StableBalanceConfig.tokens}} list:

Rust
sdk.update_user_settings(UpdateUserSettingsRequest {
    spark_private_mode_enabled: None,
    stable_balance_active_label: Some(StableBalanceActiveLabel::Set {
        label: "USDB".to_string(),
    }),
})
.await?;
Swift
try await sdk.updateUserSettings(
    request: UpdateUserSettingsRequest(
        sparkPrivateModeEnabled: nil,
        stableBalanceActiveLabel: .set(label: "USDB")
    ))
Kotlin
try {
    sdk.updateUserSettings(UpdateUserSettingsRequest(
        sparkPrivateModeEnabled = null,
        stableBalanceActiveLabel = StableBalanceActiveLabel.Set(label = "USDB")
    ))
} catch (e: Exception) {
    // handle error
}
C#
await sdk.UpdateUserSettings(
    request: new UpdateUserSettingsRequest(
        sparkPrivateModeEnabled: null,
        stableBalanceActiveLabel: new StableBalanceActiveLabel.Set(label: "USDB")
    )
);
Javascript
await sdk.updateUserSettings({
  stableBalanceActiveLabel: { type: 'set', label: 'USDB' }
})
React Native
await sdk.updateUserSettings({
  sparkPrivateModeEnabled: undefined,
  stableBalanceActiveLabel: new StableBalanceActiveLabel.Set({ label: 'USDB' })
})
Flutter
await sdk.updateUserSettings(
    request: UpdateUserSettingsRequest(
        stableBalanceActiveLabel: StableBalanceActiveLabel_Set(label: "USDB")));
Python
try:
    await sdk.update_user_settings(
        request=UpdateUserSettingsRequest(
            spark_private_mode_enabled=None,
            stable_balance_active_label=StableBalanceActiveLabel.SET(label="USDB")
        )
    )
except Exception as error:
    logging.error(error)
    raise
Go
activeLabel := breez_sdk_spark.StableBalanceActiveLabel(
	breez_sdk_spark.StableBalanceActiveLabelSet{Label: "USDB"},
)
err := sdk.UpdateUserSettings(breez_sdk_spark.UpdateUserSettingsRequest{
	StableBalanceActiveLabel: &activeLabel,
})

if err != nil {
	return err
}

When activated, the SDK immediately converts any excess sats balance to the specified token.

Deactivating stable balance

To deactivate stable balance, unset the active label:

Rust
sdk.update_user_settings(UpdateUserSettingsRequest {
    spark_private_mode_enabled: None,
    stable_balance_active_label: Some(StableBalanceActiveLabel::Unset),
})
.await?;
Swift
try await sdk.updateUserSettings(
    request: UpdateUserSettingsRequest(
        sparkPrivateModeEnabled: nil,
        stableBalanceActiveLabel: .unset
    ))
Kotlin
try {
    sdk.updateUserSettings(UpdateUserSettingsRequest(
        sparkPrivateModeEnabled = null,
        stableBalanceActiveLabel = StableBalanceActiveLabel.Unset
    ))
} catch (e: Exception) {
    // handle error
}
C#
await sdk.UpdateUserSettings(
    request: new UpdateUserSettingsRequest(
        sparkPrivateModeEnabled: null,
        stableBalanceActiveLabel: new StableBalanceActiveLabel.Unset()
    )
);
Javascript
await sdk.updateUserSettings({
  stableBalanceActiveLabel: { type: 'unset' }
})
React Native
await sdk.updateUserSettings({
  sparkPrivateModeEnabled: undefined,
  stableBalanceActiveLabel: new StableBalanceActiveLabel.Unset()
})
Flutter
await sdk.updateUserSettings(
    request: UpdateUserSettingsRequest(
        stableBalanceActiveLabel: StableBalanceActiveLabel_Unset()));
Python
try:
    await sdk.update_user_settings(
        request=UpdateUserSettingsRequest(
            spark_private_mode_enabled=None,
            stable_balance_active_label=StableBalanceActiveLabel.UNSET()
        )
    )
except Exception as error:
    logging.error(error)
    raise
Go
activeLabel := breez_sdk_spark.StableBalanceActiveLabel(
	breez_sdk_spark.StableBalanceActiveLabelUnset{},
)
err := sdk.UpdateUserSettings(breez_sdk_spark.UpdateUserSettingsRequest{
	StableBalanceActiveLabel: &activeLabel,
})

if err != nil {
	return err
}

When deactivated, the SDK automatically converts any remaining token balance back to Bitcoin.

Checking the current mode

You can check which token is currently active using get_user_settingsget_user_settingsgetUserSettingsgetUserSettingsgetUserSettingsgetUserSettingsgetUserSettingsGetUserSettingsGetUserSettings:

Rust
let user_settings = sdk.get_user_settings().await?;
info!("User settings: {:?}", user_settings);
Swift
let userSettings = try await sdk.getUserSettings()
print("User settings: \(userSettings)")
Kotlin
try {
    val userSettings = sdk.getUserSettings()
    println("User settings: $userSettings")
} catch (e: Exception) {
    // handle error
}
C#
var userSettings = await sdk.GetUserSettings();

Console.WriteLine($"User settings: {userSettings}");
Javascript
const userSettings = await sdk.getUserSettings()
console.log(`User settings: ${JSON.stringify(userSettings)}`)
React Native
const userSettings = await sdk.getUserSettings()
console.log(`User settings: ${JSON.stringify(userSettings)}`)
Flutter
final userSettings = await sdk.getUserSettings();
print('User settings: $userSettings');
Python
try:
    user_settings = await sdk.get_user_settings()

    print(f"User settings: {user_settings}")
except Exception as error:
    logging.error(error)
    raise
Go
userSettings, err := sdk.GetUserSettings()

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("User settings: %v", userSettings)

The stable_balance_active_labelstable_balance_active_labelstableBalanceActiveLabelstableBalanceActiveLabelstableBalanceActiveLabelstableBalanceActiveLabelstableBalanceActiveLabelStableBalanceActiveLabelStableBalanceActiveLabel field will be unset if stable balance is deactivated, or the label of the currently active token.

Sending payments with stable balance

When your balance is held in a stable token, you can still send Bitcoin payments. The SDK automatically detects when there's not enough Bitcoin balance to cover a payment and sets up the token-to-Bitcoin conversion for you.

When you prepare to send a payment without specifying conversion options:

  1. If you have enough Bitcoin balance, no conversion is needed
  2. If your Bitcoin balance is insufficient, the SDK automatically configures conversion options using your stable balance settings (token identifier and slippage)

Developer note

You can still explicitly specify conversion options in your request if you need custom slippage settings or want to override the automatic behavior.

Conversion details

Payments involving token conversions include a conversion_detailsconversion_detailsconversionDetailsconversionDetailsconversionDetailsconversionDetailsconversionDetailsConversionDetailsConversionDetails field that describes the conversion that took place. This is useful for displaying conversion context in your UI.

Status

The statusstatusstatusstatusstatusstatusstatusStatusStatus field tracks the lifecycle of the conversion:

StatusDescription
ConversionStatus::PendingConversionStatus.PENDINGConversionStatus.pendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatusPendingConversionStatus.PendingConversion is queued or in progress
ConversionStatus::CompletedConversionStatus.COMPLETEDConversionStatus.completedConversionStatus.CompletedConversionStatus.CompletedConversionStatus.CompletedConversionStatus.CompletedConversionStatusCompletedConversionStatus.CompletedConversion finished successfully
ConversionStatus::FailedConversionStatus.FAILEDConversionStatus.failedConversionStatus.FailedConversionStatus.FailedConversionStatus.FailedConversionStatus.FailedConversionStatusFailedConversionStatus.FailedConversion could not be completed
ConversionStatus::RefundNeededConversionStatus.REFUND_NEEDEDConversionStatus.refundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatusRefundNeededConversionStatus.RefundNeededConversion failed and requires a refund
ConversionStatus::RefundedConversionStatus.REFUNDEDConversionStatus.refundedConversionStatus.RefundedConversionStatus.RefundedConversionStatus.RefundedConversionStatus.RefundedConversionStatusRefundedConversionStatus.RefundedFailed conversion has been refunded

Conversion steps

The fromfromfromfromfromfromfromFromFrom and tototototototoToTo fields are conversion step objects describing each side of the conversion:

FieldDescription
payment_idpayment_idpaymentIdpaymentIdpaymentIdpaymentIdpaymentIdPaymentIdPaymentIdThe ID of the internal conversion payment
amountamountamountamountamountamountamountAmountAmountThe amount in the step's denomination (sats or token units)
feefeefeefeefeefeefeeFeeFeeFee charged for this step
methodmethodmethodmethodmethodmethodmethodMethodMethodPayment method (PaymentMethod::SparkPaymentMethod.SPARKPaymentMethod.sparkPaymentMethod.SparkPaymentMethod.SparkPaymentMethod.SparkPaymentMethod.SparkPaymentMethodSparkPaymentMethod.Spark for Bitcoin, PaymentMethod::TokenPaymentMethod.TOKENPaymentMethod.tokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethodTokenPaymentMethod.Token for stable tokens)
token_metadatatoken_metadatatokenMetadatatokenMetadatatokenMetadatatokenMetadatatokenMetadataTokenMetadataTokenMetadataToken metadata (name, symbol, etc.) — present when method is PaymentMethod::TokenPaymentMethod.TOKENPaymentMethod.tokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethodTokenPaymentMethod.Token
amount_adjustmentamount_adjustmentamountAdjustmentamountAdjustmentamountAdjustmentamountAdjustmentamountAdjustmentAmountAdjustmentAmountAdjustmentPresent if the amount was modified before conversion (see amount adjustments)

Amount adjustments

The amount_adjustmentamount_adjustmentamountAdjustmentamountAdjustmentamountAdjustmentamountAdjustmentamountAdjustmentAmountAdjustmentAmountAdjustment field is present when the conversion amount was modified before execution:

ReasonDescription
AmountAdjustmentReason::FlooredToMinLimitAmountAdjustmentReason.FLOORED_TO_MIN_LIMITAmountAdjustmentReason.flooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReasonFlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmount was increased to meet the minimum conversion limit
AmountAdjustmentReason::IncreasedToAvoidDustAmountAdjustmentReason.INCREASED_TO_AVOID_DUSTAmountAdjustmentReason.increasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReasonIncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmount was increased to convert the entire remaining balance, avoiding a leftover too small to convert back