Docs Sep 19, 2026

How ArbiStocks Works

Flash arbitrage for Robinhood stock tokens, plainly. What a cycle does, how routes and fees work, how to quote and run one, what can go wrong, and how bots call the engine.

01

Overview

ArbiStocks is flash arbitrage for Robinhood stock tokens on Robinhood Chain. The same stock token (NVDA, GLD, SPY and the rest) trades in several Uniswap v3 pools at once: against USDG, a 6 decimal dollar, and against WETH, at fee tiers of 0.01, 0.05, 0.30 and 1.00%. Nobody keeps those pools in line, so they drift apart.

ArbiStocks closes the gap in one transaction. The engine flash-swaps the cheap pool, sells in the rich pool, repays the first pool from the proceeds and keeps the difference. You bring no capital, only gas. If the cycle does not end with a profit it reverts, so the worst case is the gas of a failed transaction. The protocol keeps 10% of the profit, never of the size. You keep 90%.

The pool scan of Sep 18, 2026 found 49 stocks trading in 136 pools (90 against USDG, 46 against WETH). The window at the top of this page reads the same pools live from chain.

Where To Start

  • The app: scan every stock, pick a route, get an exact quote, run the cycle.
  • The oracle: one consensus price per stock across its pools, with a 30 minute TWAP and the spread between pools.
  • For bots: the engine ABI, an ethers example and the keeper script.
  • Security: invariants, tests, the owner's powers and the known risks.
02

How A Cycle Works

A cycle is a loop through 2 to 4 pools that starts and ends in the same token, USDG or WETH. Uniswap v3 pays out first and collects afterwards, inside a callback. The engine uses that window to run the whole loop before it owes anything.

  1. 1 Borrow
    Pool AThe cheap pool sends the stock first. The engine owes it USDG.
  2. 2 Sell
    Pool BInside A's callback, the engine sells the stock where it is rich.
  3. 3 Check
    Out > OwedIt must now hold more USDG than A is owed. If not, everything reverts.
  4. 4 Repay
    Pool APool A gets exactly what it is owed, from the proceeds.
  5. 5 Split
    90 / 1090% of the profit to you, 10% to the treasury. Same transaction.

Step By Step

  1. Validate. 2 to 4 distinct pools. Each one must be canonical: the Uniswap v3 factory's getPool(token0, token1, fee) returns that exact pool. Every token on the path must be USDG, WETH or a Robinhood stock token, recognised by its runtime code hash. The path must come back to the start token.
  2. Borrow. The engine calls swap on the first pool with an exact input of amountIn of the start token. The pool sends its output (the stock) first and asks for payment in uniswapV3SwapCallback.
  3. Run the rest. Inside that callback the engine holds the stock. It runs hops 2 to n. Each later hop pays its own callback from what the previous hop delivered, and must fill completely.
  4. Repay. The engine ends holding amountOut of the start token. It requires amountOut > owed, pays the first pool exactly what it took, and returns.
  5. Split. profit = amountOut − owed. The fee is profit × feeBps / 10000 and goes to the treasury; the rest goes to to, and must be at least minProfit. The engine emits Arb.

If the first pool fills only part of amountIn, the cycle runs on the real amounts the pool reports. Nothing stays in the engine: it holds no funds between transactions, never grants an allowance and never asks you for one.

Example.Cyclefork test A
LegUSDGWhat happens
Borrow3,440.733614Paid into NVDA/USDG 0.05% for NVDA, owed until the end
Sell3,455.358963NVDA sold in NVDA/USDG 0.30%
Profit14.625349Out minus owed
Fee 10%1.462534To the treasury
To you13.162815The caller started with zero USDG

A real cycle on an anvil fork of Robinhood Chain (block 66,657,028, Sep 19, 2026) after a large test swap pushed one pool out of line. Gas used: 329,539.

03

Routes And Fee Bands

A route is the ordered list of pools a cycle passes through. It buys the stock where it is cheap and sells it where it is rich, and it always comes home to the token it started with.

Route.Types
TypeHopsPathExample label
Same quote2USDG → stock → USDGBuy USDG 0.05% · Sell USDG 0.30%
Same quote2WETH → stock → WETHBuy WETH 0.05% · Sell WETH 0.30%
Cross quote3USDG → stock → WETH → USDGBuy USDG 0.05% · Sell WETH 0.05%, home through WETH/USDG 0.01%
Four hops4USDG → stock → WETH → stock → USDGTwo gaps of the same stock in one cycle

The Fee Band

