HuyXCheckerx/arbbotstable

0

stars

22

commits

Python

primary language

Sep 10, 2026

updated

README

Stablecoin Arbitrage Bot

Ethereum and Solana stablecoin arbitrage bot with a live operational dashboard. The profit sniper checks both venue orders between Stable.com and MetaMatcha on Ethereum and Solana. Jupiter is an explicit Solana fallback, not the default.

Cross-chain PYUSD / USDG quote monitor

scripts/crosschain_pyusd_usdg_monitor.py watches the Stable.com Solana USDG pool and quotes this non-atomic route without signing or broadcasting:

PYUSD (Ethereum) -> USDG (Solana, Stable.com)
                 -> USDG (Ethereum, canonical LayerZero OFT)
                 -> PYUSD (Ethereum, MetaMatcha)

Set the two public wallet addresses in .env, then run one pass:

CROSSCHAIN_ETH_ADDRESS=0x...
CROSSCHAIN_SOLANA_ADDRESS=...
python scripts/crosschain_pyusd_usdg_monitor.py --once

Omit --once to poll every ten seconds. The monitor verifies the official USDG OFT deployments through LayerZero metadata, but it does not request transfer calldata. Since the LayerZero messaging fee is paid separately in native gas, the default state is REVIEW_BRIDGE_FEE whenever the gross edge is positive. Set a conservative observed value with --bridge-fee-usd (or CROSSCHAIN_BRIDGE_FEE_USD) to see a complete estimated net. Even then, MANUAL_REVIEW is only a signal: the bridge makes the cycle non-atomic, and the MetaMatcha quote must be refreshed after USDG actually arrives on Ethereum.

Live sniper dashboard

The web app is a read-only live dashboard for start_sniper.cmd. Start the dashboard from the repository root:

python webapp.py

It opens the correct local address in the default browser. On Windows, double-click start_webapp.cmd for the same behavior. Then start the sniper in a second terminal with start_sniper.cmd. Use python webapp.py --no-open on a headless machine, or python webapp.py --host 0.0.0.0 when intentionally exposing it to a trusted LAN.

The dashboard exposes:

  • Whether the recorded sniper PID is actually running, plus mode and uptime.
  • The active Ethereum and Solana route checks.
  • Gross profit, guaranteed net profit, and the strict execution floor for every configured route.
  • Ready, no-trade, paused, unresolved, error, and confirmed result totals.
  • The latest confirmed, reverted, dropped, or unresolved transaction with an explorer link.
  • A continuously refreshed tail of logs/crosschain-sniper.log.

Machine-readable endpoints:

  • GET /api/state — the current atomic sniper status snapshot.
  • GET /api/logs — the structured tail of the sniper log and process status.
  • GET /healthz — HTTP 200 while the sniper PID is alive, otherwise 503.
  • POST /api/run — disabled with HTTP 405; execution belongs to the guarded sniper process, not the dashboard.

The sniper writes logs/sniper-dashboard.json atomically after every route transition. The browser polls that feed every two seconds and checks the PID file before showing the bot as online, so an uncleanly terminated process is not left looking active. The dashboard never signs or broadcasts a transaction.

Polygon PYUSD / USDC executor

Polygon uses a separate immutable executor for each intermediate token. Estimate the PYUSD deployment first, then broadcast it with the explicit mainnet guard:

python deploy_multichain_executor.py --chain polygon --pair PYUSD/USDC
python deploy_multichain_executor.py --chain polygon --pair PYUSD/USDC --send --confirm-mainnet DEPLOY_POLYGON_EXECUTOR

Store the emitted POLYGON_PYUSD_USDC_EXECUTOR_ADDRESS value in .env. The dashboard passes the selected pair to the multichain engine and automatically uses this pair-specific executor. Polygon is restricted to PYUSD/USDC in the dashboard and never falls back to USDT/USDC. The existing POLYGON_EXECUTOR_ADDRESS remains available only for explicitly invoked legacy USDT/USDC command-line runs.

Ethereum and Solana profit sniper

The cross-chain sniper checks all six ordered combinations of USDC, USDG, and PYUSD on both chains while Ethereum and Solana monitor in parallel. A/B means “flash-loan A and use B as the counter-token.” Each pair is checked in both venue orders: A -> B on MetaMatcha followed by B -> A on Stable.com, and A -> B on Stable.com followed by B -> A on MetaMatcha. For example, PYUSD/USDC always borrows and repays PYUSD, while USDC/PYUSD is the separate USDC-loan route. A $5 threshold is converted to a 5.000001-unit gross and guaranteed-net floor in the route's starting stablecoin, so equality at exactly $5 never broadcasts. Each engine still performs its atomic simulation, gas/fee accounting, mainnet check, and guarded submission logic.

Check all 24 chain/pair/order combinations once without broadcasting:

python sniper.py --once

Start guarded live monitoring:

python sniper.py --live --confirm-live EXECUTE_PROFIT_SNIPER

On Windows, start_sniper.cmd starts the same live command. Runtime output is written to rotating logs/crosschain-sniper.log files, with one lock preventing duplicate sniper processes. Confirmed execution is logged only after the chain returns a successful receipt. An ambiguous submission pauses only the affected chain briefly while the other chain and the process continue; an Ethereum transaction absent from the node with an unused nonce is classified as dropped. Transient provider failures use exponential backoff, while unavailable markets and insufficient pools receive route cooldowns. Every line uses an operator-facing state such as CHECK, NO TRADE, PAUSED, READY, or CONFIRMED, followed by the complete venue-labeled route and its actual order.

For a separate sniper version restricted to PYUSD and USDG, check its eight fixed routes once (two chains, two pair directions, and two venue orders):

python pyusd_usdg_sniper.py --once

