SUBTC-CLI V2 — Unified Bitcoin CLI (Testnet & Mainnet)

permalink SUBTC
#bitcoin#bitcoin-cli#btc#mainnet#subtc-cli#testnet#tools

SUBTC CLI V2 is here.

This version introduces a unified command system, multi-network support, and event-driven payment tools.

Everything runs with:

./subtc

No complexity.
No noise.
Just commands that work.

# What’s New

- Unified CLI structure
- Testnet + Mainnet support
- Network override (flag + env)
- Inbox (payment intent)
- Wait (long-poll / webhook)
- Poll (loop mode)
- Idempotent send
- Persistent config file

# Multi-Network
./subtc config set-net test
./subtc config set-net main
./subtc --net=main wallet list
SUBTC_NET=main ./subtc wallet creat

# Quick Start

./subtc key create
./subtc wallet create
./subtc wallet receive <wid>
./subtc wallet balance <wid>

# Send

./subtc send <wid> <addr> <sat>
./subtc send <wid> <addr> <sat> <idem-key>

Rules:
- Minimum: 50000 sat
- Fee: 3%
- Mainnet requires YES confirmation

# Inbox

./subtc inbox <wid> <expected_sat>

Create a payment target.
Useful for apps, bots, and automation.

# Wait

./subtc wait <wid> <addr> <sat>

Blocking mode.
Wait until funds arrive.

./subtc wait <wid> <addr> <sat> 300

Custom timeout.

./subtc wait <wid> <addr> <sat> 300 https://example.com/webhook

Webhook mode.

# Poll

./subtc poll <wid> <addr> <sat>
./subtc poll <wid> <addr> <sat> --loop
./subtc poll <wid> <addr> <sat> --loop --interval=5

# Config

./subtc config show
./subtc config set-key <key>
./subtc config set-net test

Config file:
~/.subtc.json

{
"key": "SUBTC-KEY-...",
"network": "test"
}

# Health

./subtc health

# Why This Matters

SUBTC CLI is not just a tool.

It is a programmable interface for Bitcoin payments.

- Simple for beginners
- Powerful for developers
- Ready for automation
- Works with APIs, bots, and scripts

# Use Cases

- Payment bots
- Donation systems
- Testnet apps
- Automation scripts
- AI agents

# Notes

- All commands are curl-first compatible
- Built for real usage, not demos
- Designed for developers

# Next

- Docker support
- Tor support
- Mobile usage (Termux / iOS)
- More tools under /tag/tools

Stay tuned.

Code : subtc-cli.go V2

// subtc-cli.go — SUBTC CLI V2 · Mainnet + Testnet · Single File
// Gateway V4.2 · Bitcoin Only · Zero external dependencies
//
// Build:  go build -o subtc subtc-cli.go
// Usage:  ./subtc help

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

// ─────────────────────────────────────────────────────────────────────────────
// Constants
// ─────────────────────────────────────────────────────────────────────────────

const (
	BaseURL    = "https://api.subtc.net"
	ConfigFile = ".subtc.json"
	MinSendSat = 50_000
	Version    = "2.0.0"
)

// ─────────────────────────────────────────────────────────────────────────────
// Terminal colors
// ─────────────────────────────────────────────────────────────────────────────

const (
	cReset  = "\033[0m"
	cBold   = "\033[1m"
	cGreen  = "\033[32m"
	cYellow = "\033[33m"
	cRed    = "\033[31m"
	cCyan   = "\033[36m"
	cGray   = "\033[90m"
	cOrange = "\033[38;5;214m"
)

// ─────────────────────────────────────────────────────────────────────────────
// Config  (~/.subtc.json)
// ─────────────────────────────────────────────────────────────────────────────

type Config struct {
	Key     string `json:"key"`
	Network string `json:"network"` // "test" | "main"
}

func configPath() string {
	home, _ := os.UserHomeDir()
	return filepath.Join(home, ConfigFile)
}

func loadConfig() Config {
	f, err := os.Open(configPath())
	if err != nil {
		return Config{Network: "test"}
	}
	defer f.Close()
	var c Config
	json.NewDecoder(f).Decode(&c)
	if c.Network == "" {
		c.Network = "test"
	}
	return c
}