Every hop pays its pool's fee to that pool's liquidity providers. A gap only pays when the price difference between two pools is wider than the fees on the path between them. That sum is the fee band. Buying in a 0.05% pool and selling in a 0.30% pool costs 35 bps, so the two prices must sit more than 35 bps apart before a single dollar of profit exists.

Fee.Bandsbps, two hops
Buy in / sell in0.01%0.05%0.30%1.00%
0.01%2631101
0.05%61035105
0.30%313560130
1.00%101105130200

A cross quote route adds the 0.01% WETH/USDG hop (1 bp). A stock whose pools sit inside their band reads In band; above it, Open.

Mid Edge And Size

The mid edge of a route is what a tiny cycle would earn at the pools' current prices after their fees: the product over every hop of price × (1 − fee), minus one. It is roughly the gross spread minus the fee band. It is the edge at zero size, not a profit.

Real size moves prices. The first pool gets more expensive as you buy, the second cheaper as you sell. Profit rises with size, peaks, then falls, and the peak is usually well before the gap is gone. The app's Best size finds it: one quoteMany call over a size grid, then a few rounds of refinement around the best point.

A cycle closes a gap down to the fee band and no further, because past that point the next dollar loses money. The fork tests show it: after each cycle the gross spread sat exactly on its band.

Gap.Closesfork tests
Route (NVDA)BandPushed toAfter cycle
USDG 0.05% → USDG 0.30%35 bps121.4 bps35.1 bps
WETH 0.05% → WETH 0.30%35 bps71.7 bps35.1 bps
USDG 0.05% → WETH 0.05% → WETH/USDG 0.01%11 bps68.4 bps11.0 bps
USDG 0.05% → WETH 0.05% → WETH 0.30% → USDG 0.30%70 bps117.3 bps70.3 bps

Gross spread between the route's pools on an anvil fork of Robinhood Chain (block 66,657,028, Sep 19, 2026): after a test swap pushed one pool, and after one arb from an account holding zero tokens.

Widest Gaps Right Now

Read live from chain: every stock's best route by mid edge. A positive mid edge means the gap is wider than its fee band.

Gaps.Livereading
StockPoolsSpreadBest routeMid edge
reading chain

Spread is the widest price difference between a stock's live pools. The app has every stock and every route.

04

Quotes

Quotes are not estimates. quote(pools, start, amountIn) runs the whole cycle against live pool state: the real swaps, the real ticks, the real fees. Inside the first pool's callback, after the last hop and before the first pool is paid, it reverts with Quoted(amountInUsed, amountOut). The engine catches that revert and returns the two numbers. Nothing changes on chain.

  • Unprofitable cycles quote too. The quote stops before the profit check, so it tells you how much a cycle would lose as well as how much it would make.
  • Not a view. quote runs swaps, so call it with eth_call (in ethers: staticCall), never in a transaction.
  • Many at once. quoteMany(routes, starts, amounts) quotes a list of routes and sizes in one call. An entry that fails returns ok = false instead of failing the batch, and each entry may burn at most 10 million gas, so one oversized size cannot starve the rest.
  • No storage needed. The engine's factory, USDG, WETH and stock code hash live in its code, not its storage. So the compiled engine can be placed at an unused address with an eth_call state override and quote with empty storage. That is how the app quoted before deploy; it now quotes the deployed engine, and the numbers match.
Note

A quote is exact for the block it reads. Blocks on Robinhood Chain come about every 0.1 seconds and any trade can move a pool. By the time your transaction lands the gap can be smaller, or gone. That is what the minProfit guard is for.

05

Running A Cycle

In the app, a cycle is four clicks and one signature. You need a wallet on Robinhood Chain with a little ETH for gas. You never approve a token.

  1. Scan. The Scan tab lists every stock with its pools, spread, best route and net edge after pool fees. Pick one marked Open.
  2. Route. Pick a route, press Best size, and read the exact quote: cycle size, gross profit, the 10% protocol fee, the gas estimate and what you receive.
  3. Guard. Choose the least you accept: 25, 50 or 75% of your quoted share. It becomes minProfit. If the gap shrinks below it before your transaction lands, the cycle reverts instead of paying you less.
  4. Run. Press Run Cycle and sign. The button reads No Gap Right Now when the quote is not positive.

After it lands, the app shows the real profit from the receipt's Arb event and links the transaction. If it reverts, the app says why in plain words.

Gas.Usedper cycle
CycleGas
2 hops, USDG329,539
2 hops, WETH357,332
3 hops499,105
4 hops511,560