Start that restricted version live with:

python pyusd_usdg_sniper.py --live --confirm-live EXECUTE_PROFIT_SNIPER

The restricted launcher rejects --chains, --pairs/--routes, and --orders/--swap-orders, so USDC pairs cannot be enabled accidentally. It uses the same process lock and logs as the general sniper, so only one sniper version can run at a time. All operational controls such as thresholds, cooldowns, --once, and --request-stop remain available. Ethereum USDG requires the upgraded executor emitted by deploy_eth_executor_usdc.py. Solana direct pairs also require exactly one standard Marginfi bank for the selected loan mint. A route without the required bank or sufficient Stable.com capacity remains in monitoring mode and cannot broadcast. On Solana, the default engine asks meta.matcha.xyz for 0x and OKX transactions, accepts only successful wallet-specific simulations, and embeds the best result in the atomic transaction. A route that cannot fit the complete 1,232-byte transaction is treated as unavailable and retried after its own directional cooldown. Set SOL_FLASH_ARB_DEX_PROVIDER=jupiter only to restore the legacy Jupiter path.

On Ethereum, ETH_ARB_FLASH_PROVIDER=auto checks funding liquidity before requesting venue quotes. It prefers zero-fee Morpho and falls back to Aave v3, including Aave's live flash-loan premium in both the on-chain repayment gate and the displayed gross/net profit. USDG currently needs this upgraded executor because Morpho has negligible USDG liquidity. Uniswap v4 funding is available as an explicit experimental option, but it cannot compose with a Matcha route that attempts to unlock the same v4 PoolManager.

Compile and estimate the upgraded Ethereum executor before deployment, then use the guarded command only after reviewing the estimate:

python deploy_eth_executor_usdc.py
python deploy_eth_executor_usdc.py --send --confirm-mainnet DEPLOY_EXECUTOR

Store the emitted ETH_ARB_STABLECOIN_EXECUTOR value in .env. Until that new address is configured, the runner rejects Aave/Uniswap funding before requesting quotes instead of calling an incompatible deployed executor.

Stop cleanly after any active route checks finish:

python sniper.py --request-stop

On Windows, double-click stop_sniper.cmd for the same cooperative shutdown.

P&L method

For each attempt:

stablecoin change = (USDC + USDG + PYUSD at $1 + USDT at $0.999 after) - the same value before
SOL cost USD      = observed SOL decrease × average execution-time SOL/USD price
realized net P&L  = stablecoin change - SOL cost USD

USDC, USDG, and PYUSD are estimated at $1; USDT is valued at its $0.999 Stable.com redemption value so an exposed USDT position does not recognize the 0.1% exit fee as profit. The SOL decrease is measured directly from confirmed wallet lamport balances immediately before and after the complete attempt, so it includes base fees, priority fees, and native SOL charged during that attempt. External SOL transfers made from the same wallet during an attempt would also appear as consumption; use a dedicated bot wallet for clean accounting.

Accounting, live dashboard state, and the complete attempt ledger persist in the SQLite database bot_state.db. Daily USDC-equivalent profit is calculated from the attempt ledger by UTC day using the same realized net P&L method above. On the first upgraded run, existing bot_state.json records are imported into the database. If no JSON state exists, the old pnl.txt value is retained separately as a prior-method estimate; it is not mixed into the new realized net P&L because it did not contain exact per-attempt SOL consumption. The legacy files are no longer written after migration. Runtime database and legacy state files are intentionally excluded from Git so a server pull does not erase live state.

Dynamic trade sizing

All arbitrage cycles are anchored in USDC. The scanner evaluates these seven strategies:

USDC -> USDG  on Stable.com -> USDC on Jupiter
USDC -> PYUSD on Stable.com -> USDC on Jupiter
USDC -> USDG  on Stable.com -> PYUSD on Jupiter -> USDC on Stable.com
USDC -> PYUSD on Stable.com -> USDG  on Jupiter -> USDC on Stable.com
USDC -> USDG  on Jupiter    -> USDC on Stable.com
USDC -> PYUSD on Jupiter    -> USDC on Stable.com
USDC -> USDT  on Jupiter    -> USDC on Stable.com

No Stable.com-first USDT route is generated. The one-way USDT route deducts Stable.com's 0.1% USDT/USDC fee during sizing and every quote revalidation; for example, 100,000 USDT is valued as 99,900 USDC at settlement. It also requires Stable.com's USDC reserve to remain at or above 50,000 after the full USDT input is accounted for. This reserve floor is checked during sizing, verification, retries, final execution, and recovery; it does not apply to USDG or PYUSD routes. The USDT cycle has a separate hard minimum net profit of $1.00, checked after the 0.1% Stable.com fee and estimated execution costs. USDG and PYUSD retain their existing route-specific profit thresholds.

The USDG↔PYUSD strategies are three-leg cycles and always begin and end in USDC; they never start from existing USDG/PYUSD inventory. Before entry, the scanner requires capacity both in the Stable.com pool that supplies the first token and in the Stable.com USDC pool that settles the final token. It repeats the USDC-capacity check immediately before and after the Jupiter middle leg. Execution tracks separate baselines for both intermediate tokens and swaps only the deltas acquired by the cycle, leaving pre-existing balances untouched. If a later leg fails, recovery is scheduled for whichever token is actually left in the wallet.

The scanner checks the strategies in the order above, prioritizing Stable.com entries before Jupiter entries so a Stable.com rejection cannot strand a Jupiter-acquired intermediate balance. For each feasible direction it:

  1. Quotes a bounded grid from MIN_TRADE_SIZE_USD to the maximum wallet/pool size.
  2. Refines around the best coarse result.
  3. Subtracts a conservative execution-cost estimate derived from recent observed SOL consumption.
  4. Requires at least MIN_NET_PROFIT_USD after that estimated cost.
  5. Chooses the eligible size with the highest absolute net dollar profit. Lower exposure breaks a tie at six-decimal dollar precision.
  6. Stops scanning later directions, then revalidates the selected size twice before taking first-leg exposure.

