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:
- You can receive payments in any format (Lightning, Spark, on-chain deposits)
- The SDK automatically converts any received sats to your chosen stable token
- 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%).
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),
});
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
)
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,
)
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
)
};
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
}
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
}
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,
));
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,
)
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:
sdk.update_user_settings(UpdateUserSettingsRequest {
spark_private_mode_enabled: None,
stable_balance_active_label: Some(StableBalanceActiveLabel::Set {
label: "USDB".to_string(),
}),
})
.await?;
try await sdk.updateUserSettings(
request: UpdateUserSettingsRequest(
sparkPrivateModeEnabled: nil,
stableBalanceActiveLabel: .set(label: "USDB")
))
try {
sdk.updateUserSettings(UpdateUserSettingsRequest(
sparkPrivateModeEnabled = null,
stableBalanceActiveLabel = StableBalanceActiveLabel.Set(label = "USDB")
))
} catch (e: Exception) {
// handle error
}
await sdk.UpdateUserSettings(
request: new UpdateUserSettingsRequest(
sparkPrivateModeEnabled: null,
stableBalanceActiveLabel: new StableBalanceActiveLabel.Set(label: "USDB")
)
);
await sdk.updateUserSettings({
stableBalanceActiveLabel: { type: 'set', label: 'USDB' }
})
await sdk.updateUserSettings({
sparkPrivateModeEnabled: undefined,
stableBalanceActiveLabel: new StableBalanceActiveLabel.Set({ label: 'USDB' })
})
await sdk.updateUserSettings(
request: UpdateUserSettingsRequest(
stableBalanceActiveLabel: StableBalanceActiveLabel_Set(label: "USDB")));
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
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:
sdk.update_user_settings(UpdateUserSettingsRequest {
spark_private_mode_enabled: None,
stable_balance_active_label: Some(StableBalanceActiveLabel::Unset),
})
.await?;
try await sdk.updateUserSettings(
request: UpdateUserSettingsRequest(
sparkPrivateModeEnabled: nil,
stableBalanceActiveLabel: .unset
))
try {
sdk.updateUserSettings(UpdateUserSettingsRequest(
sparkPrivateModeEnabled = null,
stableBalanceActiveLabel = StableBalanceActiveLabel.Unset
))
} catch (e: Exception) {
// handle error
}
await sdk.UpdateUserSettings(
request: new UpdateUserSettingsRequest(
sparkPrivateModeEnabled: null,
stableBalanceActiveLabel: new StableBalanceActiveLabel.Unset()
)
);
await sdk.updateUserSettings({
stableBalanceActiveLabel: { type: 'unset' }
})
await sdk.updateUserSettings({
sparkPrivateModeEnabled: undefined,
stableBalanceActiveLabel: new StableBalanceActiveLabel.Unset()
})
await sdk.updateUserSettings(
request: UpdateUserSettingsRequest(
stableBalanceActiveLabel: StableBalanceActiveLabel_Unset()));
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
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:
let user_settings = sdk.get_user_settings().await?;
info!("User settings: {:?}", user_settings);
let userSettings = try await sdk.getUserSettings()
print("User settings: \(userSettings)")
try {
val userSettings = sdk.getUserSettings()
println("User settings: $userSettings")
} catch (e: Exception) {
// handle error
}
var userSettings = await sdk.GetUserSettings();
Console.WriteLine($"User settings: {userSettings}");
const userSettings = await sdk.getUserSettings()
console.log(`User settings: ${JSON.stringify(userSettings)}`)
const userSettings = await sdk.getUserSettings()
console.log(`User settings: ${JSON.stringify(userSettings)}`)
final userSettings = await sdk.getUserSettings();
print('User settings: $userSettings');
try:
user_settings = await sdk.get_user_settings()
print(f"User settings: {user_settings}")
except Exception as error:
logging.error(error)
raise
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:
- If you have enough Bitcoin balance, no conversion is needed
- 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:
| Status | Description |
|---|---|
ConversionStatus::PendingConversionStatus.PENDINGConversionStatus.pendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatus.PendingConversionStatusPendingConversionStatus.Pending | Conversion is queued or in progress |
ConversionStatus::CompletedConversionStatus.COMPLETEDConversionStatus.completedConversionStatus.CompletedConversionStatus.CompletedConversionStatus.CompletedConversionStatus.CompletedConversionStatusCompletedConversionStatus.Completed | Conversion finished successfully |
ConversionStatus::FailedConversionStatus.FAILEDConversionStatus.failedConversionStatus.FailedConversionStatus.FailedConversionStatus.FailedConversionStatus.FailedConversionStatusFailedConversionStatus.Failed | Conversion could not be completed |
ConversionStatus::RefundNeededConversionStatus.REFUND_NEEDEDConversionStatus.refundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatus.RefundNeededConversionStatusRefundNeededConversionStatus.RefundNeeded | Conversion failed and requires a refund |
ConversionStatus::RefundedConversionStatus.REFUNDEDConversionStatus.refundedConversionStatus.RefundedConversionStatus.RefundedConversionStatus.RefundedConversionStatus.RefundedConversionStatusRefundedConversionStatus.Refunded | Failed conversion has been refunded |
Conversion steps
The fromfromfromfromfromfromfromFromFrom and tototototototoToTo fields are conversion step objects describing each side of the conversion:
| Field | Description |
|---|---|
payment_idpayment_idpaymentIdpaymentIdpaymentIdpaymentIdpaymentIdPaymentIdPaymentId | The ID of the internal conversion payment |
amountamountamountamountamountamountamountAmountAmount | The amount in the step's denomination (sats or token units) |
feefeefeefeefeefeefeeFeeFee | Fee charged for this step |
methodmethodmethodmethodmethodmethodmethodMethodMethod | Payment 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_metadatatokenMetadatatokenMetadatatokenMetadatatokenMetadatatokenMetadataTokenMetadataTokenMetadata | Token metadata (name, symbol, etc.) — present when method is PaymentMethod::TokenPaymentMethod.TOKENPaymentMethod.tokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethod.TokenPaymentMethodTokenPaymentMethod.Token |
amount_adjustmentamount_adjustmentamountAdjustmentamountAdjustmentamountAdjustmentamountAdjustmentamountAdjustmentAmountAdjustmentAmountAdjustment | Present 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:
| Reason | Description |
|---|---|
AmountAdjustmentReason::FlooredToMinLimitAmountAdjustmentReason.FLOORED_TO_MIN_LIMITAmountAdjustmentReason.flooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimitAmountAdjustmentReasonFlooredToMinLimitAmountAdjustmentReason.FlooredToMinLimit | Amount was increased to meet the minimum conversion limit |
AmountAdjustmentReason::IncreasedToAvoidDustAmountAdjustmentReason.INCREASED_TO_AVOID_DUSTAmountAdjustmentReason.increasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDustAmountAdjustmentReasonIncreasedToAvoidDustAmountAdjustmentReason.IncreasedToAvoidDust | Amount was increased to convert the entire remaining balance, avoiding a leftover too small to convert back |
Related pages
- Token conversion - Learn about converting between Bitcoin and tokens
- Custom configuration - All configuration options
- User settings - Getting and updating user settings
- Handling tokens - Working with tokens in the SDK