L2 execution gas of arb in the fork runs (block 66,657,028, Sep 19, 2026). It moves with the ticks a swap crosses. The live node adds a small L1 data part, which the app asks the node for.

Reverts.Plain
ErrorMeans
UnprofitableThe gap closed. Nothing moved but gas.
MinProfitIt still paid, but less than your guard.
ExpiredPast the deadline. Quote again.
IsPausedThe owner paused cycles. Quotes still work.
PartialFillA later pool could not fill the size.
06

Fees

  • 10% of the profit, never of the size. The engine's feeBps is 1000. In fork test A a cycle of 3,440.73 USDG made 14.625349 USDG: 1.462534 went to the treasury and 13.162815 to the caller.
  • Zero on a loss. A cycle without profit reverts, so there is nothing to take a share of. You pay the gas of the failed transaction.
  • Same transaction, same token. The fee is paid in the start token (USDG or WETH) to the treasury inside the cycle. It rounds down.
  • Capped at 20% in the code. MAX_FEE_BPS is a constant of 2000. The owner can set the fee anywhere from 0 to 20% and never above.
  • Pool fees are not ours. Each hop pays its Uniswap fee (0.01, 0.05, 0.30 or 1.00%) to that pool's liquidity providers. Every quote already includes them.
  • Gas is yours. Paid in ETH on Robinhood Chain, a few cents for a cycle.

The engine is the authority on the live fee: read feeBps(). Your minProfit is what you want to receive, after the fee.

07

Risks

Flash cycles cannot lose your principal, because there is none. They can still cost you, and nothing here is guaranteed.

  • Profit is not guaranteed. A quote is exact for one block. The pools can move before your transaction lands.
  • Another bot can take the gap first. Then your cycle no longer pays, reverts, and costs you gas.
  • A cycle that no longer pays reverts and costs gas. Every failed attempt costs that transaction's gas and nothing else.
  • Stock tokens are Robinhood's. Their token contracts can pause or restrict transfers. While a stock is paused, every route through it reverts.
  • Lookalike tokens. The engine recognises stock tokens by their proxy code, not by who deployed them, so a copy of that proxy passes the check. It cannot hurt the engine, which holds nothing, but a hand-built route through one is the caller's choice. The app and the keeper only build routes from the stock list in the config.
  • Contract risk. The engine is covered by unit, fuzz, invariant and fork tests, and has not had a third-party audit yet. See Security.
  • Owner powers. One owner key can pause cycles and set the fee anywhere up to 20%. It cannot touch a cycle's funds or yours.
  • Chain and data. Robinhood Chain is an Arbitrum Nitro chain with one sequencer: if it stops, nothing settles. If our read proxy fails, pages show the last snapshot with its date.
  • Not advice. ArbiStocks is software, not a broker, and gives no investment advice. It is not affiliated with Robinhood or Uniswap.
08

For Bots

The engine is public: anyone can call arb, from a wallet or a script, and the protocol fee is the same for everyone. The ABI and the pool list ship with the site as plain ES modules: /js/abi.js (ENGINE_ABI) and /js/config.js (ENGINE, USDG, WETH and UNIVERSE, every stock with its pools). This page never prints an address: read ENGINE.address from the config, where it carries the deployed engine and its deploy block.

ENGINE_ABI (core)
function arb(address[] pools, address start, uint256 amountIn, uint256 minProfit, address to, uint256 deadline) returns (uint256 profit, uint256 fee)
function quote(address[] pools, address start, uint256 amountIn) returns (uint256 amountInUsed, uint256 amountOut)
function quoteMany(address[][] routes, address[] starts, uint256[] amounts) returns (uint256[] amountInUsed, uint256[] amountOut, bool[] ok)
function path(address[] pools, address start) view returns (address[] tokens)
function feeBps() view returns (uint16)
function MAX_FEE_BPS() view returns (uint16)
function MAX_HOPS() view returns (uint8)
function paused() view returns (bool)
event Arb(address indexed caller, address indexed to, address indexed start, address[] pools, uint256 amountIn, uint256 amountOut, uint256 profit, uint256 fee)
error Unprofitable(uint256 amountInUsed, uint256 amountOut)
error MinProfit(uint256 received, uint256 minProfit)
error Quoted(uint256 amountInUsed, uint256 amountOut)
ENGINE_ABI (full, from /js/abi.js)open
Loading the ABI from /js/abi.js.

Many Candidates In One Transaction

