SUBTC-CLI V2 — Unified Bitcoin CLI (Testnet & Mainnet)
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**.