Tokenized equity market making system that provides onchain liquidity and captures arbitrage profits.
- Onchain Liquidity: Raindex orders continuously offer to buy/sell tokenized equities at spreads around oracle prices
- Automatic Hedging: When liquidity is taken onchain, the Rust bot executes offsetting trades on traditional brokerages to hedge the change in exposure
- Profit Capture: Earns the spread on every trade while hedging directional exposure
The system enables efficient price discovery for onchain tokenized equity markets by providing continuous two-sided liquidity.
- Supported Executors: Execute hedges through Alpaca Broker API (managed accounts, auto-rebalancing) or dry-run mode for testing
- Real-Time Hedging: WebSocket-based monitoring for near instant execution when onchain liquidity is taken
- Fractional Share Support: Executes fractional shares on Alpaca; dry-run mirrors the same execution model for testing
- Alpaca Hedge Preflight: Checks available offchain shares for sells and cash buying power for buys (includes unsettled T+1 equity-sale proceeds, excludes margin) before submitting Alpaca hedge orders
- Serialized Counter-Trade Submission: Within one bot process, queued and periodic hedge submissions share a lock and reserve budget against active offchain orders before placing new Alpaca counter-trades
- Complete Audit Trail: Database tracking linking every onchain trade to offchain hedge executions
- Exposure Hedging: Automatically executes offsetting trades to reduce directional exposure from onchain fills
- Operator Vault Controls: CLI supports generic ERC20 deposits to and withdrawals from Raindex vaults, with a USDC-specific withdrawal shortcut
- Orchestrator-Mode Mint Authorization: For assets issuance serves through
an
ST0xOrchestratorvault, signs an EIP-712 MintAuthV1 recipient authorization (nonce persisted before delivery, byte-identical retries) and delivers it to issuance before the mint can complete; vault-direct assets are untouched (see SPEC.md "Mint Recipient Authorization")
- Nix with flakes enabled - For reproducible development environment
- direnv (recommended) - The repo includes an
.envrcthat automatically loads the Nix dev shell when youcdinto the project. Without direnv, runnix developmanually in each terminal
The Rust build has two compile-time dependencies that must be set up before
cargo check will succeed:
- Solidity ABI artifacts - The Rust
sol!macros reference JSON ABI files that are produced by per-feature Nix derivations undernix/and exposed viaST0X_*_ABIenvironment variables when you enter the dev shell -- no submodule checkout or manualforge buildstep required - SQLite database - The
sqlx::query!macros validate SQL against a live database at compile time
git clone https://github.com/ST0x-Technology/st0x.liquidity.git
cd st0x.liquidity
direnv allow # or `nix develop` if not using direnv
sqlx db create # create SQLite database for sqlx macros
sqlx migrate run # apply migrations
cargo check # verify setupSolidity ABIs are produced as per-feature Nix derivations under nix/
(forge-std.nix, rain-math-float.nix, rain-orderbook.nix, and
raindex-governance.nix -- the shared RaindexInventory ABI) and exposed to
cargo through environment variables set by the dev shell -- no submodule
checkout, no manual forge build required.
To reset the database: sqlx db reset -y
AI agents: For Rust/TypeScript work, run agents inside the dev shell so they
have access to all tooling (e.g., nix develop -c claude). For editing Nix
code, a regular shell is fine.
The application uses TOML configuration files split into plaintext config and
encrypted secrets. See example.config.toml and example.secrets.toml for all
available options. Operational intervals such as
apalis_finished_job_cleanup_interval_secs must be explicitly configured and
nonzero, as must inventory_divergence_threshold (the number of consecutive
offchain polls that must diverge from the inventory view before the poller
escalates a forced snapshot reconciliation).
When equities are configured, [pricing].ws_url and the encrypted
[pricing].api_key are also required. Remote endpoints must use wss://;
plaintext ws:// is limited to st0x-pricing and loopback. The server
subscribes to the pricing service's Raindex wt<symbol> stream for
dashboard-only USD values; an outage leaves those values unavailable without
stopping hedging or falling back to a trade fill.
The [chains.<name>.trading] table requires an explicit inventory_mode
("legacy" or "managed") and a vault_owner address (the on-chain owner the
vaults are keyed by; no fallback). "managed" additionally requires an
inventory address (the shared RaindexInventory the bot operates via
OPERATOR_ROLE) and is forbidden from being set under "legacy". Its required
inventory_adapters is an array of { venue, operator } records attributing
public operator addresses to venues such as Bebop and Uniswap v4; unknown
operators remain visibly unattributed. This deployment metadata belongs in
plaintext config, not secrets or environment variables. Trade protocol v3
preserves configured and unknown onchain venues; older protocol versions
collapse adapter and Unknown Onchain venues to Raindex for compatibility. See
the [chains.base] block in example.config.toml for the full field
documentation.
The chain registry enforces four rules at startup, each failing closed rather than skipping the chain:
- Every
[chains.<name>]table needs a matching[chains.<name>]secrets entry supplying itsrpc_url, and vice versa. Acting on a chain with no endpoint, and holding an endpoint for a chain with no addresses, both leave fund routing undefined. - Exactly one chain may carry a
[trading]table. The shape admits more, but the runtime drives a single fill watcher, so a second trading chain would be fully described and never read -- unhedged exposure presenting as a working config. - At least one chain must be configured, and at least one of those must trade.
- Every
[chains.<name>]entry must declarerequired_confirmationsdirectly on the chain table (not inside[trading]). There is no default: the depth encodes that chain's reorg behaviour, so omitting it fails config parsing.
Current broker support is limited to alpaca-broker-api and dry-run.
validate-config runs the startup validation without starting the server or
reaching any external service:
# Config half only -- no secrets, no network, no clock.
cargo run --bin validate-config -- --config config/prod/st0x-hedge.toml
# Everything the deploy gate checks, including the config/secrets cross-checks.
cargo run --bin validate-config -- --config path/to/config.toml --secrets path/to/secrets.tomlWithout --secrets it judges the config file alone: schema (unknown keys are
rejected), the port, chain, asset and [rebalancing] cross-field rules, and
every value the config carries on its own. What it cannot see is what the
secrets file supplies -- broker credentials, per-chain rpc_urls, wallet keys,
pricing and issuance API keys -- which stays the deploy gate's job. Both modes
run the same checks the bot runs at boot, so a rule broken here breaks startup.
Because the secrets-free mode needs nothing but the file, CI validates every
config the repository ships (config/**/*.toml, example.config.toml,
e2e/config.toml) on every pull request, via the
every_repo_config_passes_config_only_validation test in st0x-config. A
config edit is therefore caught in the pull request that makes it, rather than
by a deployed service refusing to boot.
cargo run --bin server -- --config path/to/config.toml --secrets path/to/secrets.tomlManual wrap of tokenized equity into wrapped vault shares (requires rebalancing mode and a configured liquidity wallet for the selected network):
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml wrap-equity --symbol AAPL --quantity 10.5Manual unwrap of wrapped equity shares (requires rebalancing mode and a configured liquidity wallet for the selected network):
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml unwrap-equity --symbol AAPL --quantity 10.5Both commands default to Base and resolve addresses from [assets.equities]. On a non Base network, pass the target network and the st0x.registry token list for it:
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml wrap-equity --symbol RKLB --quantity 0.1 --network ethereum --registry path/to/st0x.registry/token-lists/ethereum.jsonManual cancellation of an open Alpaca order by the id printed at placement:
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml cancel 61e7b016-9c91-4a97-b912-615c9d365c9dA cancel for an id the broker does not know reports it as unknown, and a cancel for an order that already filled or was cancelled reports it as no longer cancelable — neither is an error.
Manual repair of local position tracking after an operator trade or rebalance:
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml position set --symbol SPYM --zero --reason "manual rebalance completed"
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml position set --symbol SPYM --long 100 --price 200 --reason "manual buy not observed by bot"
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml position set --symbol SPYM --short 12.5 --price 200 --reason "manual sell not observed by bot"--price (USDC per share) is required for a nonzero target when the symbol uses
a dollar-value execution threshold and no price is already known; without it the
repaired exposure could never be valued and would never hedge.
position set is rejected while the symbol still has a pending offchain hedge
order; resolve it first with position release-hedge, then retry.
After verifying a missing daily snapshot mark against a historical-price source, set the preceding regular-session close through the audited CQRS repair command:
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml portfolio-snapshot set --day 2026-07-20 --symbol AAPL --usd-mark 211.18 --observed-at 2026-07-17T20:00:00Z --source "Nasdaq historical close" --reason "repair missing snapshot mark"The repair updates every captured location for that symbol and day without changing the live position price. If the read model needs recovery, stop the bot first (the rebuild replays events read at its start; concurrent captures would force it to be re-run), then replay all captured balances and corrections with:
cargo run -p st0x-cli -- --config path/to/config.toml --secrets path/to/secrets.toml view rebuild --aggregate portfolio-snapshot --allAlpaca Broker API (managed accounts, supports auto-rebalancing):
For managed/omnibus accounts. Requires Broker API access from Alpaca. This is the only integration that supports automatic portfolio rebalancing (USDC/equity threshold-based).
Add credentials to your TOML config file under the [broker] section (see
example.config.toml and example.secrets.toml). Alpaca configs must also set
broker.counter_trade_slippage_bps, which controls the buy-side preflight
buffer and the protection bound on extended-hours limit orders;
broker.extended_hours_reprice_timeout_secs, the ordinary extended-hours
reprice cadence; broker.close_flatten_reprice_timeout_secs, the faster cadence
inside close-flatten; broker.extended_hours_close_flatten_window_secs, the
length of the final window before weekends, exchange holidays, or an unknown
next session; and broker.close_flatten_cross_max_bps, the maximum cross at the
session close. The maximum cross must be at least counter_trade_slippage_bps,
since the ramp starts there, and no more than 9,999 bps, the global
counter-trade slippage ceiling. All five are required and have no implicit
defaults. Ordinary extended-hours orders retain their 300-second timeout, while
close-flatten orders use the dedicated 60-second timeout and cross progressively
wider until the session closes.
Extended-hours limit orders use an ordered reference chain: an optional current
bid/ask quote source, the broker's position mark, then an emergency
delayed_sip quote. The current deployment has no primary quote provider, so
its effective behaviour remains mark first, delayed SIP second. The executor
capability is already present for a future source such as Alpaca SIP, and the
mark remains the fallback if that source is missing or fails. No market-data
feed config is exposed until a real provider is selected. See
ADR 0019.
Inside the close-flatten window the cross ramps linearly from
counter_trade_slippage_bps at the window's start to
close_flatten_cross_max_bps at the close, so each reprice crosses further than
the last and the bot converges on a fill before the gap. Outside it,
extended-hours orders keep the flat counter_trade_slippage_bps band. Only once
every reference source has failed does the attempt dead-letter (counted by
hedge_dead_lettered_total{symbol,reason}). Transient failures in queued
PlaceHedge attempts, such as timeouts and 5xx, receive three durable redrives
after 1s, 2s, and 4s; exhausting that budget increments the same dead-letter
metric. Scan-time transient and rate-limited preflight failures instead wait for
CheckPositions to re-enqueue the hedge on its next scan.
The system runs on a NixOS host on DigitalOcean, managed by deploy-rs. All infrastructure is defined declaratively in Nix and Terraform.
GitHub Actions (CI/CD)
|
| deploy-rs over SSH
v
NixOS host (DigitalOcean droplet)
├── st0x-hedge (systemd, hedging bot)
├── datasette (systemd, SQLite database explorer)
├── nginx (dashboard + WebSocket proxy)
└── grafana (metrics visualization)
Services are deployed as independent nix profiles, allowing per-service updates and rollbacks without affecting other services.
Before the bot is stopped, activation validates staged config/secrets and, for Turnkey wallets, runs a read-only policy coverage check for every startup MAX approval. Coverage requires an allow policy whose consensus the authenticated API user can satisfy alone and whose target condition provably applies; applicable or unprovable denies take precedence. Missing token or wrapper coverage fails the deployment with the symbol and contract details while the existing bot and installed files remain untouched.
The bot unit exposes a PID-scoped startup signal in its systemd runtime directory. A bot deployment succeeds only after Conductor finishes startup initialization and every essential runtime task has entered its run loop; an early exit or a missing readiness signal fails within five minutes, emits unit status and recent journal output in the deploy log, and triggers deploy-rs rollback. The first rollout requires deploying the system profile before the service profile; service-only deploys verify that prerequisite before stopping the current bot. Local server runs omit the systemd ready-file environment and use a no-op notifier.
| File | Purpose |
|---|---|
os.nix |
NixOS system configuration (services, firewall, users) |
deploy.nix |
deploy-rs profiles and deployment wrappers |
rust.nix |
Nix derivation for Rust binaries |
keys.nix |
SSH public keys and role-based access |
infra/secrets.nix |
ragenix secret declarations |
secret/*.toml.age |
Encrypted service configs (decrypted at deploy) |
infra/ |
Terraform for DigitalOcean infrastructure |
disko.nix |
Disk partitioning for nixos-anywhere bootstrap |
# Deploy everything (system config + all service binaries)
nix run .#deployAll
# Deploy only NixOS system configuration (SSH, firewall, systemd units, nginx)
nix run .#deployNixos
# Deploy a specific service profile
nix run .#deployService st0x-hedge
nix run .#deployService datasettenix run .#remote # interactive shell
nix run .#remote -- <command> # run a commandEach service profile maintains a history of deployments. Rollback requires two steps: reverting the nix profile, then restarting the affected services (the profile switch alone does not trigger a restart).
deploy-rs uses legacy (nix-env-style) profiles internally.
nix profile rollback is not compatible — you must use nix-env.
SSH into the host and run:
# Roll back the bot profile to previous deployment
nix-env --profile /nix/var/nix/profiles/per-service/st0x-hedge --rollback
systemctl restart st0x-hedge
# Roll back the Datasette profile
nix-env --profile /nix/var/nix/profiles/per-service/datasette --rollback
systemctl restart datasetteService configs are encrypted with ragenix (age encryption using SSH keys) and
committed to git as .age files. The NixOS host decrypts them at activation
using its SSH key, mounting cleartext to /run/agenix/ (tmpfs).
# Edit an encrypted config
nix run .#secret secret/st0x-hedge.toml.age
# Re-encrypt all secrets after key changes
ragenix --rules ./infra/secrets.nix -rKey access is managed via roles in keys.nix:
roles.ssh- SSH access to the host (operator + CI)roles.infra- can decrypt terraform state (operator + CI)roles.service- can decrypt service configs (operator + host)
Infrastructure is managed with Terraform, wrapped in Nix for reproducibility:
nix run .#tfInit # initialize terraform
nix run .#tfPlan # preview changes
nix run .#tfApply # apply changes
nix run .#tfDestroy # tear down infrastructureTerraform state is encrypted with age and committed to git.
For initial setup of a new host, Terraform provisions a DigitalOcean Ubuntu droplet. nixos-anywhere then converts it to NixOS over SSH:
nix run .#tfApply # provision Ubuntu droplet
nix run .#bootstrap # convert to NixOS (updates host key + rekeys secrets)
nix run .#deployAll # first deployment- CI (
.github/workflows/ci.yaml): Builds all packages, runs tests and clippy inside nix derivations, and builds the dashboard. Runs for pull request activity and pushes tomaster. - CD (
.github/workflows/cd.yaml): Deploys to the NixOS host vianix run .#deployAll. Runs on push to master.
To reproduce CI checks locally, use the same dev shell CI uses:
nix develop .#ci-backend -c cargo check --workspace
nix develop .#ci-backend -c cargo nextest run --workspace --all-features
nix develop .#ci-backend -c cargo clippy --workspace --all-targets --all-featuresnix run .#simulate launches the full-system chaos eventual-consistency e2e
test (full_system_concurrent) with mprocs
running the dashboard and bot side-by-side. Trades fire in randomized order with
delayed broker fills; between rounds the test injects chaos (bot restarts, NAV
bumps, asset add/remove, broker latency) and then asserts hedging, mint, and
USDC rebalancing still converge. Open http://localhost:5173 to watch the
dashboard while it runs. Set SIMULATE_EXIT_AFTER_CHAOS=1 to exit once
assertions pass instead of idling for dashboard inspection.
nix run .#simulate-market runs the infinite market simulation instead —
continuous user trades at ~10-second intervals. Use this when you want to
observe long-running liquidity cycling rather than a single bounded chaos
scenario.
nix run .#simulate-14d starts the same stack as simulate-market, but
preloads 14 days of seeded hedge-latency, mint, redemption, and USDC-rebalance
history so Performance tab trends and the Transfers panel are populated
immediately.
nix run .#simulate-trade-outcomes starts the same stack as simulate-market,
but rotates every counter-trade through the three outcomes the trade history
renders: filled, rejected by the broker, and cancelled by the broker after a
partial fill. Use it to inspect the Status column, the venue/asset/time-range
filters, and the accepted/filled/unfilled breakdown in the detail panel without
waiting for a real failure.
nix run .#simulate-failures starts the same stack as simulate-market, then
creates failed mint and redemption rebalances whose mock Alpaca provider later
completes and prints the transfer recheck commands that recover them.
What simulate-market does:
- Starts a local Anvil blockchain with deployed Raindex orderbook contracts
- Deploys mock services: Alpaca broker, tokenization API, CCTP attestation
- Creates Raindex liquidity orders — one buy and one sell per symbol (AAPL, TSLA) — all sharing a single USDC vault, with per-symbol equity vaults
- Starts the bot (hedging, equity rebalancing, USDC bridging all enabled)
- Starts the dashboard dev server
- Continuously takes orders at 10-second intervals, simulating users buying and selling tokenized equities
The simulate-14d variant also preloads 14 days of history -- hedge-latency
cycles, equity mints, equity redemptions, and USDC rebalances (alternating
Alpaca<->Base direction) -- before live trades begin, so the Performance tab's
percentile charts and rebalance-stage breakdown, and the dashboard's Transfers
panel, all show a trend immediately instead of waiting for historical data to
accumulate. The dashboard's default 1W view renders the most recent week of
that seed at daily granularity; switch to 2W to see the full 14-day history,
still at daily granularity (within ~12h of the bot starting -- the seed is a
fixed point in time, so a much longer-running session ages its oldest day out of
the 2W window).
The bot counter-trades each fill on the mock broker, mints/redeems to rebalance equity supply between venues, and bridges USDC via mock CCTP to keep cash balanced. If the system works correctly, the vaults never permanently drain — the bot cycles liquidity back through hedging and rebalancing.
Press Ctrl-C to stop.
Workspace crates:
st0x-hedge(root) - Main arbitrage bot: event loop, CQRS/ES aggregates, conductor, and dashboard backendst0x-cli(crates/cli/) - Operator command-line applicationst0x-config(crates/config/) - TOML/secrets loading and runtime context assembly; restricted to thest0x-hedgeandst0x-cliapplication cratesst0x-dto(crates/dto/) - Dashboard DTOs and TypeScript binding generationst0x-execution(crates/execution/) - StandaloneExecutortrait abstraction with Alpaca Broker API and mock implementationsst0x-tokenization(crates/tokenization/) - StandaloneTokenizertrait abstraction with Alpaca tokenization API and mock implementationsst0x-bridge(crates/bridge/) - Cross-chain bridge abstractions and CCTP implementationst0x-raindex(crates/raindex/) -Raindextrait and shared domain types for Rain OrderBook vault operationsst0x-registry(crates/registry/) - Shared reference-data registry:SymbolCache(token address -> symbol) and per-symbolget_symbol_lockst0x-wrapper(crates/wrapper/) -Wrappertrait and ERC-4626 wrap/unwrap domain typesst0x-evm(crates/evm/) - EVM wallet, provider, and test-chain supportst0x-finance(crates/finance/) - Shared financial primitives:Symbol,FractionalShares,Usdc,Usd, and related domain typesst0x-float-serde(crates/float-serde/) - Shared Rain Float formatting and serde helpers for workspace wire formatsst0x-float-macro(crates/float-macro/) - Proc-macro for compile-timeFloatliterals (float!(1.5))
st0x-event-sorcery is an external git dependency (lives in the separate
event-sorcery repo) and is
not a workspace crate.
flake.nix # Nix flake: packages, devShell, NixOS config
os.nix # NixOS system configuration
deploy.nix # deploy-rs profiles and wrappers
rust.nix # Rust package derivation
disko.nix # Disk partitioning for bootstrap
keys.nix # SSH keys and role-based access
config/
├── prod/
│ └── st0x-hedge.toml # plaintext prod service config
└── staging/
└── st0x-hedge.toml # plaintext staging service config
infra/
├── secrets.nix # ragenix secret declarations
└── ... # Terraform (DigitalOcean)
secret/
├── st0x-hedge.toml.age # encrypted core service secrets
└── st0x-hedge-pricing.toml.age # encrypted pricing credential overlay
dashboard/ # SvelteKit operations dashboard
.github/workflows/
├── ci.yaml # Build, test, clippy, dashboard
└── cd.yaml # Deploy to NixOS host
cargo check # fast compilation check
cargo nextest run --workspace # run all tests
cargo clippy --workspace --all-targets --all-features -- -D clippy::all
cargo fmt # format Rust code
nix fmt # format Nix code (when editing .nix files)All commands are run via nix run .#<name>. Commands that access infrastructure
or secrets decrypt state using your SSH key (~/.ssh/id_ed25519 by default).
Pass -i <path> to use a different key.
Development:
| Command | Usage | Notes |
|---|---|---|
genBunNix |
nix run .#genBunNix |
Regenerates dashboard/bun.nix from bun.lock |
Building (Nix):
| Command | Usage | Notes |
|---|---|---|
st0x-liquidity |
nix build .#st0x-liquidity |
Build + tests |
st0x-clippy |
nix build .#st0x-clippy |
Clippy linting |
st0x-dashboard |
nix build .#st0x-dashboard |
SvelteKit dashboard |
Deployment (requires SSH key for host access and terraform state decryption):
| Command | Usage | Notes |
|---|---|---|
prodDeployAll |
nix run .#prodDeployAll |
Deploy prod system config + all services |
prodDeployNixos |
nix run .#prodDeployNixos |
Deploy prod NixOS system config only |
prodDeployNixosBoot |
nix run .#prodDeployNixosBoot |
Register prod NixOS config for next boot |
prodDeployService |
nix run .#prodDeployService <profile> |
Deploy a single prod service |
stagingDeployAll |
nix run .#stagingDeployAll |
Deploy staging system config + all services |
stagingDeployNixosBoot |
nix run .#stagingDeployNixosBoot |
Register staging NixOS config for next boot |
remote |
nix run .#remote [-- <cmd>] |
SSH into production host |
secret |
nix run .#secret <file.age> |
Edit an encrypted config, then re-encrypt all |
bootstrap |
nix run .#bootstrap |
One-time NixOS install on a new host |
The deploy workflows also expose a broker-migration mode for the one-time
dbus to dbus-broker migration. Use that mode when a NixOS change must be
registered for next boot instead of live-switched: it boot-deploys the system
profile, reboots the host, waits for SSH over Tailscale, verifies dbus-broker,
then runs the normal full deploy to reactivate service profiles. This keeps the
host on the current NixOS default instead of carrying a permanent override back
to classic dbus.
To run the production migration:
- Draft the release tag from
master. - Open the Deploy to Production GitHub Actions workflow.
- Click Run workflow.
- Enter the release tag.
- Set
modetobroker-migration. - Start the workflow and wait for the final
Deploy all profiles after rebootstep to pass. - Use the default
allmode for future production deploys.
Infrastructure (requires SSH key for terraform state decryption):
| Command | Usage | Notes |
|---|---|---|
tfInit |
nix run .#tfInit |
Initialize terraform |
tfPlan |
nix run .#tfPlan |
Preview infrastructure changes |
tfApply |
nix run .#tfApply |
Apply planned changes |
tfDestroy |
nix run .#tfDestroy |
Tear down infrastructure |
tfEditVars |
nix run .#tfEditVars |
Edit terraform vars in $EDITOR |
tfRekey |
nix run .#tfRekey |
Re-encrypt terraform state + vars |
resolveIp |
nix run .#resolveIp |
Print the production host IP |
After changing dashboard/bun.lock, regenerate and format the Nix lockfile:
nix run .#genBunNix
nix fmt -- dashboard/bun.nixCI will fail if bun.nix is out of sync with bun.lock.
- SPEC.md - Complete technical specification and architecture
- docs/domain.md - Domain model, terminology, and naming conventions
- AGENTS.md - Development guidelines for AI-assisted coding
- example.config.toml - Configuration reference
- example.secrets.toml - Secrets reference
Market Making Flow:
- Provide Liquidity: Raindex orders offer continuous two-sided liquidity for tokenized equities at spreads around oracle prices
- Detect Fills: WebSocket monitors orderbook events when traders take liquidity onchain
- Parse Trade: Extract details (symbol, amount, direction, price) from blockchain events
- Accumulate: Batch positions until the configured execution threshold is
reached (typically dollar-based for Alpaca Broker API, whole-share for
dry-run) - Hedge: Execute offsetting market order on traditional brokerage to reduce exposure
- Track: Maintain complete audit trail linking onchain fills to offchain hedges
Profit Model: The system earns the spread on each trade (difference between onchain order price and offchain hedge execution price) while hedging directional exposure.
Note: Alpaca Broker API supports fractional share execution and the bot can
hedge using dollar-value thresholds. dry-run remains available for local
testing with whole-share thresholds when that is operationally useful.