A route you quoted can be gone by the time you land. The runner takes up to 16 candidate cycles, best first, tries each through the engine under its own gas cap, and keeps the first that pays. The engine pays your to directly, so the runner holds nothing. If none pay it reverts with NoneProfitable(tried, outOfGas); a high outOfGas means raise the cap. Read its address from PERIPHERY.runner in the config.

ArbiRunner
function runFirstProfitable(address[][] routes, address[] starts, uint256[] amounts, uint256[] minProfits, address to, uint256 deadline, uint256 gasPerCandidate) returns (uint256 index, uint256 profit, uint256 fee)
error NoneProfitable(uint256 tried, uint256 outOfGas)
error NotEnoughGas(uint256 tried)

Pass 800,000 or more as gasPerCandidate: a 4 hop cycle used about 512,000 on a mainnet fork.

The Desk

The desk runs cycles with itself as the recipient, books a share of each profit for whoever ran it and the rest for its owner, and each side withdraws with claim(token). It is closed to the keepers its owner allows. It is built for keepers the owner runs or pays: the engine is open to everyone, and calling it directly keeps 90% of a cycle's profit, which is more than a desk bounty. Read its address from PERIPHERY.desk.

Quote With Ethers

quote.mjs
// exact quote for one route (ethers v6). config.js and abi.js come from arbistocks.com/js/
import { ethers } from 'ethers';
import { ENGINE, USDG, UNIVERSE } from './config.js';
import { ENGINE_ABI } from './abi.js';

const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); // any Robinhood Chain node
const engine = new ethers.Contract(ENGINE.address, ENGINE_ABI, provider);

// NVDA: buy in its USDG 0.05% pool, sell in its USDG 0.30% pool (try both directions)
const nvda = UNIVERSE.find((s) => s.symbol === 'NVDA');
const pool = (quote, fee) => nvda.pools.find((p) => p.quote === quote && p.fee === fee).address;
const route = [pool('USDG', 500), pool('USDG', 3000)];

const amountIn = ethers.parseUnits('1000', USDG.decimals);           // 1,000 USDG
const [used, out] = await engine.quote.staticCall(route, USDG.address, amountIn);
const profit = out - used;                                           // negative when it would lose
console.log(ethers.formatUnits(profit, USDG.decimals), 'USDG');

Run The Cycle

arb.mjs
// continues quote.mjs. The account needs ETH for gas and nothing else.
const signer = new ethers.Wallet(process.env.KEEPER_KEY, provider);
const feeBps = BigInt(await engine.feeBps());                      // 1000 = 10% of profit

if (profit > 0n) {
  const mine = profit - (profit * feeBps) / 10000n;                  // what you would receive
  const minProfit = mine / 2n;                                       // guard: revert below half of it
  const deadline = Math.floor(Date.now() / 1000) + 120;
  const tx = await engine.connect(signer).arb(route, USDG.address, amountIn, minProfit, signer.address, deadline);
  const receipt = await tx.wait();
  const arb = receipt.logs.map((l) => engine.interface.parseLog(l)).find((e) => e?.name === 'Arb');
  console.log('profit', arb.args.profit, 'fee', arb.args.fee);
}

Sizes In One Call, And Quoting Without A Deploy

many.mjs
// three sizes of the same route in one eth_call
const sizes = ['250', '1000', '5000'].map((s) => ethers.parseUnits(s, USDG.decimals));
const [usedMany, outMany, ok] = await engine.quoteMany.staticCall(
  sizes.map(() => route), sizes.map(() => USDG.address), sizes);

// without a deploy: put the compiled engine (ENGINE_RUNTIME in /js/engine-code.js) at ENGINE.previewAddress
const iface = new ethers.Interface(ENGINE_ABI);
const data = iface.encodeFunctionData('quote', [route, USDG.address, amountIn]);
const raw = await provider.send('eth_call', [
  { to: ENGINE.previewAddress, data }, 'latest', { [ENGINE.previewAddress]: { code: ENGINE_RUNTIME } }]);
const [usedPreview, outPreview] = iface.decodeFunctionResult('quote', raw);

The Keeper

The engine ships with a keeper script, tools/keeper.mjs. It scans every stock the way the app does, prints the opportunities, and only sends when you ask it to. It reads the key from the KEEPER_KEY environment variable and never prints it.

keeper
node tools/keeper.mjs                          # scan once, print opportunities, send nothing
node tools/keeper.mjs --symbols NVDA,SPY       # only these stocks
node tools/keeper.mjs --loop 15                # scan every 15 seconds until stopped
node tools/keeper.mjs --json                   # machine-readable output
KEEPER_KEY=<key> node tools/keeper.mjs --send  # run the best cycle if it clears --min-usd after gas