Defaults:

MIN_TRADE_SIZE_USD=1000
MIN_NET_PROFIT_USD=0.10
JUPITER_FIRST_MIN_NET_PROFIT_USD=0.05
MIN_NET_RETURN_BPS=0
JUPITER_ENTRY_MAX_RETRIES=5
JUPITER_PRIORITY_FEE_MODE=cap
JUPITER_PRIORITY_FEE_CAP_LAMPORTS=10000
CROSS_ROUTE_EXECUTION_COST_MULTIPLIER=1.5
DEFAULT_EXECUTION_COST_USD=0.005
EXECUTION_COST_SAFETY_MULTIPLIER=1.25

With these defaults, direct USDG/PYUSD Jupiter-to-Stable routes require $0.05 net profit because their Stable.com settlement increases the corresponding token reserve. Other routes retain their existing thresholds: normally $0.10 for USDG/PYUSD strategies and at least $1.00 for USDT. The existing low-reserve rule can still lower a matching USDG/PYUSD route to $0.05. With an estimated cost of $0.00625, a $0.05 candidate must therefore show at least $0.05625 gross difference. Within the first fully sized route that has an eligible candidate, the bot selects the size with the highest net dollar profit and proceeds immediately; it does not age that quote by comparing later routes. For example, $0.20 net on 20,000 outranks $0.14 net on 10,000 even though the smaller trade has a higher percentage return.

MIN_NET_RETURN_BPS remains an optional eligibility floor. It can reject a quote, but it is not used to rank quotes that pass.

Three-leg USDG/PYUSD cycles reserve 1.5 times the normal estimated execution cost by default because they submit three transactions instead of two. Change CROSS_ROUTE_EXECUTION_COST_MULTIPLIER only if the observed fee history justifies a different conservative multiplier.

To limit Jupiter API usage, ordinary sizing probes only the minimum, 2x minimum, 5x minimum, and exact maximum before refining around the best anchor. For Jupiter-first routes, the second successful wallet-specific verification order is submitted directly as the entry instead of requesting an identical third executable order.

If that Jupiter entry definitively fails on-chain, expires without landing, or fails before submission, the bot requests up to five fresh executable orders. Every retry must still pass the current net-profit and Stable.com pool-capacity checks. Its profit calculation also deducts the SOL fees already spent by the failed entries. Ambiguous transactions are never retried.

Jupiter priority fees default to cap mode, which asks Jupiter to treat JUPITER_PRIORITY_FEE_CAP_LAMPORTS as a maximum and also enforces that ceiling locally before signing. Set JUPITER_PRIORITY_FEE_MODE=recommended to omit the fee override and sign Jupiter's automatically selected fee unchanged. Recommended mode can spend substantially more SOL during congestion. This setting affects only Jupiter transactions; the Stable.com leg keeps its existing compute-unit price.

USDG reserve drain mode

For USDC -> USDG on Stable.com -> USDC on Jupiter, the scanner prefers near-full-drain candidates when the wallet can bring the pool into the refill window. If the wallet cannot reach that window, it falls back to ordinary dynamic sizing instead of discarding the route. The fallback includes the exact wallet-or-pool-limited maximum, including fractional token units, and still selects the eligible size with the highest absolute net profit.

Sizing remains in raw six-decimal units. Stable.com rejects an operation that leaves less than $1.80 USDG. A default $0.10 safety buffer makes the effective drain window $1.90-$1.99 while keeping it below the $2.00 refill trigger:

USDG_DRAIN_MIN_REMAINDER_USD=1.80
USDG_DRAIN_SAFETY_BUFFER_USD=0.10
USDG_MAX_REMAINDER_USD=1.99

4,998.010000 USDG -> leaves 1.990000
4,998.055000 USDG -> leaves 1.945000
4,998.100000 USDG -> leaves 1.900000

The protocol floor is hard-clamped, so legacy or mistaken configuration cannot lower the effective remainder below the safe minimum; the former $1 maximum is migrated to the new $1.99 default. A fresh raw pool balance is checked immediately before either a drain or partial order. If Stable.com reports a higher live constraint, the bot raises its runtime floor when the refill window permits and applies exponential backoff instead of retrying the rejected order immediately. Every candidate must still meet the absolute $0.10 net-profit floor.

WebSocket confirmation and Stable.com synchronization

Balance confirmation is WebSocket-first at Solana's confirmed commitment. The monitor stores a revision and slot for every subscribed account, and the bot captures those cursors immediately before transaction submission. An entry requires fresh updates showing the intermediate token received and USDC debited. An exit requires fresh updates showing the intermediate token cleared and USDC credited. Events that arrive while a submit call is still returning are consumed immediately; stale cached balances cannot confirm a new transaction. The bot will not take first-leg exposure until all account subscriptions are ready. If no matching event arrives within the configured timeout, it takes one RPC balance snapshot instead of polling 11-16 times.

Every signed transaction stores its local signature and blockhash atomically in bot_state.db before broadcast. If an RPC or Jupiter HTTP response is lost, or the first snapshot is still too early, the bot freezes new submissions and reconciles that signature with delayed WS updates. The lock survives the panel's automatic process restart. It resumes only after the transaction is confirmed/failed or an unrecorded transaction's blockhash has expired, preventing a hidden accepted transaction from being submitted twice. A confirmed transaction with unexpected balances stays locked for operator review instead of being balance-polled indefinitely.

