Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c734de2
docs(spec): base58 signers + delegate support + onboarding design
0xtristan Jun 18, 2026
50f70b4
chore: gitignore docs/superpowers working docs
0xtristan Jun 18, 2026
2b56bb3
feat(bullet): parse signer keys from hex or base58
0xtristan Jun 18, 2026
99e3981
fix(bullet): silence clippy format_collect in key tests; tidy dep order
0xtristan Jun 18, 2026
07af5d4
feat(bullet): accept base58 keys via private_key / BB_BULLET_PRIVATE_KEY
0xtristan Jun 18, 2026
a582adb
docs(bullet): update key-precedence doc comments for private_key/base58
0xtristan Jun 18, 2026
b1ecad7
feat(bullet): resolve delegate signer to master via delegateOf
0xtristan Jun 18, 2026
a70ecb6
fix(bullet): propagate delegateOf body-read errors as BotError
0xtristan Jun 18, 2026
9195a59
feat(bullet): read account data + subscribe via resolved master address
0xtristan Jun 18, 2026
d9d0c10
docs: base58 keys, delegate wallets, from-scratch onboarding
0xtristan Jun 18, 2026
3217556
docs(agents): use canonical BB_BULLET_PRIVATE_KEY in setup example
0xtristan Jun 18, 2026
5fde9b1
docs: use canonical BB_BULLET_PRIVATE_KEY in example configs; fix env…
0xtristan Jun 18, 2026
5524e26
fix(review): robust env key resolution, retryable delegateOf 5xx/429,…
0xtristan Jun 18, 2026
9265192
feat(hyperliquid): support API/agent wallets via account_address
0xtristan Jun 18, 2026
dd6998b
feat(bullet): log a hint when a signer resolves to its own account
0xtristan Jun 19, 2026
1cd97a6
docs(config): document BB_HYPERLIQUID_ACCOUNT_ADDRESS + network-match…
0xtristan Jun 19, 2026
8432e99
docs(config): clarify network matches the delegate's network (testnet…
0xtristan Jun 19, 2026
375c0e1
feat(hyperliquid): log whether reads use the signer or a master account
0xtristan Jun 19, 2026
e5d4f34
fix(bullet): treat delegateOf 400 'is not a delegate' as self
0xtristan Jun 19, 2026
064fbce
feat!: standardize on base58 for Bullet, hex for Hyperliquid
0xtristan Jun 19, 2026
34882df
feat: auto-load .env, unify key resolution, add Hyperliquid key_file
0xtristan Jun 19, 2026
cefd022
feat(hyperliquid): read collateral from spot balance for unified acco…
0xtristan Jun 21, 2026
e7e7e2c
fix(hyperliquid): round order prices to 5 sig figs; flatten orphaned …
0xtristan Jun 22, 2026
b68afa7
feat(cli): flatten closes all configured venues (Bullet + Hyperliquid)
0xtristan Jun 22, 2026
f4630e1
fix(hyperliquid): normalize cloid format so own fills are recognized
0xtristan Jun 22, 2026
34f0878
fix(harness): flatten on SIGTERM, not just Ctrl-C
0xtristan Jun 22, 2026
2dcaeeb
fix(review): address bot findings on flatten + funding-arb cleanup
0xtristan Jun 22, 2026
8695a00
fix(review): treat empty BB_*_KEY_FILE as absent in load_config merges
0xtristan Jun 22, 2026
669c47f
fix(keygen): faucet curl needs a browser User-Agent; restructure READ…
0xtristan Jun 22, 2026
3b077da
feat(cli): add 'bb-bot faucet' command + web-faucet fallback
0xtristan Jun 23, 2026
76154b8
test(funding-arb): cover incomplete-entry cleanup (filled leg closed,…
0xtristan Jun 23, 2026
0c15873
chore(deps): bump quinn-proto 0.11.14 → 0.11.15 (RUSTSEC-2026-0185)
0xtristan Jun 23, 2026
369ff5b
fix(review): address remaining bot findings
0xtristan Jun 23, 2026
74a9f58
fix(funding-arb): don't treat a capped partial cleanup as flat
0xtristan Jun 23, 2026
0f03852
fix(review): delegateOf retries; emergency_flatten sizes from live po…
0xtristan Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copy to .env (gitignored) and fill in. `bb-bot` auto-loads ./.env at startup,
# so `cp .env.example .env` then `cargo run -- run ...` just works. Real
# environment variables already set take precedence. Use `--env-file <path>` to
# load a different file. Never commit secrets into config/*.toml.
#
# Paste each key exactly as the venue's UI gives it to you:
# Bullet → base58
# Hyperliquid → hex (0x…)

# --- Bullet (base58) ---
# Delegate signer secret from the Bullet delegate UI (base58; hex also works).
# A delegate can only trade (no deposit/withdraw) and is revocable. The bot
# resolves a delegate to its master account automatically for reads.
BB_BULLET_PRIVATE_KEY=
# Alternatively, point at a file containing the key string (e.g. from
# `bb-bot keygen`). Takes precedence over BB_BULLET_PRIVATE_KEY.
# BB_BULLET_KEY_FILE=/path/to/id.key

# --- Hyperliquid (hex) ---
# API-wallet key (secp256k1 hex) from https://app.hyperliquid.xyz/API.
BB_HYPERLIQUID_PRIVATE_KEY=
# Your MAIN account address (0x… from the HL UI). Required when the key above is
# an API wallet — positions/balances/fills live on the main account, not the API
# wallet. Leave unset if the key is your main wallet itself.
BB_HYPERLIQUID_ACCOUNT_ADDRESS=
# Alternatively, a file containing the hex key. Takes precedence over the key string.
# BB_HYPERLIQUID_KEY_FILE=/path/to/hl.key
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@

# Local/experimental configs — put personal configs here
config/local/

# Superpowers brainstorming/specs (local working docs)
docs/superpowers/
44 changes: 29 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Generate a keypair (first time):

```sh
cargo run --bin bb-bot -- keygen --network testnet
# → writes ~/.config/bullet/id.json (0600), prints address + faucet curl
# → writes ~/.config/bullet/id.key (base58 secret, 0600), prints address + faucet curl
```

Fund and onboard the account (first time). The faucet credits the on-chain
Expand All @@ -49,18 +49,18 @@ order placement fail with `user_variants not found`:
cargo run --bin bb-bot -- deposit --network testnet --asset USDC --amount 5000
```

Run a bot (default: reads `~/.config/bullet/id.json`):
Run a bot (default: reads `~/.config/bullet/id.key`):

```sh
cargo run --bin bb-bot -- run --config config/simple-mm-example.toml
```

Or point at an explicit keystore / use hex for CI:
Or point at an explicit key file / pass a key string for CI:

```sh
export BB_BULLET_KEY_FILE="/path/to/keystore.json" # preferred
# OR
export BB_BULLET_PRIVATE_KEY_HEX="0x..." # fallback
export BB_BULLET_KEY_FILE="/path/to/id.key" # preferred
# OR (base58 from Phantom/delegation export, or hex)
export BB_BULLET_PRIVATE_KEY="<base58-or-hex>" # fallback
```

## Architecture — the harness, feeds, and actors
Expand Down Expand Up @@ -294,6 +294,13 @@ the full walkthrough including reconnect patterns and the `Trade` /
`InfoClient::with_reconnect` handles reconnection. Symbol mapping: Bullet
`"BTC-USD"` ↔ HL `"BTC"`. `ActiveAssetCtx` provides real funding rates;
`AllMids` remains a mark-price fallback when no funding field is present.
**API/agent wallets**: set `account_address` (env `BB_HYPERLIQUID_ACCOUNT_ADDRESS`)
to the master account. The agent key signs (orders are attributed to the master
on-chain, `vault_address: None` per the SDK's `approve_agent` pattern); reads
(`user_state` / `open_orders` / `user_fills`) and the `UserFills` / `OrderUpdates`
subscriptions use `account_address`. Unset → reads default to the signer's own
address (main-wallet-key case). HL has no on-chain delegate lookup, so unlike
Bullet the master must be given explicitly.

## Config Format

Expand All @@ -303,15 +310,22 @@ TOML. Top-level sections: `[engine]`, `[exchanges.<name>]`, `[strategy]`,
- `[engine]` — `tick_interval_ms`, `status_port` (optional), or
`status_bind = "host:port"` for explicit bind. `symbol` lives inside each
`[strategy.<name>]` section so multi-symbol setups are explicit.
- Exchange configs: `type = "<name>"` + adapter-specific fields. Bullet
resolves key material in this order (explicit config wins; env fills a
field the config omits, so an ambient env var can't silently switch
wallets): `key_file` (in config) → env `BB_BULLET_KEY_FILE` →
`private_key_hex` (in config) → env `BB_BULLET_PRIVATE_KEY_HEX`. File-based
keystore is preferred — see `bb-bot keygen`. Hyperliquid keys via
`BB_HYPERLIQUID_PRIVATE_KEY_HEX`. (Standalone `deposit`/`flatten`/`observe`
take no config, so there env is the source: `BB_BULLET_KEY_FILE` → env hex
→ default keystore.)
- Exchange configs: `type = "<name>"` + adapter-specific fields. Keys use each
venue's native format: **base58 for Bullet**, **hex for Hyperliquid** — paste
what the UI gives you. Both adapters resolve key material identically through
`bb_core::keys::resolve_key_string`, in this order (explicit config wins; env
fills a field the config omits, so an ambient env var can't silently switch
wallets): `key_file` (config) → env `BB_<VENUE>_KEY_FILE` → `private_key`
(config) → env `BB_<VENUE>_PRIVATE_KEY`. A `key_file` is a file containing the
key string (as written by `bb-bot keygen`), not a JSON keystore. If a Bullet
signer is a **delegate** key, the adapter resolves it to its master account
(via the `delegateOf` endpoint) for all reads and the user-orders
subscription; signing uses the delegate key directly. For Hyperliquid API/agent
wallets, set `BB_HYPERLIQUID_ACCOUNT_ADDRESS` (or `account_address` in config)
to the master account. `bb-bot` auto-loads `./.env` at startup (override with
`--env-file <path>`); already-set environment variables take precedence.
(Standalone `deposit`/`flatten`/`observe` take no config, so there env is the
source: `BB_BULLET_KEY_FILE` → `BB_BULLET_PRIVATE_KEY` → default key file.)
- Strategy configs: `type = "<name>"` with sub-table `[strategy.<name>]`.

## Code Style
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ toml = "0.8"
rust_decimal = { version = "1", features = ["serde-with-str"] }
thiserror = "2"
secrecy = { version = "0.8", features = ["serde"] }
bs58 = "0.5"
getrandom = "0.2"
reqwest = { version = "0.13", features = ["json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
async-trait = "0.1"
clap = { version = "4", features = ["derive"] }
axum = "0.8"
dotenvy = "0.15"

# Internal crates
bb-core = { path = "crates/bb-core" }
Expand Down
6 changes: 3 additions & 3 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ status_port = 3030
type = "bullet"
network = "testnet"
# Key material — do NOT put private keys in this file.
# Option 1 (preferred): key_file = "/path/to/id.json" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY_HEX="0x..."
# Option 1 (preferred): key_file = "/path/to/id.key" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY="<base58 key>"

[strategy]
type = "dip-buyer"
Expand All @@ -297,7 +297,7 @@ max_position = "0.01"
Run:

```sh
export BB_BULLET_PRIVATE_KEY_HEX="0x..."
export BB_BULLET_PRIVATE_KEY="<base58 key>"
cargo run --bin bb-bot -- run --config config/dip-buyer-example.toml
```

Expand Down
97 changes: 67 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,49 +28,86 @@ events are structurally impossible.
For an annotated component diagram, event-flow walkthrough, adapter layout
rules, and the broker contract, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

## Quick start
## Quick start — testing (testnet, ~5 minutes)

The fastest path: a throwaway testnet key funded from the faucet. **No wallet,
no web UI, no real funds.** This is the recommended way to try the bot.

```sh
# Build
cargo build
cargo run --bin bb-bot -- validate --config config/simple-mm-example.toml # no keys needed

# Run tests
cargo nextest run
# 1. Generate a testnet burner key → writes ~/.config/bullet/id.key (0600),
# prints your address and the exact faucet command.
cargo run --bin bb-bot -- keygen --network testnet

# Validate a starter config (no keys needed)
cargo run --bin bb-bot -- validate --config config/simple-mm-example.toml
# 2. Fund it from the faucet. keygen prints this with your address filled in.
# The browser User-Agent header is REQUIRED — without it the faucet returns
# "Forbidden".
curl -X POST -H "User-Agent: Mozilla/5.0" \
"https://app.testnet.bullet.xyz/api/testnet/faucet?address=<YOUR_ADDRESS>"

# Generate a Bullet testnet key, fund it with the printed faucet curl,
# deposit into the perp margin account, then run the starter market maker.
cargo run --bin bb-bot -- keygen --network testnet
# ...run the faucet curl printed above, then deposit into the margin account:
# 3. Move funds into the perp margin account. This also initializes the trading
# account — without it, order placement fails with `user_variants not found`.
cargo run --bin bb-bot -- deposit --network testnet --asset USDC --amount 5000

# 4. Run the starter market maker (reads ~/.config/bullet/id.key by default).
cargo run --bin bb-bot -- run --config config/simple-mm-example.toml
```

Recommended first path:

1. `keygen` — create a testnet key.
2. Fund it with the faucet command printed by `keygen`. The faucet credits your
on-chain wallet, not your trading account. The faucet is **testnet only** —
on mainnet you fund the wallet with real bridged/deposited assets instead.
3. `deposit` — move funds from the on-chain wallet into the perp margin account
(e.g. `deposit --network testnet --asset USDC --amount 5000`). The asset must
match a name in Bullet's exchangeInfo (e.g. `USDC`) and the amount is in that
asset's units. This also initializes the trading account; without it, order
placement fails with `user_variants not found`.
4. `observe` — collect Bullet/Binance spread data without trading.
5. `validate` — preflight the config.
6. `run` — start tiny, watch logs plus `GET /status`.
7. `flatten` — cancel and close manually if you need to clean up.
Other commands: `observe` (collect Bullet/Binance spread data, no trading),
`flatten` (cancel orders + market-close positions), `validate` (preflight a config).

## Production (mainnet, real funds)

**Do not run mainnet with a `keygen` burner** — that puts a key controlling real
funds inside the bot. Instead use a **delegate** (Bullet) / **API wallet**
(Hyperliquid): a separate key scoped to trading only (cannot deposit or
withdraw), revocable from the webapp at any time, so the bot never holds a key
that can drain your wallet.

1. Sign in at [app.bullet.xyz](https://app.bullet.xyz) with your wallet (e.g.
Phantom) — this creates the embedded wallet that is your Bullet trading account.
2. Deposit collateral through the webapp.
3. Create a delegate (see the
[delegate setup guide](https://docs.bullet.xyz/bulletx-exchange/how-to-guide/delegate-account-setup)),
then put its **base58** key in `.env` as `BB_BULLET_PRIVATE_KEY` (or save it
to a file and point `BB_BULLET_KEY_FILE` at it).
4. For Hyperliquid, create an API wallet at
[app.hyperliquid.xyz/API](https://app.hyperliquid.xyz/API). Set
`BB_HYPERLIQUID_PRIVATE_KEY` to the **API-wallet key** (hex), and
`BB_HYPERLIQUID_ACCOUNT_ADDRESS` to your **main account address** (the `0x…`
shown in the HL UI). The API wallet signs; positions/balances/fills are read
from the main account.
5. Set `network = "mainnet"` in the config's `[exchanges.*]` sections.

> **What is a delegate / API wallet?** A separate keypair authorized to trade on
> behalf of your account. It can place and cancel orders but **cannot deposit or
> withdraw**, and you can revoke it from the webapp at any time — so you trade
> without exposing your main wallet's private key. On both venues the bot signs
> with this key but reads account state from the **main account** — Bullet
> resolves the master automatically via `delegateOf`; on Hyperliquid you supply
> it via `BB_HYPERLIQUID_ACCOUNT_ADDRESS`.

Put these in `.env` — `bb-bot` auto-loads `./.env` at startup, so
`cp .env.example .env`, fill it in, and run. (Use `--env-file <path>` to load a
different file; real environment variables already set take precedence.)

## Key management

Private keys are passed via environment variables or keystore files, not copied
into example configs. Two options:

- **Bullet key file (recommended):** generate once with `cargo run --bin bb-bot -- keygen`, then set `BB_BULLET_KEY_FILE` or add `key_file = "/path/to/id.json"` under `[exchanges.bullet]`.
- **Hex key:** set `BB_BULLET_PRIVATE_KEY_HEX` / `BB_HYPERLIQUID_PRIVATE_KEY_HEX`, e.g. via a `.env` file (already gitignored).
Keys use each venue's native format — **paste exactly what the UI gives you**:
**base58 for Bullet** (Phantom / delegation export), **hex for Hyperliquid** (the
HL API page). Never put them in `config/*.toml`.

- **Key string in `.env` (typical):** set `BB_BULLET_PRIVATE_KEY` (base58) and
`BB_HYPERLIQUID_PRIVATE_KEY` (hex); auto-loaded from `.env`. When the
Hyperliquid key is an API wallet, also set `BB_HYPERLIQUID_ACCOUNT_ADDRESS` to
your main account address.
- **Key file (keeps the secret off the environment):** for Bullet, generate one
with `cargo run --bin bb-bot -- keygen`, then set `BB_BULLET_KEY_FILE` (or
`key_file` in `[exchanges.bullet]`). Both venues accept `key_file` /
`BB_<VENUE>_KEY_FILE` — a file containing the key string; it takes precedence
over the inline key.

## Strategies

Expand Down
4 changes: 2 additions & 2 deletions config/avellaneda-stoikov-example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ status_port = 3034 # must be unique per running bot — collides if two bots sh
type = "bullet"
network = "testnet"
# Key material — do NOT put private keys in this file.
# Option 1 (preferred): key_file = "/path/to/id.json" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY_HEX="0x..."
# Option 1 (preferred): key_file = "/path/to/id.key" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY="..." (base58 or hex)

[strategy]
type = "avellaneda-stoikov"
Expand Down
13 changes: 8 additions & 5 deletions config/funding-arb-example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ status_port = 3031

[exchanges.bullet]
type = "bullet"
network = "testnet"
network = "testnet" # match where your delegate was created: testnet (app.testnet.bullet.xyz) or mainnet (app.bullet.xyz)
# Key material — do NOT put private keys in this file.
# Option 1 (preferred): key_file = "/path/to/id.json" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY_HEX="0x..."
# Option 1 (preferred): key_file = "/path/to/id.key" (run `bb-bot keygen`)
# Option 2: export BB_BULLET_PRIVATE_KEY="..." (base58 from the delegate UI, or hex)

[exchanges.hyperliquid]
type = "hyperliquid"
network = "testnet"
network = "testnet" # match where your API wallet was created: testnet or mainnet (app.hyperliquid.xyz)
# Key material — do NOT put private keys in this file.
# export BB_HYPERLIQUID_PRIVATE_KEY_HEX="0x..."
# export BB_HYPERLIQUID_PRIVATE_KEY="0x..." # API-wallet key (signs orders)
# export BB_HYPERLIQUID_ACCOUNT_ADDRESS="0x..." # your MAIN account (reads
# positions/balances/fills). Required for an API wallet; omit only if the
# key above is your main wallet's own key.

[strategy]
type = "funding-arb"
Expand Down
4 changes: 2 additions & 2 deletions config/grid-example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ status_port = 3033 # must be unique per running bot — collides if two bots sh
type = "bullet"
network = "testnet"
# Key material — do NOT put private keys in this file.
# Option 1 (preferred): key_file = "/path/to/id.json" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY_HEX="0x..."
# Option 1 (preferred): key_file = "/path/to/id.key" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY="..." (base58 or hex)

[strategy]
type = "grid"
Expand Down
2 changes: 1 addition & 1 deletion config/reference-arb-example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ status_port = 3032
[exchanges.bullet]
type = "bullet"
network = "testnet"
# Set via: export BB_BULLET_PRIVATE_KEY_HEX="0x..." or BB_BULLET_KEY_FILE=/path/to/key.json
# Set via: export BB_BULLET_PRIVATE_KEY="..." (base58 or hex) or BB_BULLET_KEY_FILE=/path/to/key.json

[strategy]
type = "reference-arb"
Expand Down
4 changes: 2 additions & 2 deletions config/simple-mm-example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ status_port = 3030 # must be unique per running bot — collides if two bots sh
type = "bullet"
network = "testnet"
# Key material — do NOT put private keys in this file.
# Option 1 (preferred): key_file = "/path/to/id.json" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY_HEX="0x..."
# Option 1 (preferred): key_file = "/path/to/id.key" (run `bb-bot keygen`)
# Option 2 (CI/ephemeral): export BB_BULLET_PRIVATE_KEY="..." (base58 or hex)

[strategy]
type = "simple-mm"
Expand Down
1 change: 1 addition & 0 deletions crates/bb-bot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
clap = { workspace = true }
dotenvy = { workspace = true }
secrecy = { workspace = true }
async-trait = { workspace = true }
Loading