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.
  • Configuration: It reads the list of accounts and the main account's active WIF from market.yaml (or uses CLI args/environment variables ACTIVE_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 my hive-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:

Running the script to sell HBD.

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:

Debug run output

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



0
0
0.000
6 comments
avatar

Thanks for this tool buddy🤗 this is very helpful for us hivers

0
0
0.000
avatar

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! 😁 🙏 💚 ✨ 🤙

0
0
0.000