The scanner also refuses every new first leg while the wallet holds more than the normal 0.1-token intermediate tolerance in USDG, PYUSD, or USDT. This applies equally to a balance detected at startup. It writes one durable recovery plan containing the exact token amount. The separately supervised recovery_worker.py first checks the wallet-specific executable Jupiter order. If its conservative net recovery profit is at least $0.10, it returns through Jupiter; otherwise (including no Jupiter quote) it submits the Stable.com token-to-USDC exit. If Stable.com's USDT capacity is temporarily below the position, the worker safely returns the largest amount that preserves a $1 pool reserve, persists the confirmed remainder, and continues recovery. The Jupiter calculation includes the configured execution-cost reserve and a one-basis-point slippage reserve by default. Both return paths use fresh WebSocket balance revisions for confirmation, with one RPC snapshot fallback. The worker never opens a first leg or increases the planned amount to match a larger wallet balance. The scanner remains locked until the recovery is confirmed.

Stable.com's create-order service may index a USDG refill after the confirmed on-chain account notification. The bot therefore gives an observed USDG pool increase a short settlement window before quoting that drain route. If the API still reports the old available balance, the route receives a short exponential cooldown rather than submitting the same request repeatedly.

WS_CONFIRM_TIMEOUT_SECONDS=12
SIGNATURE_CHECK_INTERVAL_SECONDS=5
STABLE_POOL_REFILL_SYNC_SECONDS=15
STABLE_BACKEND_LAG_RETRY_SECONDS=5

Local setup

From PowerShell in this repository:

Copy-Item .env.example .env
python -m pip install -r requirements.txt
npm install
notepad .env

Configure the Ethereum and/or Solana RPC, dedicated operator key, executor or Marginfi account, and provider API settings for the chains you will enable. Use a dedicated wallet and begin with limited funds. Validate without broadcasting:

python sniper.py --once

For normal Windows operation, use two terminals (or double-click both command files):

.\start_webapp.cmd
.\start_sniper.cmd

Open http://127.0.0.1:25284. The first command serves the dashboard; the second starts the guarded live sniper and supplies its live data. Stop the sniper cooperatively with python sniper.py --request-stop, and stop the web server with Ctrl+C.

Put the project on GitHub for the first time

This directory currently sits inside a parent Git repository, so initialize a repository specifically inside the project before adding files:

cd /Users/perycent/Downloads/arbbot
git init
git branch -M main
git status --ignored
git add .
git commit -m "Initial arbitrage bot"

Confirm that .env, .local, logs, bot_state.db, bot_state.json, and pnl.txt are not in the commit:

git status --ignored
if git ls-files | grep -Eq '(^|/)(\.env|bot_state\.db|bot_state\.json|pnl\.txt|logs/)'; then
  echo "STOP: a secret or runtime file is tracked"
  exit 1
fi

Create an empty repository on GitHub without adding a README or .gitignore. Then connect and push:

git remote add origin git@github.com:YOUR_GITHUB_USER/arbbot.git
git push -u origin main

Alternatively, with the GitHub CLI installed:

gh repo create arbbot --private --source=. --remote=origin --push

A private repository is recommended because this is financial trading infrastructure. The private key must still remain only in .env, never in GitHub.

Install on a server

Public repository

git clone https://github.com/YOUR_GITHUB_USER/arbbot.git
cd arbbot
cp .env.example .env
chmod 600 .env
# Edit .env with the server's real secrets.
bash server_start.sh

Private repository

Use a read-only GitHub deploy key:

  1. On the server, run ssh-keygen -t ed25519 -C arbbot-server and do not add a passphrase for unattended startup.
  2. Copy the public key from ~/.ssh/id_ed25519.pub.
  3. In GitHub, open the repository's Settings → Deploy keys → Add deploy key.
  4. Add the public key and leave Allow write access disabled.
  5. Test with ssh -T git@github.com.
  6. Clone with git clone git@github.com:YOUR_GITHUB_USER/arbbot.git.

Never copy a personal GitHub access token into the source code or .env.

Normal update workflow

On the development computer:

git add .
git commit -m "Describe the change"
git push

On the server, restart server_start.sh. It performs git pull --ff-only, installs the declared dependencies, and then starts app.py:

cd /path/to/arbbot
bash server_start.sh

For Pterodactyl, set the startup command to:

bash server_start.sh

Python-file-only server panels

If the panel only accepts an App Py File and does not let you enter a startup command, upload server_start.py and set it as that file. On its first start it creates .git, connects to the repository, downloads the tracked application files, installs requirements.txt, and starts app.py.

Set GIT_REPOSITORY_URL if your repository URL differs from the default, and set GIT_BRANCH if you deploy a branch other than main. Create the ignored .env file in the server's project folder before starting the bot. Later restarts run a fast-forward Git pull; set SKIP_GIT_PULL=1 to skip that update.

The container must include git, Python, pip, and outbound GitHub access. If Pterodactyl already performs package installation, set SKIP_GIT_PULL=1 only when you intentionally do not want automatic pulls.

git pull --ff-only deliberately refuses to overwrite server-side source edits. Make code changes on the development machine, push them, then pull on the server. Keep server-only secrets and runtime data in the ignored files.

Operational notes

  • The dashboard listens on 0.0.0.0. Put it behind authentication or a private network before exposing it publicly.
  • Logs are written under logs/ and should be rotated by the host.
  • bot_state.db uses SQLite transactions and write-ahead logging so the dashboard never reads a partially written update.
  • The two exchange legs are separate transactions. Use strict notional limits and supervise the bot until atomic execution or a bounded-loss unwind policy is implemented.

Contributors

HuyXCheckerx

22 commits

HuyXCheckerx/arbbotstable

0

stars

22

commits

Python