# --rpc URL      the node to read and send through (default: $RPC_URL, else the public node)
# --min-usd N    least net profit worth sending: your 90% minus gas (default 0.50)
# --guard P      minProfit = P% of what you would receive at the quote (default 50)
# --to ADDRESS   who receives the profit (default: the key's own address)
# --top N        rows to print (default 10)

The engine is deployed, so the keeper can send; without a deployed engine it quotes in preview and --send refuses. Nodes on Robinhood Chain cap JSON-RPC batches at 5 requests and answer an occasional 429: batch reads through Multicall3 and retry quietly.

09

Security

The engine is one contract in Solidity 0.8.28 with no proxy and no upgrade path. What it must never do, and how that is enforced:

  • It holds nothing between transactions. Profit leaves in the same transaction it is made.
  • Callbacks are authenticated. Before every swap the engine records the one pool it expects to hear from, in transient storage; any other caller reverts. The first pool's callback also checks the hash of the route.
  • Only canonical pools. factory.getPool(token0, token1, fee) must return the pool itself.
  • Only USDG, WETH and stock tokens. Tokens are recognised by runtime code hash, not by name.
  • Profit or revert. A cycle that does not end with more than it borrowed reverts, and so does one that pays you less than minProfit.
  • The fee is capped at 20% in the code, and arb, quote and quoteMany share a transient reentrancy lock.

The tests behind each of these, the owner's exact powers and the known risks are on the Security page.

10

FAQ

Do I need any tokens to run a cycle?

No. Only ETH on Robinhood Chain for gas. The engine borrows from the first pool inside the transaction and pays it back before the transaction ends.

What if the gap closes before my transaction lands?

It reverts. Nothing moves, and you pay the gas of a failed transaction.

How much does ArbiStocks take?

10% of the profit of a cycle, in the same transaction. Nothing of the size, and nothing on a loss.

Can the fee go up?

The owner can set it between 0 and 20%. The 20% ceiling is a constant in the contract; nobody can go above it.

Do I approve anything?

No. The engine never asks for an allowance and never holds your tokens.

Why do quotes change every few seconds?

Blocks come about every 0.1 seconds and every trade moves a pool. The app quotes again every 6 seconds while the tab is open.

Which stocks can I arb?

Every Robinhood stock token with two or more live Uniswap v3 pools on Robinhood Chain: 49 at the Sep 18, 2026 scan.

Why does a stock say In band?

Its pools differ by less than the pool fees between them, so no cycle pays. It turns Open when the spread leaves its fee band.

Is the engine audited?

Not by a third party yet. It has unit, fuzz, invariant and fork tests, listed on the Security page.

Is this investment advice?

No. ArbiStocks is software. Profit is never guaranteed and a failed cycle costs gas.

11

Glossary

bps
Basis points. 1 bp is 0.01%, 100 bps is 1%.
Cycle
One arb call: borrow from the first pool, run every hop, repay, split the profit.
Flash swap
A Uniswap v3 swap that pays out first and collects inside a callback, in the same transaction.
Route
The ordered pools of a cycle, 2 to 4 of them, starting and ending in the same token.
Hop
One swap in one pool.
Start token
USDG or WETH: what the cycle borrows, ends in and pays profit in.
USDG
Global Dollar, a dollar token with 6 decimals, the main quote token of stock pools on Robinhood Chain.
WETH
Wrapped ether, the other quote token.
Fee tier
A Uniswap v3 pool's swap fee: 0.01, 0.05, 0.30 or 1.00%, paid to its liquidity providers.
Spread
The widest price difference between a stock's live pools, in bps.
Fee band
The sum of pool fees on a route. A gap inside it does not pay.
Mid edge
What a tiny cycle would earn at current prices after pool fees: roughly spread minus fee band.
Profit
What a cycle ends with minus what the first pool took.
Protocol fee
10% of the profit, to the treasury, capped at 20% in the code.
minProfit
The least the caller accepts to receive after the fee. Below it the cycle reverts.
Canonical pool
A pool the Uniswap v3 factory itself returns for its two tokens and fee.
Stock code hash
The runtime code hash every Robinhood stock token shares; the engine uses it to recognise real stock tokens.
State override
An eth_call option that runs code at an address as if it were deployed there. The app quoted this way before the engine was deployed.
Keeper
A script that scans for gaps and runs cycles on its own.