func (c Config) save() error {
	f, err := os.OpenFile(configPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
	if err != nil {
		return err
	}
	defer f.Close()
	enc := json.NewEncoder(f)
	enc.SetIndent("", "  ")
	return enc.Encode(c)
}

func (c Config) key() string {
	if v := os.Getenv("SUBTC_KEY"); v != "" {
		return v
	}
	return c.Key
}

func (c Config) net() string {
	if v := os.Getenv("SUBTC_NET"); v == "main" || v == "test" {
		return v
	}
	return c.Network
}

func (c Config) requireKey() {
	if c.key() == "" {
		die("no API key — run: subtc key create")
	}
}

func (c Config) netLabel() string {
	if c.net() == "main" {
		return cOrange + "mainnet" + cReset
	}
	return cCyan + "testnet" + cReset
}

func (c Config) isMainnet() bool {
	return c.net() == "main"
}

// ─────────────────────────────────────────────────────────────────────────────
// HTTP client
// ─────────────────────────────────────────────────────────────────────────────

var httpClient = &http.Client{Timeout: 360 * time.Second}

type J = map[string]any

func apiGET(path string) J {
	resp, err := httpClient.Get(BaseURL + path)
	must(err)
	defer resp.Body.Close()
	return decode(resp.Body)
}

func apiPOST(cfg Config, mode string, body J, idempotency string) J {
	if body == nil {
		body = J{}
	}
	raw, _ := json.Marshal(body)

	req, err := http.NewRequest(http.MethodPost,
		fmt.Sprintf("%s/v1/btc?mode=%s", BaseURL, mode),
		bytes.NewReader(raw),
	)
	must(err)
	req.Header.Set("Content-Type", "application/json")
	if k := cfg.key(); k != "" {
		req.Header.Set("X-SUBTC-KEY", k)
	}
	if idempotency != "" {
		req.Header.Set("X-SUBTC-IDEMPOTENCY", idempotency)
	}

	resp, err := httpClient.Do(req)
	must(err)
	defer resp.Body.Close()
	return decode(resp.Body)
}

func decode(r io.Reader) J {
	raw, err := io.ReadAll(r)
	must(err)
	var result J
	if err := json.Unmarshal(raw, &result); err != nil {
		die("bad JSON from server: " + string(raw))
	}
	return result
}

// ─────────────────────────────────────────────────────────────────────────────
// Display helpers
// ─────────────────────────────────────────────────────────────────────────────

func header(title string) {
	fmt.Printf("\n%s%s%s\n%s\n", cBold, title, cReset, strings.Repeat("─", 44))
}

func info(key, val string) {
	fmt.Printf("%s%-20s%s %s\n", cCyan, key+":", cReset, val)
}

func ok(msg string) {
	fmt.Printf("%s✓ %s%s\n", cGreen, msg, cReset)
}

func warn(msg string) {
	fmt.Printf("%s! %s%s\n", cYellow, msg, cReset)
}

func die(msg string) {
	fmt.Fprintf(os.Stderr, "%s✗ %s%s\n", cRed, msg, cReset)
	os.Exit(1)
}

func must(err error) {
	if err != nil {
		die(err.Error())
	}
}

func printJSON(v J) {
	b, _ := json.MarshalIndent(v, "", "  ")
	fmt.Println(cGray + string(b) + cReset)
}

func fmtSat(sat float64) string {
	return fmt.Sprintf("%.0f sat  (%.8f BTC)", sat, sat/1e8)
}

func str(v J, key string) string {
	if x, ok := v[key]; ok {
		switch t := x.(type) {
		case string:
			return t
		case float64:
			return fmt.Sprintf("%.0f", t)
		}
	}
	return ""
}

func nested(v J, key string) string {
	if r, ok := v["result"].(J); ok {
		return str(r, key)
	}
	return str(v, key)
}

func resultMap(v J) J {
	if r, ok := v["result"].(J); ok {
		return r
	}
	return v
}

func confirmMainnet(action string) {
	fmt.Printf("\n%s⚠  MAINNET — %s%s\n", cOrange, action, cReset)
	fmt.Printf("   Type %sYES%s to confirm: ", cBold, cReset)
	var input string
	fmt.Scanln(&input)
	if input != "YES" {
		fmt.Println("Aborted.")
		os.Exit(0)
	}
}

// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────

func cmdHealth(cfg Config) {
	result := apiGET("/health")
	header("Health · " + cfg.netLabel())
	printJSON(result)
}

func cmdKeyCreate(cfg *Config) {
	result := apiPOST(*cfg, "key_create", nil, "")
	header("Key Created")
	key := nested(result, "key")
	if key == "" {
		printJSON(result)
		return
	}
	info("key", key)
	cfg.Key = key
	if err := cfg.save(); err != nil {
		warn("could not save key: " + err.Error())
	} else {
		ok("Key saved → ~/.subtc.json")
	}
}

func cmdKeyStatus(cfg Config) {
	cfg.requireKey()
	result := apiPOST(cfg, "key_status", nil, "")
	header("Key Status")
	printJSON(result)
}

func cmdWalletCreate(cfg Config) {
	cfg.requireKey()
	result := apiPOST(cfg, "wallet_create", J{"net": cfg.net()}, "")
	header("Wallet Created · " + cfg.netLabel())
	if wid := nested(result, "wallet_id"); wid != "" {
		info("wallet_id", wid)
	}
	printJSON(result)
}

func cmdWalletList(cfg Config) {
	cfg.requireKey()
	result := apiPOST(cfg, "wallet_list", nil, "")
	header("Wallets · " + cfg.netLabel())
	printJSON(result)
}

func cmdWalletReceive(cfg Config, walletID string) {
	cfg.requireKey()
	result := apiPOST(cfg, "wallet_receive", J{"wallet_id": walletID}, "")
	header("Receive Address · " + cfg.netLabel())
	if addr := nested(result, "address"); addr != "" {
		info("address", addr)
	}
	printJSON(result)
}

func cmdWalletBalance(cfg Config, walletID string) {
	cfg.requireKey()
	result := apiPOST(cfg, "wallet_balance", J{"wallet_id": walletID}, "")
	header("Balance · " + walletID[:min(12, len(walletID))] + "…  " + cfg.netLabel())
	res := resultMap(result)
	if s, ok2 := res["balance_sat"].(float64); ok2 {
		info("balance", fmtSat(s))
	}
	if s, ok2 := res["unconfirmed_sat"].(float64); ok2 {
		info("unconfirmed", fmtSat(s))
	}
	printJSON(result)
}

func cmdSend(cfg Config, walletID, toAddr string, amountSat int64, idem string) {
	cfg.requireKey()
	if amountSat < MinSendSat {
		die(fmt.Sprintf("minimum send is %d sat (got %d)", MinSendSat, amountSat))
	}
	if idem == "" {
		idem = fmt.Sprintf("send-%d", time.Now().Unix())
	}

	header("Send BTC · " + cfg.netLabel())
	info("wallet_id",   walletID)
	info("to",          toAddr)
	info("amount",      fmtSat(float64(amountSat)))
	info("fee",         "3% (service + network)")
	info("idempotency", idem)
	fmt.Println()

	if cfg.isMainnet() {
		confirmMainnet(fmt.Sprintf("sending %s to %s", fmtSat(float64(amountSat)), toAddr))
	}

	result := apiPOST(cfg, "wallet_send", J{
		"wallet_id":  walletID,
		"to":         toAddr,
		"amount_sat": amountSat,
	}, idem)

	res := resultMap(result)
	if txid := str(res, "txid"); txid != "" {
		ok("Transaction broadcast")
		info("txid", txid)
		if s, ok2 := res["sent_sat"].(float64); ok2 {
			info("sent_sat", fmtSat(s))
		}
		if s, ok2 := res["service_fee_sat"].(float64); ok2 {
			info("fee_sat", fmtSat(s))
		}
	}
	printJSON(result)
}

func cmdInbox(cfg Config, walletID string, expectedSat int64) {
	cfg.requireKey()
	result := apiPOST(cfg, "inbox_create", J{
		"wallet_id":    walletID,
		"expected_sat": expectedSat,
	}, "")
	header("Inbox · " + cfg.netLabel())
	if addr := nested(result, "address"); addr != "" {
		ok("Inbox ready")
		info("address",  addr)
		info("expected", fmtSat(float64(expectedSat)))
	}
	printJSON(result)
}

func cmdWait(cfg Config, walletID, address string, expectedSat int64, timeoutSec int, callbackURL string) {
	cfg.requireKey()

	body := J{
		"wallet_id":    walletID,
		"address":      address,
		"expected_sat": expectedSat,
		"timeout_sec":  timeoutSec,
	}
	if callbackURL != "" {
		body["callback_url"] = callbackURL
	}

	header("Waiting for Funds · " + cfg.netLabel())
	info("wallet_id",    walletID)
	info("address",      address)
	info("expected_sat", fmtSat(float64(expectedSat)))
	info("timeout",      fmt.Sprintf("%d sec", timeoutSec))
	if callbackURL != "" {
		info("callback_url", callbackURL)
		info("mode",         "webhook (fire & forget)")
	} else {
		info("mode", "long-poll (blocking…)")
	}
	fmt.Println()

	result := apiPOST(cfg, "wallet_wait_event", body, "")
	res := resultMap(result)
	if reached, _ := res["reached"].(bool); reached {
		ok("Payment confirmed!")
	}
	if s, ok2 := res["received_sat"].(float64); ok2 {
		info("received", fmtSat(s))
	}
	printJSON(result)
}

func cmdPoll(cfg Config, walletID, address string, expectedSat int64, loop bool, intervalSec int) {
	cfg.requireKey()

	header("Poll · " + cfg.netLabel())
	info("wallet_id",    walletID)
	info("address",      address)
	info("expected_sat", fmtSat(float64(expectedSat)))
	if loop {
		info("mode", fmt.Sprintf("loop every %d sec — Ctrl+C to stop", intervalSec))
	}
	fmt.Println()

	poll := func() bool {
		result := apiPOST(cfg, "wallet_poll", J{
			"wallet_id":    walletID,
			"address":      address,
			"expected_sat": expectedSat,
		}, "")
		res       := resultMap(result)
		reached, _ := res["reached"].(bool)
		recSat, _  := res["received_sat"].(float64)
		icon := "⏳"
		if reached {
			icon = "✅"
		}
		fmt.Printf("%s  received: %-26s reached: %v\n", icon, fmtSat(recSat), reached)
		return reached
	}

	if !loop {
		poll()
		return
	}
	for {
		if poll() {
			ok("Payment confirmed — stopping loop")
			break
		}
		time.Sleep(time.Duration(intervalSec) * time.Second)
	}
}

func cmdConfigShow(cfg Config) {
	header("Config")
	keyDisplay := cfg.Key
	if len(keyDisplay) > 12 {
		keyDisplay = keyDisplay[:12] + "••••••••"
	}
	if keyDisplay == "" {
		keyDisplay = "(none — run: subtc key create)"
	}
	info("key",     keyDisplay)
	info("network", cfg.net()+"  "+cfg.netLabel())
	fmt.Println("\n  file:         ~/.subtc.json")
	fmt.Println("  env-override: SUBTC_KEY · SUBTC_NET")
}

// ─────────────────────────────────────────────────────────────────────────────
// Help
// ─────────────────────────────────────────────────────────────────────────────

func printHelp(cfg Config) {
	fmt.Printf(`
%sSUBTC CLI V%s%s  ·  Gateway V4.2  ·  Zero dependencies
Active network: %s

%sUsage:%s
  subtc [--net=test|main] <command> [args]

%sNetwork:%s
  subtc config set-net main              Switch to mainnet (persistent)
  subtc config set-net test              Switch to testnet (persistent)
  subtc --net=main <cmd>                 One-shot network override

%sKey:%s
  subtc key create                       Create & save API key
  subtc key status                       Verify current key

%sWallet:%s
  subtc wallet create                    Create wallet on active network
  subtc wallet list                      List all wallets
  subtc wallet receive  <wid>            Generate receive address
  subtc wallet balance  <wid>            Show balance (SAT + BTC)

%sSend:%s
  subtc send <wid> <addr> <sat> [idem]   Idempotent · min 50,000 sat
                                         Mainnet requires YES confirmation

%sInbox:%s
  subtc inbox <wid> <expected_sat>       Pre-configured receive inbox

%sWait / Poll:%s
  subtc wait <wid> <addr> <sat> [timeout_sec] [callback_url]
  subtc poll <wid> <addr> <sat> [--loop] [--interval=N]

%sConfig:%s
  subtc config show
  subtc config set-key <key>
  subtc config set-net <test|main>
  subtc health

%sExamples:%s
  subtc key create
  subtc config set-net main
  subtc wallet create
  subtc --net=test wallet balance w_abc123
  subtc send w_abc123 bc1qXXX 50000

%sUnits:%s   1 BTC = 100,000,000 SAT · Fee: 3%% · Min: 50,000 SAT
%sDocs:%s    https://subtc.net/api
`,
		cBold, Version, cReset,
		cfg.netLabel(),
		cBold, cReset,
		cCyan, cReset,
		cCyan, cReset,
		cCyan, cReset,
		cCyan, cReset,
		cCyan, cReset,
		cCyan, cReset,
		cCyan, cReset,
		cGreen, cReset,
		cGray, cReset,
		cGray, cReset,
	)
}

// ─────────────────────────────────────────────────────────────────────────────
// Argument helpers
// ─────────────────────────────────────────────────────────────────────────────

func argSat(s, name string) int64 {
	v, err := strconv.ParseInt(s, 10, 64)
	if err != nil || v <= 0 {
		die("invalid " + name + ": " + s)
	}
	return v
}

func argInt(s string, def int) int {
	v, err := strconv.Atoi(s)
	if err != nil {
		return def
	}
	return v
}

func hasFlag(args []string, flag string) bool {
	for _, a := range args {
		if a == flag {
			return true
		}
	}
	return false
}

func flagVal(args []string, prefix, def string) string {
	for _, a := range args {
		if strings.HasPrefix(a, prefix) {
			return a[len(prefix):]
		}
	}
	return def
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func parseNetFlag(args []string) (string, []string) {
	net := ""
	var out []string
	for i := 0; i < len(args); i++ {
		switch {
		case strings.HasPrefix(args[i], "--net="):
			net = args[i][6:]
		case args[i] == "--net" && i+1 < len(args):
			net = args[i+1]
			i++
		default:
			out = append(out, args[i])
		}
	}
	return net, out
}

// ─────────────────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────────────────

func main() {
	cfg := loadConfig()

	if len(os.Args) < 2 {
		printHelp(cfg)
		return
	}

	netOverride, args := parseNetFlag(os.Args[1:])
	if netOverride == "main" || netOverride == "test" {
		cfg.Network = netOverride
	} else if netOverride != "" {
		die("--net must be 'test' or 'main'")
	}

	if len(args) == 0 {
		printHelp(cfg)
		return
	}

	cmd  := args[0]
	rest := args[1:]

	switch cmd {

	case "health":
		cmdHealth(cfg)

	case "key":
		if len(rest) == 0 {
			die("usage: subtc key <create|status>")
		}
		switch rest[0] {
		case "create":
			cmdKeyCreate(&cfg)
		case "status":
			cmdKeyStatus(cfg)
		default:
			die("unknown key sub-command: " + rest[0])
		}

	case "wallet":
		if len(rest) == 0 {
			die("usage: subtc wallet <create|list|receive|balance>")
		}
		switch rest[0] {
		case "create":
			cmdWalletCreate(cfg)
		case "list":
			cmdWalletList(cfg)
		case "receive":
			if len(rest) < 2 {
				die("usage: subtc wallet receive <wallet_id>")
			}
			cmdWalletReceive(cfg, rest[1])
		case "balance":
			if len(rest) < 2 {
				die("usage: subtc wallet balance <wallet_id>")
			}
			cmdWalletBalance(cfg, rest[1])
		default:
			die("unknown wallet sub-command: " + rest[0])
		}

	case "send":
		if len(rest) < 3 {
			die("usage: subtc send <wallet_id> <to_addr> <amount_sat> [idempotency_key]")
		}
		idem := ""
		if len(rest) >= 4 {
			idem = rest[3]
		}
		cmdSend(cfg, rest[0], rest[1], argSat(rest[2], "amount_sat"), idem)

	case "inbox":
		if len(rest) < 2 {
			die("usage: subtc inbox <wallet_id> <expected_sat>")
		}
		cmdInbox(cfg, rest[0], argSat(rest[1], "expected_sat"))

	case "wait":
		if len(rest) < 3 {
			die("usage: subtc wait <wallet_id> <address> <expected_sat> [timeout_sec] [callback_url]")
		}
		timeout     := 300
		callbackURL := ""
		if len(rest) >= 4 {
			timeout = argInt(rest[3], 300)
		}
		if len(rest) >= 5 {
			callbackURL = rest[4]
		}
		cmdWait(cfg, rest[0], rest[1], argSat(rest[2], "expected_sat"), timeout, callbackURL)

	case "poll":
		if len(rest) < 3 {
			die("usage: subtc poll <wallet_id> <address> <expected_sat> [--loop] [--interval=N]")
		}
		loop     := hasFlag(rest[3:], "--loop") || hasFlag(rest[3:], "-l")
		interval := argInt(flagVal(rest[3:], "--interval=", "3"), 3)
		cmdPoll(cfg, rest[0], rest[1], argSat(rest[2], "expected_sat"), loop, interval)

	case "config":
		sub := ""
		if len(rest) > 0 {
			sub = rest[0]
		}
		switch sub {
		case "", "show":
			cmdConfigShow(cfg)
		case "set-key":
			if len(rest) < 2 {
				die("usage: subtc config set-key <key>")
			}
			cfg.Key = rest[1]
			must(cfg.save())
			ok("API key saved → ~/.subtc.json")
		case "set-net":
			if len(rest) < 2 {
				die("usage: subtc config set-net <test|main>")
			}
			n := rest[1]
			if n != "test" && n != "main" {
				die("network must be 'test' or 'main'")
			}
			cfg.Network = n
			must(cfg.save())
			ok(fmt.Sprintf("Default network → %s", cfg.netLabel()))
		default:
			die("unknown config sub-command: " + sub)
		}

	case "help", "--help", "-h":
		printHelp(cfg)

	default:
		warn("unknown command: " + cmd)
		printHelp(cfg)
	}
}

This guide shows a complete payment flow using SUBTC CLI V2
with live testnet results.

# Step 1: Create Key

./subtc key create

Key Created
────────────────────────────────────────────
key:                 SUBTC-KEY-79821c194cd50ff0ce948920532fd8953a8c417de3d61c90
✓ Key saved → ~/.subtc.json

# Step 2: Check Key Status

./subtc key status

Key Status
────────────────────────────────────────────
{
  "ok": true,
  "request_id": "fcf859dae07b00d7",
  "result": {
    "key": "SUBTC-KEY-79821c194cd50ff0ce948920532fd8953a8c417de3d61c90",
    "wallet_count": 0
  }
}

# Step 3: Set Network to Testnet

./subtc config set-net test
✓ Default network → testnet

# Step 4: Create Wallet

./subtc wallet create

Wallet Created · testnet
────────────────────────────────────────────
wallet_id: w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e

# Step 5: Generate Receive Address

./subtc wallet receive w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e

Receive Address · testnet
────────────────────────────────────────────
address: tb1qhcy9w09yk22ppf3fdg6twnjh2zvgarf4zlsw4s

# Step 6: Check Wallet Balance

./subtc wallet balance w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e

Balance · w_acfd7aa7e5…  testnet
────────────────────────────────────────────
balance: 191849 sat  (0.00191849 BTC)
unconfirmed: 191849 sat  (0.00191849 BTC)

# Step 7: Wait for Funds (Blocking)

./subtc wait w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e tb1qhcy9w09yk22ppf3fdg6twnjh2zvgarf4zlsw4s 30000

Waiting for Funds · testnet
────────────────────────────────────────────
wallet_id: w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e
address: tb1qhcy9w09yk22ppf3fdg6twnjh2zvgarf4zlsw4s
expected_sat: 30000 sat
received: 191849 sat

# Step 8: Send BTC

./subtc send w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w 50000

Send BTC · testnet
────────────────────────────────────────────
wallet_id: w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e
to: tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w
amount: 50000 sat  (0.00050000 BTC)
fee: 3% (service + network)
✓ Transaction broadcast
txid: 7360354a92094b97f20df66eab5c4da4bfa6e3ceba86ae4e6d5a077a06d7809f
sent_sat: 48500 sat
fee_sat: 1500 sat

# Step 9: Check Balance After Send

./subtc wallet balance w_acfd7aa7e54f31d533c4f6eed2cf18c9c192957c2f2e

Balance · w_acfd7aa7e5…  testnet
────────────────────────────────────────────
balance: 41849 sat  (0.00041849 BTC)
unconfirmed: 0 sat

# Step 10: Health Check

./subtc health

Health · testnet
────────────────────────────────────────────
{
  "ok": true,
  "request_id": "ee7d76f45093c3fe",
  "result": {
    "coin": "btc",
    "configured": true,
    "fee_bps": 300,
    "min_send_sat": 20000
  }
}

# Step 11: Show Config

./subtc config show

Config
────────────────────────────────────────────
key: SUBTC-KEY-79••••••••
network: test  testnet
file: ~/.subtc.json
env-override: SUBTC_KEY · SUBTC_NET

# ✅ Summary

- Created key & wallet
- Generated receive address
- Checked balance
- Waited for funds
- Sent BTC
- Verified final balance
- Health check & config review

All commands tested successfully on **V2**.