primary language

Sep 10, 2026

updated

README

Stablecoin Arbitrage Bot

Ethereum and Solana stablecoin arbitrage bot with a live operational dashboard. The profit sniper checks both venue orders between Stable.com and MetaMatcha on Ethereum and Solana. Jupiter is an explicit Solana fallback, not the default.

Cross-chain PYUSD / USDG quote monitor

scripts/crosschain_pyusd_usdg_monitor.py watches the Stable.com Solana USDG pool and quotes this non-atomic route without signing or broadcasting:

PYUSD (Ethereum) -> USDG (Solana, Stable.com)
                 -> USDG (Ethereum, canonical LayerZero OFT)
                 -> PYUSD (Ethereum, MetaMatcha)

Set the two public wallet addresses in .env, then run one pass:

CROSSCHAIN_ETH_ADDRESS=0x...
CROSSCHAIN_SOLANA_ADDRESS=...
python scripts/crosschain_pyusd_usdg_monitor.py --once

Omit --once to poll every ten seconds. The monitor verifies the official USDG OFT deployments through LayerZero metadata, but it does not request transfer calldata. Since the LayerZero messaging fee is paid separately in native gas, the default state is REVIEW_BRIDGE_FEE whenever the gross edge is positive. Set a conservative observed value with --bridge-fee-usd (or CROSSCHAIN_BRIDGE_FEE_USD) to see a complete estimated net. Even then, MANUAL_REVIEW is only a signal: the bridge makes the cycle non-atomic, and the MetaMatcha quote must be refreshed after USDG actually arrives on Ethereum.

Live sniper dashboard

The web app is a read-only live dashboard for start_sniper.cmd. Start the dashboard from the repository root:

python webapp.py

It opens the correct local address in the default browser. On Windows, double-click start_webapp.cmd for the same behavior. Then start the sniper in a second terminal with start_sniper.cmd. Use python webapp.py --no-open on a headless machine, or python webapp.py --host 0.0.0.0 when intentionally exposing it to a trusted LAN.

The dashboard exposes:

  • Whether the recorded sniper PID is actually running, plus mode and uptime.
  • The active Ethereum and Solana route checks.
  • Gross profit, guaranteed net profit, and the strict execution floor for every configured route.
  • Ready, no-trade, paused, unresolved, error, and confirmed result totals.
  • The latest confirmed, reverted, dropped, or unresolved transaction with an explorer link.
  • A continuously refreshed tail of logs/crosschain-sniper.log.

Machine-readable endpoints:

  • GET /api/state — the current atomic sniper status snapshot.
  • GET /api/logs — the structured tail of the sniper log and process status.
  • GET /healthz — HTTP 200 while the sniper PID is alive, otherwise 503.
  • POST /api/run — disabled with HTTP 405; execution belongs to the guarded sniper process, not the dashboard.

The sniper writes logs/sniper-dashboard.json atomically after every route transition. The browser polls that feed every two seconds and checks the PID file before showing the bot as online, so an uncleanly terminated process is not left looking active. The dashboard never signs or broadcasts a transaction.

Polygon PYUSD / USDC executor

Polygon uses a separate immutable executor for each intermediate token. Estimate the PYUSD deployment first, then broadcast it with the explicit mainnet guard:

python deploy_multichain_executor.py --chain polygon --pair PYUSD/USDC
python deploy_multichain_executor.py --chain polygon --pair PYUSD/USDC --send --confirm-mainnet DEPLOY_POLYGON_EXECUTOR

Store the emitted POLYGON_PYUSD_USDC_EXECUTOR_ADDRESS value in .env. The dashboard passes the selected pair to the multichain engine and automatically uses this pair-specific executor. Polygon is restricted to PYUSD/USDC in the dashboard and never falls back to USDT/USDC. The existing POLYGON_EXECUTOR_ADDRESS remains available only for explicitly invoked legacy USDT/USDC command-line runs.

Ethereum and Solana profit sniper

The cross-chain sniper checks all six ordered combinations of USDC, USDG, and PYUSD on both chains while Ethereum and Solana monitor in parallel. A/B means “flash-loan A and use B as the counter-token.” Each pair is checked in both venue orders: A -> B on MetaMatcha followed by B -> A on Stable.com, and A -> B on Stable.com followed by B -> A on MetaMatcha. For example, PYUSD/USDC always borrows and repays PYUSD, while USDC/PYUSD is the separate USDC-loan route. A $5 threshold is converted to a 5.000001-unit gross and guaranteed-net floor in the route's starting stablecoin, so equality at exactly $5 never broadcasts. Each engine still performs its atomic simulation, gas/fee accounting, mainnet check, and guarded submission logic.

Check all 24 chain/pair/order combinations once without broadcasting:

python sniper.py --once

Start guarded live monitoring:

python sniper.py --live --confirm-live EXECUTE_PROFIT_SNIPER

On Windows, start_sniper.cmd starts the same live command. Runtime output is written to rotating logs/crosschain-sniper.log files, with one lock preventing duplicate sniper processes. Confirmed execution is logged only after the chain returns a successful receipt. An ambiguous submission pauses only the affected chain briefly while the other chain and the process continue; an Ethereum transaction absent from the node with an unused nonce is classified as dropped. Transient provider failures use exponential backoff, while unavailable markets and insufficient pools receive route cooldowns. Every line uses an operator-facing state such as CHECK, NO TRADE, PAUSED, READY, or CONFIRMED, followed by the complete venue-labeled route and its actual order.

For a separate sniper version restricted to PYUSD and USDG, check its eight fixed routes once (two chains, two pair directions, and two venue orders):

python pyusd_usdg_sniper.py --once

Start that restricted version live with:

python pyusd_usdg_sniper.py --live --confirm-live EXECUTE_PROFIT_SNIPER

The restricted launcher rejects --chains, --pairs/--routes, and --orders/--swap-orders, so USDC pairs cannot be enabled accidentally. It uses the same process lock and logs as the general sniper, so only one sniper version can run at a time. All operational controls such as thresholds, cooldowns, --once, and --request-stop remain available. Ethereum USDG requires the upgraded executor emitted by deploy_eth_executor_usdc.py. Solana direct pairs also require exactly one standard Marginfi bank for the selected loan mint. A route without the required bank or sufficient Stable.com capacity remains in monitoring mode and cannot broadcast. On Solana, the default engine asks meta.matcha.xyz for 0x and OKX transactions, accepts only successful wallet-specific simulations, and embeds the best result in the atomic transaction. A route that cannot fit the complete 1,232-byte transaction is treated as unavailable and retried after its own directional cooldown. Set SOL_FLASH_ARB_DEX_PROVIDER=jupiter only to restore the legacy Jupiter path.

On Ethereum, ETH_ARB_FLASH_PROVIDER=auto checks funding liquidity before requesting venue quotes. It prefers zero-fee Morpho and falls back to Aave v3, including Aave's live flash-loan premium in both the on-chain repayment gate and the displayed gross/net profit. USDG currently needs this upgraded executor because Morpho has negligible USDG liquidity. Uniswap v4 funding is available as an explicit experimental option, but it cannot compose with a Matcha route that attempts to unlock the same v4 PoolManager.

Compile and estimate the upgraded Ethereum executor before deployment, then use the guarded command only after reviewing the estimate:

python deploy_eth_executor_usdc.py
python deploy_eth_executor_usdc.py --send --confirm-mainnet DEPLOY_EXECUTOR

Store the emitted ETH_ARB_STABLECOIN_EXECUTOR value in .env. Until that new address is configured, the runner rejects Aave/Uniswap funding before requesting quotes instead of calling an incompatible deployed executor.

Stop cleanly after any active route checks finish:

python sniper.py --request-stop

On Windows, double-click stop_sniper.cmd for the same cooperative shutdown.

P&L method

For each attempt:

stablecoin change = (USDC + USDG + PYUSD at $1 + USDT at $0.999 after) - the same value before
SOL cost USD      = observed SOL decrease × average execution-time SOL/USD price
realized net P&L  = stablecoin change - SOL cost USD

USDC, USDG, and PYUSD are estimated at $1; USDT is valued at its $0.999 Stable.com redemption value so an exposed USDT position does not recognize the 0.1% exit fee as profit. The SOL decrease is measured directly from confirmed wallet lamport balances immediately before and after the complete attempt, so it includes base fees, priority fees, and native SOL charged during that attempt. External SOL transfers made from the same wallet during an attempt would also appear as consumption; use a dedicated bot wallet for clean accounting.

Accounting, live dashboard state, and the complete attempt ledger persist in the SQLite database bot_state.db. Daily USDC-equivalent profit is calculated from the attempt ledger by UTC day using the same realized net P&L method above. On the first upgraded run, existing bot_state.json records are imported into the database. If no JSON state exists, the old pnl.txt value is retained separately as a prior-method estimate; it is not mixed into the new realized net P&L because it did not contain exact per-attempt SOL consumption. The legacy files are no longer written after migration. Runtime database and legacy state files are intentionally excluded from Git so a server pull does not erase live state.

Dynamic trade sizing

All arbitrage cycles are anchored in USDC. The scanner evaluates these seven strategies:

USDC -> USDG  on Stable.com -> USDC on Jupiter
USDC -> PYUSD on Stable.com -> USDC on Jupiter
USDC -> USDG  on Stable.com -> PYUSD on Jupiter -> USDC on Stable.com
USDC -> PYUSD on Stable.com -> USDG  on Jupiter -> USDC on Stable.com
USDC -> USDG  on Jupiter    -> USDC on Stable.com
USDC -> PYUSD on Jupiter    -> USDC on Stable.com
USDC -> USDT  on Jupiter    -> USDC on Stable.com

No Stable.com-first USDT route is generated. The one-way USDT route deducts Stable.com's 0.1% USDT/USDC fee during sizing and every quote revalidation; for example, 100,000 USDT is valued as 99,900 USDC at settlement. It also requires Stable.com's USDC reserve to remain at or above 50,000 after the full USDT input is accounted for. This reserve floor is checked during sizing, verification, retries, final execution, and recovery; it does not apply to USDG or PYUSD routes. The USDT cycle has a separate hard minimum net profit of $1.00, checked after the 0.1% Stable.com fee and estimated execution costs. USDG and PYUSD retain their existing route-specific profit thresholds.

The USDG↔PYUSD strategies are three-leg cycles and always begin and end in USDC; they never start from existing USDG/PYUSD inventory. Before entry, the scanner requires capacity both in the Stable.com pool that supplies the first token and in the Stable.com USDC pool that settles the final token. It repeats the USDC-capacity check immediately before and after the Jupiter middle leg. Execution tracks separate baselines for both intermediate tokens and swaps only the deltas acquired by the cycle, leaving pre-existing balances untouched. If a later leg fails, recovery is scheduled for whichever token is actually left in the wallet.

The scanner checks the strategies in the order above, prioritizing Stable.com entries before Jupiter entries so a Stable.com rejection cannot strand a Jupiter-acquired intermediate balance. For each feasible direction it:

  1. Quotes a bounded grid from MIN_TRADE_SIZE_USD to the maximum wallet/pool size.
  2. Refines around the best coarse result.
  3. Subtracts a conservative execution-cost estimate derived from recent observed SOL consumption.
  4. Requires at least MIN_NET_PROFIT_USD after that estimated cost.
  5. Chooses the eligible size with the highest absolute net dollar profit. Lower exposure breaks a tie at six-decimal dollar precision.
  6. Stops scanning later directions, then revalidates the selected size twice before taking first-leg exposure.

