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.
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.
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:
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 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.
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.
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.
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:
MIN_TRADE_SIZE_USD to the maximum wallet/pool size.MIN_NET_PROFIT_USD after that estimated cost.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.
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.
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
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.
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.
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
Use a read-only GitHub deploy key:
ssh-keygen -t ed25519 -C arbbot-server and do not add a passphrase for unattended startup.~/.ssh/id_ed25519.pub.ssh -T git@github.com.git clone git@github.com:YOUR_GITHUB_USER/arbbot.git.Never copy a personal GitHub access token into the source code or .env.
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
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.
0.0.0.0. Put it behind authentication or a private network before exposing it publicly.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.22 commits
Python
70.2%
TypeScript
14.4%
CSS
4.5%
Solidity
4.0%
JavaScript
3.7%
HTML
2.4%
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.
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.
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:
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 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.
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.
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.
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:
MIN_TRADE_SIZE_USD to the maximum wallet/pool size.MIN_NET_PROFIT_USD after that estimated cost.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.
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.
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
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.
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.
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
Use a read-only GitHub deploy key:
ssh-keygen -t ed25519 -C arbbot-server and do not add a passphrase for unattended startup.~/.ssh/id_ed25519.pub.ssh -T git@github.com.git clone git@github.com:YOUR_GITHUB_USER/arbbot.git.Never copy a personal GitHub access token into the source code or .env.
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
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.
0.0.0.0. Put it behind authentication or a private network before exposing it publicly.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.22 commits
Python
70.2%
TypeScript
14.4%
CSS
4.5%
Solidity
4.0%
JavaScript
3.7%
HTML
2.4%