Another Lazy Tool: Auto-Selling HBD for HIVE
Following up on sharing some of my utility scripts, here's another one I use quite frequently – almost daily, in fact. This one automates the process of selling small amounts of HBD for HIVE on the internal market.
Why? Pure Laziness (Mostly!)
Honestly, while using the internal market UI on sites like PeakD or Ecency isn't hard, doing it repeatedly across multiple accounts just to convert small HBD balances into HIVE can feel tedious. I prefer having my liquid balances mostly in HIVE, so I built this script to handle the conversion automatically.
The Tool: An HBD Market Seller Script
It's a Python script that checks the HBD balance of accounts listed in a config file and, if the balance meets a minimum threshold, places a market order to sell that HBD for HIVE at the current best bid price.
Key Features & How It Works:
- Multi-Account Authority (Active Key!): Similar to the
claim-rewards
script, this one can operate on multiple accounts listed in a YAML file (market.yaml
by default). It uses the ACTIVE KEY of the first account in the list to perform the market sell operation for all accounts in the list.- Important Security Note: This requires granting ACTIVE authority from your alt accounts to your main controlling account. Active key authority is powerful and allows transferring funds, so only grant this if you fully understand the implications and trust the setup! This is different from the
claim-rewards
script which only needed posting authority.
- Important Security Note: This requires granting ACTIVE authority from your alt accounts to your main controlling account. Active key authority is powerful and allows transferring funds, so only grant this if you fully understand the implications and trust the setup! This is different from the
- Configuration: It reads the list of accounts and the main account's active WIF from
market.yaml
(or uses CLI args/environment variablesACTIVE_WIF
). - Thresholds & Limits: You can configure a minimum HBD balance (
--min-hbd-amount
) required to trigger a sell, preventing tiny dust transactions. You can also set a maximum amount of HBD (--max-hbd
) to sell in a single run per account. - Dry Run Mode: Includes a
--dry-run
flag to simulate the process and see what it would do without actually broadcasting any transactions. - Uses
hive-nectar
: Built using myhive-nectar
library for interacting with the Hive blockchain and market.
The Code
It's not a huge script, but it packs in the logic for handling configuration, connecting to Hive, checking balances, getting market data, and placing the sell order.
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "hive-nectar",
# "pyyaml",
# ]
#
# [tool.uv.sources]
# hive-nectar = { git = "https://github.com/thecrazygm/hive-nectar/" }
# ///
import argparse
import logging
import os
import sys
import yaml
from nectar import Hive
from nectar.account import Account
from nectar.market import Market
from nectar.nodelist import NodeList
from nectar.wallet import Wallet
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
def redact_config(config: dict) -> dict:
"""
Return a copy of config with sensitive fields redacted.
"""
redacted = dict(config) if isinstance(config, dict) else {}
for key in ["active_key", "posting_key", "wif", "private_key"]:
if key in redacted:
redacted[key] = "***REDACTED***"
return redacted
def load_config(config_path: str = None) -> dict:
"""
Load configuration from YAML file.
"""
logger.debug(f"Attempting to load config (config_path={config_path})")
if config_path:
try:
with open(config_path, "r") as f:
data = yaml.safe_load(f)
logger.info(f"Loaded config from {config_path}")
_debug_data = redact_config(data)
logger.debug(f"Config loaded: {_debug_data}")
return data or {}
except Exception as e:
logger.error(f"Failed to load config from {config_path}: {e}")
sys.exit(1)
if os.path.exists("market.yaml"):
try:
with open("market.yaml", "r") as f:
data = yaml.safe_load(f)
logger.info("Loaded config from market.yaml")
_debug_data = redact_config(data)
logger.debug(f"Config loaded: {_debug_data}")
return data or {}
except Exception as e:
logger.error(f"Failed to load config from market.yaml: {e}")
sys.exit(1)
logger.debug("No YAML config file found; using defaults/CLI/env")
return {}
def get_active_key(cli_active_key: str = None, yaml_active_key: str = None) -> str:
logger.debug(
f"Retrieving active_key (cli_active_key provided: {bool(cli_active_key)}, yaml_active_key provided: {bool(yaml_active_key)})"
)
active_key = None
if cli_active_key:
logger.info("Using active_key from --active-key argument.")
active_key = cli_active_key
elif yaml_active_key:
logger.info("Using active_key from YAML config file.")
active_key = yaml_active_key
else:
active_key = os.environ.get("ACTIVE_WIF")
if active_key:
logger.info("Using active_key from ACTIVE_WIF environment variable.")
if not active_key:
logger.error(
"Active key must be provided via --active-key, YAML config, or ACTIVE_WIF env variable."
)
sys.exit(1)
logger.debug("active_key successfully retrieved.")
return active_key
class HiveTrader:
def __init__(
self, wif: str, dry_run: bool, min_hbd_amount: float, max_hbd: float = None
):
"""
Initialize Hive trading instance.
:param wif: Active private key
:param dry_run: Whether to simulate transactions
:param min_hbd_amount: Minimum HBD to trigger sell
:param max_hbd: Maximum HBD to sell in one run (None = no limit)
"""
logger.debug(
f"Initializing HiveTrader with dynamic NodeList, dry_run={dry_run}, min_hbd_amount={min_hbd_amount}, max_hbd={max_hbd}"
)
self.min_hbd_amount = min_hbd_amount
self.max_hbd = max_hbd
try:
logger.debug("Creating and updating NodeList...")
nodelist = NodeList()
nodelist.update_nodes()
nodes = nodelist.get_hive_nodes()
logger.debug(f"Selected Hive nodes: {nodes}")
self.hive = Hive(keys=[wif], node=nodes, nobroadcast=dry_run) # Use provided wif (active key)
self.wallet = Wallet(blockchain_instance=self.hive)
self.market = Market("HIVE:HBD", blockchain_instance=self.hive)
except Exception as e:
logger.error(f"Failed to initialize Hive instance: {e}")
raise
def sell_hbd(self, account_name: str) -> bool:
"""
Sell up to max_hbd HBD for HIVE at the highest bid in the open market.
Uses the authority of the key provided during HiveTrader initialization.
:param account_name: Account whose HBD balance is checked and sold FROM.
:return: Whether the sell operation was attempted/successful.
"""
logger.debug(f"Initiating sell_hbd process for account: {account_name}.")
try:
account = Account(account_name, blockchain_instance=self.hive)
hbd_balance = account.get_balance("available", "HBD")
logger.debug(f"Fetched HBD balance for {account_name}: {hbd_balance}")
# Ensure hbd_balance exists and has amount attribute
if hbd_balance is None or not hasattr(hbd_balance, 'amount'):
logger.info(f"Could not retrieve valid HBD balance for {account_name}. Skipping.")
return False
if hbd_balance.amount <= self.min_hbd_amount:
logger.info(
f"[{account_name}] HBD balance ({hbd_balance.amount}) is below minimum ({self.min_hbd_amount}). No sell."
)
return False
available_hbd = float(hbd_balance.amount)
amount_to_sell = available_hbd
if self.max_hbd is not None and available_hbd > self.max_hbd:
logger.info(
f"[{account_name}] Limiting HBD to sell from {available_hbd:.3f} to max_hbd={self.max_hbd:.3f}"
)
amount_to_sell = self.max_hbd
logger.info(f"[{account_name}] Current HBD to sell: {amount_to_sell:.3f}")
ticker = self.market.ticker()
logger.debug(f"Market ticker: {ticker}")
# We are selling HBD (base) to get HIVE (quote)
# We need the highest bid price (someone willing to pay HIVE for HBD)
# The price is typically HIVE per HBD (quote/base)
# A lower price means less HIVE per HBD
# A higher price means more HIVE per HBD
# We want to sell at the highest bid to get the most HIVE immediately.
# `hive-nectar` market.sell places a limit sell order.
# To sell immediately, we should place a limit order slightly below the lowest ask,
# or more simply, use market.buy() to buy HIVE using HBD (effectively selling HBD).
# Using market.buy approach: Calculate how much HIVE to buy using amount_to_sell HBD
# Price is HIVE/HBD (quote/base). We use lowest_ask price.
# Amount of HIVE to buy = amount_to_sell_HBD / lowest_ask_price (HIVE/HBD)
lowest_ask_price = float(ticker["lowest_ask"]) # Price in HIVE per HBD
if lowest_ask_price <= 0:
logger.error(f"[{account_name}] Invalid lowest ask price ({lowest_ask_price}). Cannot place order.")
return False
hive_amount_to_buy = amount_to_sell / lowest_ask_price
logger.info(
f"[{account_name}] Placing order to buy {hive_amount_to_buy:.3f} HIVE using {amount_to_sell:.3f} HBD (Price: {lowest_ask_price:.6f} HIVE/HBD)"
)
logger.debug(
f"Calling market.buy(price={lowest_ask_price}, amount={hive_amount_to_buy}, account={account_name})"
)
# Use the market.buy method from nectar, specifying the account
tx = self.market.buy(lowest_ask_price, hive_amount_to_buy, account=account_name)
if self.hive.nobroadcast:
logger.info(f"[DRY RUN] Would have bought {hive_amount_to_buy:.3f} HIVE with {amount_to_sell:.3f} HBD for {account_name}.")
# In dry run, tx might be the unsigned transaction dict
logger.debug(f"[DRY RUN] Transaction details: {tx}")
else:
logger.info(f"[{account_name}] Market buy order placed successfully.")
logger.debug(f"Transaction details: {tx}")
return True
except Exception as e:
logger.exception(
f"An error occurred while selling HBD for account: {account_name}: {e}"
)
return False
def main():
"""Main execution function."""
parser = argparse.ArgumentParser(description="Hive HBD Market Seller")
parser.add_argument(
"-k",
"--active-key",
type=str,
default=None,
help="Active WIF (private key) for authority account. If omitted, uses ACTIVE_WIF env variable or YAML config.",
)
parser.add_argument(
"-c",
"--config",
type=str,
default=None,
help="Path to YAML config file. If omitted, uses market.yaml if available.",
)
parser.add_argument(
"-d",
"--dry-run",
action="store_true",
help="Simulate market sell without broadcasting transactions.",
)
parser.add_argument(
"-m",
"--min-hbd-amount",
type=float,
default=None,
help="Minimum HBD amount to trigger a sell (overrides config/default).",
)
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
parser.add_argument(
"-x",
"--max-hbd",
type=float,
default=None,
help="Maximum HBD to sell in one run (overrides config/default).",
)
args = parser.parse_args()
logger.debug(f"Parsed CLI arguments: {args}")
if args.debug:
logger.setLevel(logging.DEBUG)
logger.debug("Debug logging enabled.")
# Load config from YAML if provided
yaml_config = load_config(args.config)
# Gather config values with priority: CLI > YAML > defaults/env
# Redact active_key in YAML config debug log
_yaml_config_debug = redact_config(yaml_config)
logger.debug(f"YAML config: {_yaml_config_debug}")
accounts = yaml_config.get("accounts")
if not accounts or not isinstance(accounts, list) or len(accounts) == 0:
logger.error(
"No accounts list found in config. Please provide a list of accounts in your YAML config."
)
sys.exit(1)
authority_account = accounts[0] # The account whose active key is provided
logger.info(f"Using authority of first account listed: {authority_account}")
active_key = get_active_key(args.active_key, yaml_config.get("active_key"))
dry_run = args.dry_run or yaml_config.get("dry_run", False)
min_hbd_amount = (
args.min_hbd_amount
if args.min_hbd_amount is not None
else yaml_config.get("min_hbd_amount", 0.001) # Default minimum is 0.001 HBD
)
max_hbd = (
args.max_hbd if args.max_hbd is not None else yaml_config.get("max_hbd", None) # Default is no max
)
logger.debug(
f"Final config: authority_account={authority_account}, dry_run={dry_run}, min_hbd_amount={min_hbd_amount}, max_hbd={max_hbd}"
)
try:
# Initialize HiveTrader with the active key of the authority account
trader = HiveTrader(active_key, dry_run, min_hbd_amount, max_hbd)
logger.debug(
"HiveTrader initialized; proceeding to sell HBD for all configured accounts"
)
# Loop through all accounts in the config list
for account_name in accounts:
logger.info(f"--- Processing account: {account_name} ---")
# Attempt to sell HBD FROM this account, using the authority key provided to HiveTrader
trader.sell_hbd(account_name=account_name)
logger.info("--- All accounts processed ---")
except Exception as e:
logger.error(f"Critical error during trader initialization or processing: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Usefulness/Caveats
Like I mentioned, this might be a bit niche compared to something like the multi-account reward claimer. It solves my specific workflow preference of keeping liquid HIVE rather than HBD. The most important thing to remember is that this script requires your ACTIVE KEY and relies on you granting ACTIVE authority from your alt accounts to your main account if you want to use the multi-account feature. Active authority is powerful – use it carefully!
Seeing it Run
Here's what a normal run might look like:
And with debug mode on, you get a lot more detail about the balances checked and market prices, but I just ran it so No Sell:
Get the Code
Since this feels a bit more specialized, I haven't set up a full repo for it currently. You can grab the code directly from this Gist:
https://gist.github.com/TheCrazyGM/2ec22939be0396828fa575420c80d0bd
Maybe it's useful to someone else out there who also prefers HIVE over HBD liquidly and manages multiple accounts. Let me know what you think!
As always,
Michael Garcia a.k.a. TheCrazyGM
Neat tool. Is yaml something yall made?
No. https://yaml.org/ it's another markup language similar to json, it's just a bit easier for human readability.
Thank you, gonna go read some about it.
Thanks for this tool buddy🤗 this is very helpful for us hivers
While I don't really need it for myself personally, I can definitely see this being very helpful for some Hivers! Specialized tools like this are a very good thing indeed! Thank you! 😁 🙏 💚 ✨ 🤙
Impressive.