Defaults:

MIN_TRADE_SIZE_USD=1000
MIN_NET_PROFIT_USD=0.10
JUPITER_FIRST_MIN_NET_PROFIT_USD=0.05
MIN_NET_RETURN_BPS=0
JUPITER_ENTRY_MAX_RETRIES=5
JUPITER_PRIORITY_FEE_MODE=cap
JUPITER_PRIORITY_FEE_CAP_LAMPORTS=10000
CROSS_ROUTE_EXECUTION_COST_MULTIPLIER=1.5
DEFAULT_EXECUTION_COST_USD=0.005
EXECUTION_COST_SAFETY_MULTIPLIER=1.25

With these defaults, direct USDG/PYUSD Jupiter-to-Stable routes require $0.05 net profit because their Stable.com settlement increases the corresponding token reserve. Other routes retain their existing thresholds: normally $0.10 for USDG/PYUSD strategies and at least $1.00 for USDT. The existing low-reserve rule can still lower a matching USDG/PYUSD route to $0.05. With an estimated cost of $0.00625, a $0.05 candidate must therefore show at least $0.05625 gross difference. Within the first fully sized route that has an eligible candidate, the bot selects the size with the highest net dollar profit and proceeds immediately; it does not age that quote by comparing later routes. For example, $0.20 net on 20,000 outranks $0.14 net on 10,000 even though the smaller trade has a higher percentage return.

MIN_NET_RETURN_BPS remains an optional eligibility floor. It can reject a quote, but it is not used to rank quotes that pass.

Three-leg USDG/PYUSD cycles reserve 1.5 times the normal estimated execution cost by default because they submit three transactions instead of two. Change CROSS_ROUTE_EXECUTION_COST_MULTIPLIER only if the observed fee history justifies a different conservative multiplier.

To limit Jupiter API usage, ordinary sizing probes only the minimum, 2x minimum, 5x minimum, and exact maximum before refining around the best anchor. For Jupiter-first routes, the second successful wallet-specific verification order is submitted directly as the entry instead of requesting an identical third executable order.

If that Jupiter entry definitively fails on-chain, expires without landing, or fails before submission, the bot requests up to five fresh executable orders. Every retry must still pass the current net-profit and Stable.com pool-capacity checks. Its profit calculation also deducts the SOL fees already spent by the failed entries. Ambiguous transactions are never retried.

Jupiter priority fees default to cap mode, which asks Jupiter to treat JUPITER_PRIORITY_FEE_CAP_LAMPORTS as a maximum and also enforces that ceiling locally before signing. Set JUPITER_PRIORITY_FEE_MODE=recommended to omit the fee override and sign Jupiter's automatically selected fee unchanged. Recommended mode can spend substantially more SOL during congestion. This setting affects only Jupiter transactions; the Stable.com leg keeps its existing compute-unit price.

USDG reserve drain mode

For USDC -> USDG on Stable.com -> USDC on Jupiter, the scanner prefers near-full-drain candidates when the wallet can bring the pool into the refill window. If the wallet cannot reach that window, it falls back to ordinary dynamic sizing instead of discarding the route. The fallback includes the exact wallet-or-pool-limited maximum, including fractional token units, and still selects the eligible size with the highest absolute net profit.

Sizing remains in raw six-decimal units. Stable.com rejects an operation that leaves less than $1.80 USDG. A default $0.10 safety buffer makes the effective drain window $1.90-$1.99 while keeping it below the $2.00 refill trigger:

USDG_DRAIN_MIN_REMAINDER_USD=1.80
USDG_DRAIN_SAFETY_BUFFER_USD=0.10
USDG_MAX_REMAINDER_USD=1.99

4,998.010000 USDG -> leaves 1.990000
4,998.055000 USDG -> leaves 1.945000
4,998.100000 USDG -> leaves 1.900000

The protocol floor is hard-clamped, so legacy or mistaken configuration cannot lower the effective remainder below the safe minimum; the former $1 maximum is migrated to the new $1.99 default. A fresh raw pool balance is checked immediately before either a drain or partial order. If Stable.com reports a higher live constraint, the bot raises its runtime floor when the refill window permits and applies exponential backoff instead of retrying the rejected order immediately. Every candidate must still meet the absolute $0.10 net-profit floor.

WebSocket confirmation and Stable.com synchronization

Balance confirmation is WebSocket-first at Solana's confirmed commitment. The monitor stores a revision and slot for every subscribed account, and the bot captures those cursors immediately before transaction submission. An entry requires fresh updates showing the intermediate token received and USDC debited. An exit requires fresh updates showing the intermediate token cleared and USDC credited. Events that arrive while a submit call is still returning are consumed immediately; stale cached balances cannot confirm a new transaction. The bot will not take first-leg exposure until all account subscriptions are ready. If no matching event arrives within the configured timeout, it takes one RPC balance snapshot instead of polling 11-16 times.

Every signed transaction stores its local signature and blockhash atomically in bot_state.db before broadcast. If an RPC or Jupiter HTTP response is lost, or the first snapshot is still too early, the bot freezes new submissions and reconciles that signature with delayed WS updates. The lock survives the panel's automatic process restart. It resumes only after the transaction is confirmed/failed or an unrecorded transaction's blockhash has expired, preventing a hidden accepted transaction from being submitted twice. A confirmed transaction with unexpected balances stays locked for operator review instead of being balance-polled indefinitely.

