Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ The bot can also run **headless** (no TUI) as a systemd daemon on a Linux server
- **Risk engine** — multi-layer defence applied to every trade event:
- **Micro-trade filter** — rejects orders with < $1.00 notional (anti-spoofing)
- **Size cap** — `MAX_TRADE_SIZE_USD` ceiling on every trade
- **Target Position USD Bounding** — filters out targets whose notional trade size falls outside `min_amount` and `max_amount` configured in the scanner block
- **Daily volume limit** — `MAX_DAILY_VOLUME_USD` (0 = disabled); counts both BUY and SELL side; resets at UTC midnight
- **Consecutive-loss circuit breaker** — `MAX_CONSECUTIVE_LOSSES` (0 = disabled); pauses trading for `LOSS_COOLDOWN_SECS` after N consecutive losses
- **Rapid-flip guard** — 60-second cooldown per token prevents the bot from immediately re-entering a position it just exited
Expand Down Expand Up @@ -244,6 +245,7 @@ On first run, if `config.toml` is missing the bot auto-generates it from default
| `max_trade_size_usd` | `10.00` | Maximum USDC to spend per copied trade (hard ceiling for all sizing modes) |
| `max_delay_seconds` | `10` | Discard live trade events older than this many seconds (staleness filter) |
| `sell_fee_buffer` | `0.97` | `sell_size = held × buffer` — absorbs ~3% CLOB fee. Default `0.97` |
| `ignore_closing_in_mins` | `15` | Skip entries and cancel pending orders for markets that close within this many minutes. Omit to disable. |

#### `[sizing]`

Expand All @@ -267,6 +269,8 @@ On first run, if `config.toml` is missing the bot auto-generates it from default
| `max_copy_gain_pct` | `0.05` | Skip catch-up if target is already this far in profit (5% = `0.05`) |
| `min_entry_price` | `0.02` | Minimum token price for catch-up entries (filters near-zero dust) |
| `max_entry_price` | `0.999` | Maximum token price for catch-up entries |
| `min_amount` | *none* | Minimum target position USD value limit. Omit to disable. Universal bounds check applied to both live parsing and scanner |
| `max_amount` | *none* | Maximum target position USD value limit. Omit to disable. Universal bounds check applied to both live parsing and scanner |
| `max_entries_per_cycle` | `1` | Max positions queued per scan cycle. Raise to 2–3 to enter multiple opportunities simultaneously |

#### `[risk]`
Expand Down
6 changes: 6 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ max_trade_size_usd = 10.00
max_delay_seconds = 10
# SELL size = held_size x sell_fee_buffer (absorbs CLOB fee, default 0.97 ~ 3%)
sell_fee_buffer = 0.97
# Skip entries and cancel pending orders for markets that close within this many minutes.
ignore_closing_in_mins = 15

[sizing]
# Sizing algorithm: "self_pct" | "target_usd" | "target_scalar" | "fixed"
Expand All @@ -44,6 +46,10 @@ min_entry_price = 0.02
max_entry_price = 0.999
# Max positions queued per scan cycle -- raise to 2-3 if you miss many entries
max_entries_per_cycle = 1
# Minimum target position USD value limit. Leave blank or commented to disable.
# min_amount = 0
# Maximum target position USD value limit. Leave blank or commented to disable.
# max_amount = 9999999999

[risk]
# Max USD traded per UTC day (BUY + SELL combined). 0 = disabled.
Expand Down
36 changes: 24 additions & 12 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,27 @@ pub struct SetupPayload {
}

async fn handle_setup(Json(payload): Json<SetupPayload>) -> axum::response::Response {
use crate::config::{BotConfig, TargetsConfig};
use std::io::Write;
use crate::config::{is_valid_private_key_format, BotConfig, TargetsConfig};

// 1. Write the genuine secrets to `.env`
if let Ok(mut env_file) = std::fs::File::create(".env") {
let _ = writeln!(env_file, "PRIVATE_KEY=\"{}\"", payload.private_key);
let _ = writeln!(env_file, "FUNDER_ADDRESS=\"{}\"", payload.funder_address);
if !is_valid_private_key_format(&payload.private_key) {
return (
axum::http::StatusCode::BAD_REQUEST,
"Invalid Private Key: MUST be exactly 64 hex characters (32 bytes) long.",
)
.into_response();
}

// 1. Write the genuine secrets to `.env` (preserving any existing unrelated variables)
let _ = crate::config::write_secrets_env(&payload.private_key, &payload.funder_address);

// 2. Initialize default config.toml (Target Wallets can be configured later in UI)
let default_cfg = BotConfig {
targets: TargetsConfig { wallets: vec![] },
..Default::default()
};
let _ = crate::config::write_toml(&default_cfg);
if !std::path::Path::new("config.toml").exists() {
let default_cfg = BotConfig {
targets: TargetsConfig { wallets: vec![] },
..Default::default()
};
let _ = crate::config::write_toml(&default_cfg);
}

// Force a semantic wait to ensure file flush
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Expand Down Expand Up @@ -178,7 +184,7 @@ async fn post_config(Json(payload): Json<BotConfig>) -> Json<serde_json::Value>
}

async fn get_env() -> Json<EnvData> {
let _ = dotenvy::dotenv();
let _ = dotenvy::dotenv_override();
let private_key = std::env::var("PRIVATE_KEY").unwrap_or_default();
let funder_address = std::env::var("FUNDER_ADDRESS").unwrap_or_default();
Json(EnvData {
Expand All @@ -188,6 +194,12 @@ async fn get_env() -> Json<EnvData> {
}

async fn post_env(Json(payload): Json<EnvData>) -> Json<serde_json::Value> {
if !crate::config::is_valid_private_key_format(&payload.private_key) {
return Json(
serde_json::json!({ "error": "Invalid Private Key: MUST be exactly 64 hex characters (32 bytes) long." }),
);
}

if let Err(e) = crate::config::write_secrets_env(&payload.private_key, &payload.funder_address)
{
return Json(serde_json::json!({ "error": e.to_string() }));
Expand Down
Loading
Loading