Every seed, documented.
This is the complete operator's manual for QUANTHEHOOD: how agents are funded and run, what each strategy actually does, how backtests mirror live fills, and what sits on-chain. Everything here is drawn from the running platform: the screenshots are captured live, the numbers come from the code.
Robinhood Chain testnet · 46630 testnet funds, nothing real at risk
Chapter 00
What this is
QUANTHEHOOD is a desk for autonomous trading agents. You define a strategy and its limits; the platform funds a dedicated wallet with testnet capital and executes on-chain, around the clock, whether your terminal is open or not.
The metaphor is the product: the dandelion head is your strategy, and every deployed agent is a seed released to work the market on its own. Each seed carries exactly 1,000 USDC of testnet capital, trades five tokenized stocks (TSLA, AMZN, PLTR, NFLX, AMD) against USDC, and reports its live value and PnL back to your desk.
Under the desk sit two price layers and three workers. Real-market candles from Yahoo Finance drive what you see; a Chainlink-mirror oracle on testnet drives what agents fill at. A runner ticks every active agent every 30 seconds, applies your guardrails, and settles every trade as a real on-chain swap you can open in the block explorer.
Chapter 01
Quickstart
From zero to a trading agent in about two minutes. The steps are ordered because each one depends on the last.
Bring a browser wallet
Install MetaMask, Rabby, or Coinbase Wallet. Logging in only ever signs a message. It's gasless and never sends a transaction.Claim testnet ETH (once)
ClaimETHfrom the Robinhood testnet faucet (0.01 ETH per address per 24h). ETH is the only thing you claim. The desk mints your USDC and holds its own tradable stock tokens, so ignore the faucet's stock tokens. Deploying is gated on your wallet holding at least0.005 ETH, proof of a claim; the desk never spends it, so one claim passes the gate forever.Connect at /login
PressConnect wallet. The platform issues a 5-minute nonce, your wallet signs the challenge, and the signature is your login. First connect is account creation. There is no signup form.Deploy an agent
On the desk's order ticket, pick a template (or tune a strategy by hand), name your agent, and pressDeploy agent. The platform creates a dedicated wallet and funds it with1,000 testnet USDCplus a sliver of gas. The agent arrives inactive.Press Start
Start moves the agent into the live book's rotation: it checks the market every 30 seconds, trades inside your guardrails, and records every fill with its transaction hash.Watch the desk
The live book refreshes every 20 seconds with value, PnL, and holdings. ExpandTradesfor the blotter, or follow any tx link into the public explorer.
Chapter 02
The desk
One screen, three instruments: the market chart, the live book, and the order ticket. Everything self-refreshes on its own clock.
The chartshows real Yahoo candles for the selected ticker with your own agents' buys and sells overlaid as markers, plus a dashed oracle line, the price agents actually execute at. The live book lists your agents with status, uptime, value, PnL, and a holdings composition bar; the page header sums it into desk totals (net PnL and active count). The order ticket is a two-tab panel (Deploy and Backtest) whose drafts survive tab switches.
The clockwork
Six independent cadences keep the desk honest. If the book misses two polls in a row, its indicator flips to a red Stale, retrying so you never mistake a frozen number for a live one.
| Every | What happens |
|---|---|
| 30s | The oracle price-pusher writes fresh on-chain prices (execution layer) |
| 30s | The agent runner ticks every active agent's strategy |
| 60s | The candle refresher pulls new 5m market candles (chart layer) |
| 20s | The live book re-polls your agents' value, PnL, and holdings |
| 60s | The market chart refreshes candles without touching your zoom |
| 30s | The market-session badge re-checks US trading hours |
Chapter 03
Agents
An agent is a strategy, a set of guardrails, and its own on-chain wallet. Its whole life runs through four states.
| State | Meaning |
|---|---|
| inactive | Created and funded, not trading. This is where every agent starts, and where Stop returns it. |
| active | In the runner's rotation, evaluated every 30 seconds. |
| killed | Permanently retired behind a confirm dialog. Final value is frozen for the record; no restart. |
| deleted | Soft-removed from your book (only when not active). On-chain history and the wallet survive. |
Capital, exactly once
Deploying funds the agent's wallet from the platform treasury with 1,000 USDC and 0.0001 ETHfor gas, and that is the agent's entire lifetime budget. There is no deposit, no withdrawal, and no key export: the wallet is custodial, its private key AES-256-GCM-encrypted server-side. Want more capital? Deploy another seed. The same 1,000 USDC doubles as the PnL baseline every card measures against.
The card
Each card carries the agent's name, strategy chip, wallet address, status and uptime, live value and signed PnL, and a composition bar of its holdings. Config expands the exact deployed parameters; Trades opens the blotter: pair, amount, price, time, and a tx link straight into the explorer. Sharehands anyone the agent's public explorer page: on this chain, every portfolio is auditable.
Chapter 04
Strategies
Four strategy engines, shared verbatim between live agents and the backtester. Defaults are the deploy form's.
| Strategy | Behavior | Parameters (defaults) |
|---|---|---|
| Threshold / limit | A range trader: buys whenever price is under your buy level, sells the entire holding when it climbs over your sell level. Keeps buying every tick while the price stays low. The position-cap guardrail is the intended brake. | ticker TSLA · buy below $300 · sell above $400 · trade size $100 |
| DCA | Buys a fixed amount on a fixed schedule, regardless of price. The clock only advances when a buy actually executes, so blocked buys retry next tick. | ticker TSLA · amount $50 · interval 3600s (real seconds, in backtests too) |
| Trailing stop / take-profit | One-shot position rider: buys in at start, ratchets a peak, sells everything on a trail-percent drop from peak or a take-profit gain from entry, then permanently stops. If a position cap blocks the entry, it auto-halves the attempt (down to 1/64) until it fits. | ticker TSLA · initial buy $500 · trail 10% · take profit 50% |
| Rebalance to weights | Holds the portfolio at target weights and trades only when an asset drifts outside the band. USDC's weight is implicit. It is never traded directly. Weights must total exactly 100%. | weights TSLA 50 / USDC 50 · band 5% |
Templates
Four one-click presets sit above both the Deploy and Backtest forms. Their taglines quote measured results from a strategy test matrix run on 26 days of real recorded prices, not projections. Applying a template stomps your draft (with an Undo on the toast) and, on Deploy, fills the guardrails too.
| Template | Preset | Measured note |
|---|---|---|
| Range Rider | threshold · TSLA · b<375 s>400 · $100/order · cap $300 | +6.6% on the last 26d of real prices, 2.4% max drawdown |
| Dip Hunter | threshold · TSLA · b<385 s>395 · $50/order · cap $200 · DD 15% | +2.8% on the same 26d window: tighter range, more trades |
| Steady Stacker | DCA · AMZN · $50 hourly · cap $600 | Long-horizon accumulation: judge it over weeks, not days |
| Index 60/40 | rebalance · TSLA 60% / USDC 40% · band 5% · DD 20% | Shines when the market actually moves |
Chapter 05
Guardrails
Hard limits applied after the strategy speaks and before anything executes. A blocked intent simply never trades.
| Guardrail | Exact semantics |
|---|---|
| Max position (USD) | Blocks any buy where the current position value plus the spend would exceed the cap. Sells are never blocked. |
| Max drawdown (%) | While portfolio value sits below peak × (1 − dd%), all buys are blocked. Sells stay allowed. The agent can still de-risk. The peak is tracked automatically, every tick. |
| Kill switch | Blocks everything, buys and sells. The only guardrail that stops selling. |
Strategies never see your guardrails: they propose, guardrails dispose. That separation has one designed interaction: a trailing agent whose initial buy exceeds a lower position cap would deadlock, so its entry auto-halves (investUsd/2ᵏ down to 1/64) until an attempt fits under the cap's headroom.
Chapter 06
Backtesting
Dry-run any config against three kinds of price history (including every daily candle back to each stock's IPO) through the same code path the live runner uses.
| Source | What it replays | Bounds |
|---|---|---|
| Synthetic | A deterministic simulated walk: same seed, same series, every time. Tests strategy logic, not market truth. | steps ≤ 10,000 · seed-reproducible |
| Historical | Real recorded oracle ticks: the exact prices live agents executed at. | 26 days available |
| Market | Real Yahoo market closes. Daily resolution reaches back to each stock's IPO. AMD's series starts in 1980. | 5m ≤ 60d · 1h ≤ 730d · 1d ≤ 20,000d |
Live parity, by construction
The simulator builds the same context the live runner builds, calls the same evaluate, applies the same guardrails, and fills trades with the swap contract's exact quote formula including the 0.30%spread. Timestamps are real epoch seconds, so time-based strategies (DCA's interval) behave identically in replay and live. What it deliberately doesn't model: gas, slippage curves, partial fills.
Results come back as an equity curve plus four metrics: Return, Max drawdown, Trades, and Final value. One click on Deploy this config → hands the exact tested draft to the deploy form. Guardrails are the one thing the bridge does not carry. Set them again before deploying.
Chapter 07
Market data
Two deliberate price layers: real-market candles for your eyes, a Chainlink-mirror oracle for agent fills. The chart shows both, labeled.
The chart layer is genuine Yahoo Finance OHLC, refreshed continuously: about 60 days of 5-minute bars, two years of hourlies, and daily candles to each IPO. The execution layer is the price a worker pushes on-chain every 30 seconds, mirrored from Robinhood Chain mainnet Chainlink feeds. That dashed oracle line is where your agents actually fill. Candles are lime up, red down; your buys print a lime B under the bar, sells a white S above it.
| Interval | Window shown | Depth |
|---|---|---|
| 5m | ≈300 bars, the latest sessions | ~60 days stored |
| 1h | ≈500 bars (2w change label) | ~730 days stored |
| 4h | aggregated from 1h on the fly | 3-month view |
| 1d | up to 5,000 bars (10y label) | back to IPO, TSLA 2010 |
| 1w | Monday-anchored weekly buckets | max, AMD reaches 1980 |
Chapter 08
The chain
Robinhood Chain testnet has no native oracle or DEX, so the platform deploys its own market substrate: mintable tokens, mock Chainlink feeds, and an oracle-priced swap.
| Fact | Value |
|---|---|
| Chain id | 46630. Any other chain is structurally refused |
| Default RPC | robinhood-testnet.drpc.org |
| Explorer | explorer.testnet.chain.robinhood.com |
| Faucet | faucet.testnet.chain.robinhood.com. Users claim ETH here for the deploy gate (also grants 5 canonical stock tokens, which the desk does not use) |
| Token decimals | 18 everywhere, including USDC (unlike the real 6). Oracle prices are 8-decimal integers. |
How a trade settles
Every fill is two real transactions from the agent's own wallet: an ERC-20 approve, then swap on the OraclePricedSwap contract. The pool fills any stock↔USDC pair from treasury-seeded inventory at the oracle price minus a 0.30% spread, the only trading cost on the platform. Default slippage tolerance is 1% with a 120s deadline; stock-to-stock swaps are unsupported by design. Trading reads demand a price fresher than 120s and a live sequencer. A stalled oracle halts trading rather than filling on stale marks.
Contract registry
| Contract | Address |
|---|---|
| OraclePricedSwap | 0x13f89b1B12ac30207d10A830dfB320f50aBfa8ae |
| MockUSDC | 0x234ac7f51C4797B692113d1Cc8EDE24979d3456B |
| TSLA | 0x850207031c45e1EDcBCB0042484c076108a9e5D7 |
| AMZN | 0xc2FB9F4A4d163F1Dbf03797623C4997c6635dFB0 |
| PLTR | 0xcb39B65df7FE9CeB88C12ae09199cFcaDD495A72 |
| NFLX | 0x194cEf30CBE4138CD7d8eC639b9A1cF75A8379bC |
| AMD | 0xb225856F6440922D2eb22446b50187202B48Cee1 |
| Sequencer feed | 0x2Dc4A767058984012B01F02E70a9138649f39Cfc |
Each stock carries its own Chainlink-compatible feed, seeded with 500,000 tokens of swap liquidity. The price-pusher signs a fresh updateAnswer for all five feeds every 30 seconds from mainnet Chainlink reads. NFLX, which has no mainnet feed, mirrors its latest 5-minute Yahoo close instead. A registry check cross-verifies every address against on-chain symbol and decimals at load.
Chapter 09
Access & accounts
Wallet-signature login only. No passwords, no email. Your address is your account.
Login is a nonce-challenge flow: the server issues a single-use nonce (valid for 5 minutes, mirrored into an httpOnly cookie), your wallet signs the exact challenge below via personal_sign, and the server verifies the signature and that the message carries that nonce. Sessions are stateless JWTs carrying your user id and address.
QUANTHEHOOD wants you to sign in with your wallet:
{address}
Sign in to access your agents and portfolio. This request
will not trigger a transaction or cost gas.
Nonce: {nonce}Chapter 10
API reference
Every route the platform exposes. Authenticated routes read your NextAuth session cookie; USD values travel as 1e18 fixed-point strings, oracle prices as 8-decimal integers.
Auth
Wallet-signature login. Fetch a nonce, sign the challenge, exchange it for a JWT session. First sign-in creates the account.
GET/api/auth/wallet/nonce#
Issues a single-use login nonce (32 hex chars), returned in the body and mirrored into the httpOnly qh_wallet_nonce cookie for server-side verification.
- Returns
- { nonce } + Set-Cookie: qh_wallet_nonce (HttpOnly, SameSite=Lax, Max-Age 300s)
POST/api/auth/callback/wallet#
NextAuth credentials callback for the wallet provider. Verifies the personal_sign signature over the exact challenge message and that it contains the literal line “Nonce: <nonce>” matching the cookie, then upserts the user by lowercased address.
- Params
- address, message, signature (via next-auth signIn("wallet", …))
- Returns
- JWT session cookie carrying user.id + user.address
- Errors
- signature/nonce mismatch → login rejected
Agents
Deploy, run, and retire agents. All routes require a session and operate only on agents you own.
GET/api/onboardingsession#
Pre-deploy status for the order ticket: whether your login wallet has passed the faucet gate, and how many live agents the account already runs. The deploy route re-checks both.
- Returns
- { address, ethWei, faucet { url, minWei, minEth, ok: boolean | null }, agents { alive, max } } - ok:null means the balance read failed (RPC blip)
POST/api/agentssession#
Deploys an agent: creates a custodial wallet, funds it from the treasury with 1,000 testnet USDC + 0.0001 ETH gas, stores the 1,000 USDC PnL baseline, and inserts the agent as inactive. Gated on a faucet claim and a per-account live-agent cap.
- Params
- name (required) · strategyType: threshold | dca | trailing | rebalance (required) · strategyConfig (zod-validated per strategy) · guardrails { maxPositionUsd?, maxDrawdownPct?, killSwitch? }
- Returns
- 201 { id, address }
- Errors
- 400 invalid config · 403 FAUCET_GATE (wallet must hold ≥0.005 testnet ETH) · 409 agent limit (5 live max) · 503 “chain RPC unavailable. Try again shortly” (retryable)
GET/api/agentssession#
Lists your agents (soft-deleted excluded) with live on-chain value, PnL vs the 1,000 USDC baseline, and a holdings breakdown. Killed agents return their frozen final value without touching the RPC.
- Returns
- [{ id, name, status, strategyType, address, pnl { pnlUsd, pnlPct } | null, valueUsd, holdings [{ ticker, usd }], strategyConfig, guardrails, createdAt }]
POST/api/agents/{id}/startsession#
Sets the agent active. The runner picks it up on its next 30-second tick.
- Returns
- { id, status: "active" }
- Errors
- 404 not found / not owner
POST/api/agents/{id}/stopsession#
Pauses the agent (status inactive). It can be started again.
- Returns
- { id, status: "inactive" }
- Errors
- 404 not found / not owner
POST/api/agents/{id}/killsession#
Permanently retires the agent and freezes its final portfolio value for the record. Killed agents cannot be restarted.
- Returns
- { id, status: "killed" }
- Errors
- 404 not found / not owner
DELETE/api/agents/{id}session#
Soft-deletes a stopped or killed agent from your book. On-chain history and the wallet key are never destroyed.
- Returns
- { id, status: "deleted" }
- Errors
- 409 “stop or kill the agent before deleting” · 404 not found
GET/api/agents/{id}/tradessession#
The agent's full blotter: every executed fill with amounts, tx hash, and effective fill price.
- Returns
- [{ sellTicker, buyTicker, sellAmount, buyAmount (1e18 wei strings), txHash, priceUsd (8-dp string | null), createdAt }]
- Errors
- 404 not found / not owner
Backtest
One endpoint, three price sources. Fills mirror the on-chain swap quote exactly, including the 0.30% spread.
POST/api/backtestsession#
Replays a strategy config over a price series through the same evaluate → guardrails → execute path the live runner uses, against a virtual portfolio.
- Params
- strategyType, ticker, startingUsd, series (all required) · series.kind: "synthetic" { startUsd, volPct=20, steps=100 (≤10,000), seed=1 } | "recorded" { fromTs, toTs (≤366d) } | "market" { res: 5m|1h|1d, lookbackDays (5m ≤60 · 1h ≤730 · 1d ≤20,000) }
- Returns
- { pnlCurve [{ t, valueUsd }], trades [{ t, sell, buy, sellAmount, buyAmount }], metrics { totalReturnPct, maxDrawdownPct, numTrades, finalValueUsd } }
- Errors
- 400 “no market data for that ticker” / “no price data for that range” / “invalid config: …”
Markets
Public price data. The same feeds the landing page and dashboard chart read.
GET/api/markets#
Ticker board plus platform counters. Oracle prices older than 600 seconds are silently omitted.
- Returns
- { tickers [{ ticker, priceUsd (2-dp string) }], activeAgents, totalTrades }
GET/api/markets/{ticker}/history#
OHLC candles from the Yahoo layer for one of the five tickers, falling back to oracle-tick aggregation when no candles exist. 4h and 1w are SQL-aggregated on the fly; weekly buckets anchor on Monday 00:00 UTC.
- Params
- interval: 5m | 1h | 4h | 1d | 1w (bar windows 300/500/500/5000/3000) · legacy mode without interval: hours 1–168
- Returns
- { ticker, interval, candles [{ time, open, high, low, close }] } ascending
- Errors
- 400 “unknown ticker” / “unknown interval”
GET/api/tradessession#
Your own buy/sell markers for one ticker, windowed to the charted range. This is what overlays B/S arrows on the candles.
- Params
- ticker (required) · hours 1–8760 (default 24)
- Returns
- { markers [{ time, side: buy|sell, agentName, txHash }] } - 500 newest, ascending
- Errors
- 401 · 400 “unknown ticker”
Chain
Raw reads against testnet 46630. Public by design. Every portfolio on this chain is auditable.
GET/api/chain/health#
Liveness probe: asserts the RPC reports chain id 46630 and returns the current block.
- Returns
- { chainId: 46630, blockNumber, ok: true }
GET/api/chain/prices#
All five oracle feeds with freshness (≤120s) and sequencer-up enforcement. All-or-nothing: one stale feed fails the call.
- Returns
- [{ ticker, priceUsd (8-dp integer string), decimals: 8, updatedAt }]
- Errors
- 500 on stale price or sequencer down
GET/api/chain/portfolio#
USD-valued holdings of any address across the asset registry. USDC is pegged at $1; stock prices tolerated up to 3600s stale.
- Params
- address (0x…, required)
- Returns
- { totalUsd (1e18 string), holdings [{ ticker, balance, usd }] } - zero balances omitted
- Errors
- 400 “address required”
GET/api/chain/swap/quote#
Read-only quote from the swap contract: how much buy-token a sell amount fetches, net of the 0.30% spread.
- Params
- sell, buy (tickers) · sellAmount (wei string)
- Returns
- { buyAmount (wei string) }
- Errors
- 400 missing params · 500 on quote revert
POST/api/chain/walletsession#
Creates a standalone custodial wallet for your account (not attached to an agent). The key is AES-256-GCM encrypted server-side; the address is returned, the key never is.
- Returns
- 201 { id, address }
- Errors
- 500 “wallet creation failed” (orphan row rolled back)
Chapter 11
Run your own
One Next.js app, three tsx workers, one Postgres. Fill an env file, push the schema, build, and run under pm2.
| Variable | Purpose |
|---|---|
| DATABASE_URL | Postgres connection string: app, workers, and drizzle-kit all read it |
| AUTH_SECRET | Signs the NextAuth session JWTs |
| AUTH_URL · AUTH_TRUST_HOST | Cookie host for NextAuth: baked at build time; a public hostname needs a rebuild |
| WALLET_ENCRYPTION_KEY | 64 hex chars (32 bytes): AES-256-GCM key for custodial wallet keys |
| TREASURY_PRIVATE_KEY | Funds agent wallets and signs every oracle push. Top up via the faucet |
| RH_CHAIN_RPC_URL | Testnet RPC override (default: dRPC endpoint) |
| RH_MAINNET_RPC_URL | Read-only mainnet RPC for the price-pusher's Chainlink reads |
pnpm install pnpm drizzle-kit push # sync schema (the checked-in SQL migrations lag it) pnpm build && pnpm web # serves on :2555 npx tsx services/price-pusher/index.ts # oracle, every 30s npx tsx services/agent-runner/index.ts # agent ticks, every 30s npx tsx services/candle-refresher/index.ts # Yahoo candles, every 60s
Production runs the same four processes under pm2 (rh-web, rh-pusher, rh-agent-runner, plus the candle refresher) behind a Caddy reverse proxy with automatic TLS. The test suite is 46 vitest files: pnpm test for units, pnpm itest to unlock the live-testnet integration set (it spends real testnet tokens), plus 11 Foundry tests under contracts/.
Chapter 12
Field notes
The honest edges: behaviors that surprise first-time operators, written down so they don't have to.
- Deploying is gated twice: your login wallet must hold a faucet claim (≥
0.005 ETH), and each account caps at 5 live agents. Both exist to protect the shared treasury that funds every seed. - There are two sets of
TSLA/AMZN/PLTR/NFLX/AMDtokens on this testnet: the faucet's canonical ones (unused) and the desk's own mintableMockStockcontracts (what agents actually trade, seeded 500k each into the swap pool). You never need the faucet's stock tokens. Agents buy stock by swapping USDC in the pool. Claim ETH from the faucet; everything else the desk provides. - A threshold agent re-buys every 30-second tick while price sits under its buy level. During a closed-market lull it will happily convert all 1,000 USDC into stock. The position cap is the intended brake.
- A trailing agent is one-shot: after its sell fires, it marks itself done and never re-enters. Restarting doesn't reset it.
- DCA intervals are real wall-clock seconds, in live trading and in backtests alike.
3600means hourly, everywhere. - The 0.0001 ETH gas grant is a lifetime budget. A gas-dry agent still shows active but stops filling. The only evidence is in server logs.
- Profits stay in the agent's custodial wallet forever. There is no withdrawal on a testnet where tokens have no real value. Kill freezes the books; it doesn't liquidate.
- NFLX has no mainnet Chainlink feed. Its oracle mirrors the latest 5-minute Yahoo close, so it can lag the tape by a few minutes.
- If the oracle stalls past 120 seconds, trading halts with a stale-price error rather than filling on old marks. Portfolio valuation tolerates up to an hour.
- Backtest results are history, not forecasts, and Market-source replays fill at Yahoo closes while live agents fill at oracle prices.