The scanner also refuses every new first leg while the wallet holds more than the normal 0.1-token intermediate tolerance in USDG, PYUSD, or USDT. This applies equally to a balance detected at startup. It writes one durable recovery plan containing the exact token amount. The separately supervised recovery_worker.py first checks the wallet-specific executable Jupiter order. If its conservative net recovery profit is at least $0.10, it returns through Jupiter; otherwise (including no Jupiter quote) it submits the Stable.com token-to-USDC exit. If Stable.com's USDT capacity is temporarily below the position, the worker safely returns the largest amount that preserves a $1 pool reserve, persists the confirmed remainder, and continues recovery. The Jupiter calculation includes the configured execution-cost reserve and a one-basis-point slippage reserve by default. Both return paths use fresh WebSocket balance revisions for confirmation, with one RPC snapshot fallback. The worker never opens a first leg or increases the planned amount to match a larger wallet balance. The scanner remains locked until the recovery is confirmed.

Stable.com's create-order service may index a USDG refill after the confirmed on-chain account notification. The bot therefore gives an observed USDG pool increase a short settlement window before quoting that drain route. If the API still reports the old available balance, the route receives a short exponential cooldown rather than submitting the same request repeatedly.

WS_CONFIRM_TIMEOUT_SECONDS=12
SIGNATURE_CHECK_INTERVAL_SECONDS=5
STABLE_POOL_REFILL_SYNC_SECONDS=15
STABLE_BACKEND_LAG_RETRY_SECONDS=5

Local setup

From PowerShell in this repository:

Copy-Item .env.example .env
python -m pip install -r requirements.txt
npm install
notepad .env

Configure the Ethereum and/or Solana RPC, dedicated operator key, executor or Marginfi account, and provider API settings for the chains you will enable. Use a dedicated wallet and begin with limited funds. Validate without broadcasting:

python sniper.py --once

For normal Windows operation, use two terminals (or double-click both command files):

.\start_webapp.cmd
.\start_sniper.cmd

Open http://127.0.0.1:25284. The first command serves the dashboard; the second starts the guarded live sniper and supplies its live data. Stop the sniper cooperatively with python sniper.py --request-stop, and stop the web server with Ctrl+C.

Put the project on GitHub for the first time

This directory currently sits inside a parent Git repository, so initialize a repository specifically inside the project before adding files:

cd /Users/perycent/Downloads/arbbot
git init
git branch -M main
git status --ignored
git add .
git commit -m "Initial arbitrage bot"

Confirm that .env, .local, logs, bot_state.db, bot_state.json, and pnl.txt are not in the commit:

git status --ignored
if git ls-files | grep -Eq '(^|/)(\.env|bot_state\.db|bot_state\.json|pnl\.txt|logs/)'; then
  echo "STOP: a secret or runtime file is tracked"
  exit 1
fi

Create an empty repository on GitHub without adding a README or .gitignore. Then connect and push:

git remote add origin git@github.com:YOUR_GITHUB_USER/arbbot.git
git push -u origin main

Alternatively, with the GitHub CLI installed:

gh repo create arbbot --private --source=. --remote=origin --push

A private repository is recommended because this is financial trading infrastructure. The private key must still remain only in .env, never in GitHub.

Install on a server

Public repository

git clone https://github.com/YOUR_GITHUB_USER/arbbot.git
cd arbbot
cp .env.example .env
chmod 600 .env
# Edit .env with the server's real secrets.
bash server_start.sh

Private repository

Use a read-only GitHub deploy key:

  1. On the server, run ssh-keygen -t ed25519 -C arbbot-server and do not add a passphrase for unattended startup.
  2. Copy the public key from ~/.ssh/id_ed25519.pub.
  3. In GitHub, open the repository's Settings → Deploy keys → Add deploy key.
  4. Add the public key and leave Allow write access disabled.
  5. Test with ssh -T git@github.com.
  6. Clone with git clone git@github.com:YOUR_GITHUB_USER/arbbot.git.

Never copy a personal GitHub access token into the source code or .env.

Normal update workflow

On the development computer:

git add .
git commit -m "Describe the change"
git push

On the server, restart server_start.sh. It performs git pull --ff-only, installs the declared dependencies, and then starts app.py:

cd /path/to/arbbot
bash server_start.sh

For Pterodactyl, set the startup command to:

bash server_start.sh

Python-file-only server panels

If the panel only accepts an App Py File and does not let you enter a startup command, upload server_start.py and set it as that file. On its first start it creates .git, connects to the repository, downloads the tracked application files, installs requirements.txt, and starts app.py.

Set GIT_REPOSITORY_URL if your repository URL differs from the default, and set GIT_BRANCH if you deploy a branch other than main. Create the ignored .env file in the server's project folder before starting the bot. Later restarts run a fast-forward Git pull; set SKIP_GIT_PULL=1 to skip that update.

The container must include git, Python, pip, and outbound GitHub access. If Pterodactyl already performs package installation, set SKIP_GIT_PULL=1 only when you intentionally do not want automatic pulls.

git pull --ff-only deliberately refuses to overwrite server-side source edits. Make code changes on the development machine, push them, then pull on the server. Keep server-only secrets and runtime data in the ignored files.

Operational notes

  • The dashboard listens on 0.0.0.0. Put it behind authentication or a private network before exposing it publicly.
  • Logs are written under logs/ and should be rotated by the host.
  • bot_state.db uses SQLite transactions and write-ahead logging so the dashboard never reads a partially written update.
  • The two exchange legs are separate transactions. Use strict notional limits and supervise the bot until atomic execution or a bounded-loss unwind policy is implemented.

Contributors

HuyXCheckerx

22 commits

Languages

Python

70.2%

TypeScript

14.4%

CSS

4.5%

Solidity

4.0%

JavaScript

3.7%

HTML

2.4%