DEV Community

Cover image for Building a Stock Token Trading Bot on Robinhood Chain with TypeScript
hamssog
hamssog

Posted on Originally published at hamssog.substack.com

Building a Stock Token Trading Bot on Robinhood Chain with TypeScript

A Stock Token trading bot can start as a simple price-monitoring script.

But turning it into a useful automated trading system requires much more than detecting a price.

The bot has to understand:

Asset
→ Price
→ Strategy
→ Risk
→ Execution
→ Transaction State
→ Position
→ Reconciliation
Enter fullscreen mode Exit fullscreen mode

That is the architecture I would use for building automated Stock Token trading on Robinhood Chain.

Robinhood Chain is EVM-compatible and currently uses chain ID 4663. Stock Tokens are ERC-20 tokens with 18 decimals, and Robinhood provides onchain Chainlink price feeds for them.


1. The architecture

Instead of putting everything into one trading loop, I would separate the system into services or modules:

                    STOCK TOKEN DATA
                           ↓
                  ┌────────────────┐
                  │ Asset Registry │
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │  Price Layer   │
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │ Strategy Engine│
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │   Risk Engine  │
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │    Executor    │
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │ Tx State       │
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │ Position State │
                  └───────┬────────┘
                          ↓
                  ┌────────────────┐
                  │ Reconciliation │
                  └────────────────┘
Enter fullscreen mode Exit fullscreen mode

This separation gives each component a clear job.


2. Asset discovery

The first step is knowing what Stock Tokens are available.

Robinhood's current /assets API exposes metadata including:

tokenSymbol
tokenName
deployments
chainId
currentMultiplier
pendingMultiplier
tradingCapabilities
status
Enter fullscreen mode Exit fullscreen mode

The API also exposes the canonical contract deployment for Robinhood Chain.

I would normalize that into an internal TypeScript type:

interface StockTokenAsset {
  symbol: string;
  contractAddress: string;
  chainId: number;

  currentMultiplier: string;

  status: "ACTIVE" | "INACTIVE";

  tradingCapabilities: unknown;
}
Enter fullscreen mode Exit fullscreen mode

The asset registry can then become the source of truth for the rest of the application.

API
 ↓
Asset Registry
 ↓
Trading System
Enter fullscreen mode Exit fullscreen mode

Instead of every strategy trying to rediscover the asset independently.


3. Price ingestion

Robinhood provides a REST price endpoint:

GET /rhj/prices/{symbol}
Enter fullscreen mode Exit fullscreen mode

The current API documentation says this endpoint is cached for 15 seconds and rate-limited to 60 requests/second. The response includes bid, ask, generated timestamp, trading-halt information, and other fields.

That means a bot should retain quote metadata instead of treating the returned number as timeless.

interface ReferenceQuote {
  symbol: string;
  bid: number;
  ask: number;
  generatedAt: number;
  isTradingHalt: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Then:

function isFresh(
  quote: ReferenceQuote,
  maxAgeMs: number
): boolean {
  return Date.now() - quote.generatedAt <= maxAgeMs;
}
Enter fullscreen mode Exit fullscreen mode

A trading strategy should be able to reject stale data before it generates a trade intent.


4. Price normalization

This is one of the most important implementation details.

Robinhood documents that the REST /prices endpoint returns the raw underlying-equity bid/ask and is not multiplier-adjusted.

The onchain Chainlink price is multiplier-adjusted.

When combining those surfaces, the application needs to apply currentMultiplier appropriately.

So I would make normalization explicit:

interface PricePoint {
  symbol: string;
  source: "REST" | "CHAINLINK" | "DEX";
  priceUsd: number;
  timestamp: number;
  multiplierAdjusted: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Then the price adapter handles the transformation:

REST
 ↓
Raw underlying price
 ↓
Multiplier
 ↓
Normalized value
Enter fullscreen mode Exit fullscreen mode

while:

Chainlink
 ↓
Already multiplier-adjusted
 ↓
Normalized value
Enter fullscreen mode Exit fullscreen mode

The strategy receives normalized values rather than worrying about where they came from.


5. Corporate actions

The multiplier is not just a technical field.

It is part of the economic representation of the Stock Token.

Robinhood's API exposes currentMultiplier, pendingMultiplier, and a corporate-actions endpoint. The documentation describes events including forward splits, reverse splits, dividends, mergers, spin-offs, redemptions, and other actions.

That means the system can model:

Corporate Action
       ↓
Multiplier Change
       ↓
Price Normalization
       ↓
Portfolio Valuation
       ↓
Strategy Calculation
Enter fullscreen mode Exit fullscreen mode

This should be handled at the asset/data layer instead of hidden inside individual strategies.


6. Strategy engine

The strategy should not know how to send transactions.

It should answer:

Given the current market state, should I create a trade intent?

For example:

interface TradeIntent {
  symbol: string;
  side: "BUY" | "SELL";
  quantity: bigint;

  maxSlippageBps: number;
  strategyId: string;

  createdAt: number;
}
Enter fullscreen mode Exit fullscreen mode

A strategy might generate:

return {
  symbol: "AAPL",
  side: "BUY",
  quantity: 10n,
  maxSlippageBps: 50,
  strategyId: "mean-reversion-v1",
  createdAt: Date.now()
};
Enter fullscreen mode Exit fullscreen mode

The strategy has now expressed what it wants.

It has not executed anything.


7. Different strategies can use the same infrastructure

That separation becomes valuable quickly.

The execution system can support:

Momentum
Mean reversion
Arbitrage
Scheduled trading
Rebalancing
Signal-based trading
Enter fullscreen mode Exit fullscreen mode

without creating a completely different transaction stack for every strategy.

Conceptually:

                  ┌─ Momentum
                  │
Strategies ───────┼─ Arbitrage
                  │
                  ├─ Mean Reversion
                  │
                  └─ Rebalancing
                         ↓
                    Risk Engine
                         ↓
                    Execution
Enter fullscreen mode Exit fullscreen mode

The strategy changes.

The execution infrastructure does not have to.


8. Risk engine

Before anything is signed, the trade should pass through a separate risk layer.

Typical rules include:

max trade size
max position size
max exposure
min expected edge
max slippage
max gas cost
max quote age
max concurrent positions
Enter fullscreen mode Exit fullscreen mode

For example:

interface RiskLimits {
  maxTradeUsd: number;
  maxPositionUsd: number;
  minEdgeBps: number;
  maxSlippageBps: number;
  maxQuoteAgeMs: number;
}
Enter fullscreen mode Exit fullscreen mode

Then:

interface RiskDecision {
  approved: boolean;
  reason?: string;
}
Enter fullscreen mode Exit fullscreen mode

A rejected trade should be treated as a normal state of the system, not an exception.

For example:

TRADE_INTENT
     ↓
RISK_CHECK
     ↓
REJECTED
Enter fullscreen mode Exit fullscreen mode

with a reason such as:

Position limit exceeded
Enter fullscreen mode Exit fullscreen mode

9. Arbitrage is a strategy, not the entire architecture

A Stock Token arbitrage strategy might compare:

Reference Price
        ↓
Normalized
        ↓
Onchain / DEX Price
Enter fullscreen mode Exit fullscreen mode

The naive calculation is:

grossSpread = sellPrice - buyPrice;
Enter fullscreen mode Exit fullscreen mode

But a more useful model is:

Gross Spread
- Trading Fees
- Slippage
- Gas
- Execution Costs
- Safety Buffer
-----------------
Expected Net Edge
Enter fullscreen mode Exit fullscreen mode

Then:

if (expectedNetEdgeBps < limits.minEdgeBps) {
  return reject("Insufficient edge");
}
Enter fullscreen mode Exit fullscreen mode

This distinction is important because a visible spread does not necessarily represent an executable profit opportunity.


10. Liquidity checks

The bot also needs to consider trade size.

Suppose:

Quoted spread = 1.2%
Enter fullscreen mode Exit fullscreen mode

That does not mean a $1,000 trade and a $100,000 trade can both execute at the same effective price.

The strategy should request an executable quote:

interface ExecutionQuote {
  amountIn: bigint;
  expectedAmountOut: bigint;
  minimumAmountOut: bigint;
  priceImpactBps: number;
}
Enter fullscreen mode Exit fullscreen mode

Then risk can evaluate:

Requested amount
      ↓
Liquidity
      ↓
Price impact
      ↓
Expected output
      ↓
Minimum output
Enter fullscreen mode Exit fullscreen mode

This is much closer to an actual trading decision.


11. Trading capabilities

Trading availability also belongs in the decision process.

The current Stock Token asset API exposes tradingCapabilities, while Robinhood's documentation notes that different assets can have different availability across market, extended, and overnight sessions. Developers should check the asset's capabilities before execution.

The execution gate therefore becomes:

ASSET ACTIVE
     ↓
SESSION ALLOWED
     ↓
TRADE ALLOWED
     ↓
QUOTE FRESH
     ↓
LIQUIDITY OK
     ↓
RISK APPROVED
     ↓
EXECUTE
Enter fullscreen mode Exit fullscreen mode

That is much safer than assuming every asset is always tradable.


12. Transaction execution

Once a trade passes risk checks, the executor builds the transaction.

But execution should also be stateful.

I would define:

type TransactionState =
  | "CREATED"
  | "SIGNED"
  | "SUBMITTED"
  | "PENDING"
  | "CONFIRMED"
  | "FAILED"
  | "UNKNOWN";
Enter fullscreen mode Exit fullscreen mode

The happy path is:

CREATED
   ↓
SIGNED
   ↓
SUBMITTED
   ↓
PENDING
   ↓
CONFIRMED
Enter fullscreen mode Exit fullscreen mode

But production systems need another branch.

SUBMITTED
   ↓
RPC TIMEOUT
   ↓
UNKNOWN
   ↓
RECONCILIATION
Enter fullscreen mode Exit fullscreen mode

An RPC timeout is not equivalent to a failed transaction.

The transaction may already have been broadcast or confirmed.


13. Never blindly retry an unknown transaction

This is one of the most important rules in automated execution.

Consider:

Bot submits transaction
        ↓
RPC request times out
        ↓
Bot assumes failure
        ↓
Bot submits another transaction
Enter fullscreen mode Exit fullscreen mode

Now there may be two transactions.

Instead:

SUBMITTED
   ↓
UNKNOWN
   ↓
Find transaction
   ↓
Check receipt
   ↓
Check state
Enter fullscreen mode Exit fullscreen mode

Only after reconciliation should the system determine whether another action is necessary.

This is why transaction state should be part of the architecture from day one.


14. Persistent state

Important trading state should not live only in memory.

I would persist:

trade intent
transaction hash
asset
side
requested quantity
filled quantity
execution price
status
timestamps
strategy ID
Enter fullscreen mode Exit fullscreen mode

For example:

interface TradeRecord {
  id: string;
  symbol: string;
  side: "BUY" | "SELL";

  requestedQuantity: string;
  filledQuantity: string;

  txHash?: string;

  executionPrice?: string;

  status: TransactionState;

  createdAt: Date;
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

A PostgreSQL database is a reasonable choice for this state layer.


15. Position management

Trade state and position state are related, but they are not the same thing.

A position might be:

interface Position {
  symbol: string;
  quantity: bigint;
  averageEntryPrice: number;
  realizedPnl: number;
  unrealizedPnl: number;
}
Enter fullscreen mode Exit fullscreen mode

Then:

Trades
  ↓
Fills
  ↓
Position Engine
  ↓
Portfolio
Enter fullscreen mode Exit fullscreen mode

This allows multiple executions to contribute to the same position.


16. Reconciliation

The blockchain ultimately determines what happened.

Suppose the local database records:

BUY 100
Enter fullscreen mode Exit fullscreen mode

but the onchain result is:

63 tokens received
Enter fullscreen mode Exit fullscreen mode

The portfolio should eventually reflect:

63
Enter fullscreen mode Exit fullscreen mode

Reconciliation can compare:

              DATABASE
                  │
        ┌─────────┼─────────┐
        ↓         ↓         ↓
   Tx Receipt  Balances  Events
        └─────────┼─────────┘
                  ↓
          Canonical State
Enter fullscreen mode Exit fullscreen mode

This process can run periodically and after important execution events.

It provides recovery from:

RPC errors
missed events
application crashes
partial execution
delayed transaction updates
Enter fullscreen mode Exit fullscreen mode

17. Recommended TypeScript project structure

I would organize the project roughly like this:

src/
├── assets/
│   ├── assetRegistry.ts
│   └── capabilities.ts
│
├── market-data/
│   ├── robinhoodApi.ts
│   ├── chainlink.ts
│   └── dex.ts
│
├── pricing/
│   ├── normalize.ts
│   ├── quotes.ts
│   └── liquidity.ts
│
├── strategies/
│   ├── arbitrage.ts
│   ├── momentum.ts
│   └── index.ts
│
├── risk/
│   └── riskEngine.ts
│
├── execution/
│   ├── executor.ts
│   ├── transaction.ts
│   └── stateMachine.ts
│
├── portfolio/
│   ├── positions.ts
│   └── pnl.ts
│
├── reconciliation/
│   └── reconcile.ts
│
└── index.ts
Enter fullscreen mode Exit fullscreen mode

The benefit is that every layer can be tested independently.


18. Dry-run mode

Before enabling live execution, I would make dry-run a first-class feature.

DRY RUN
   ↓
Market Data
   ↓
Strategy
   ↓
Risk
   ↓
Simulated Execution
   ↓
Trade Journal
Enter fullscreen mode Exit fullscreen mode

No transaction is broadcast.

For example:

{
  "symbol": "AAPL",
  "side": "BUY",
  "quantity": "10",
  "expectedEdgeBps": 42,
  "slippageBps": 18,
  "riskApproved": true,
  "execution": "SIMULATED"
}
Enter fullscreen mode Exit fullscreen mode

This gives the operator visibility into what the bot would do before connecting a funded wallet.


19. Monitoring

The dashboard should expose more than P&L.

Useful metrics include:

signals detected
signals rejected
risk rejection rate
average expected edge
executed trades
failed transactions
unknown transactions
transaction latency
stale quotes
reconciliation mismatches
current exposure
realized P&L
Enter fullscreen mode Exit fullscreen mode

For example:

Signals                    1,482
Risk Approved                 87
Executed                      36
Confirmed                     34
Failed                         1
Unknown                        1
Enter fullscreen mode Exit fullscreen mode

That makes the trading system observable.


20. Extending the bot into a complete product

The same infrastructure can support several different product scopes.

MVP

single wallet
single strategy
price monitoring
basic risk
automated execution
trade history
Enter fullscreen mode Exit fullscreen mode

Multi-strategy bot

multiple strategies
strategy configuration
portfolio limits
persistent state
reconciliation
Enter fullscreen mode Exit fullscreen mode

Trading application

web dashboard
wallet management
portfolio
P&L
alerts
execution history
API
monitoring
Enter fullscreen mode Exit fullscreen mode

Larger trading infrastructure

multi-wallet execution
multiple strategies
historical data
backtesting
strategy APIs
advanced risk
analytics
Enter fullscreen mode Exit fullscreen mode

The core execution engine can remain reusable across these versions.


21. Robinhood Chain gives the system an EVM-based foundation

One reason this architecture is practical is that Robinhood Chain is EVM-compatible.

Robinhood's current documentation supports standard Ethereum tooling for smart-contract development, including Foundry and Hardhat, and provides both mainnet and testnet environments. Mainnet uses chain ID 4663, while testnet uses 46630.

That means a TypeScript application can use the normal EVM ecosystem for:

wallets
RPC
viem
ethers
contract calls
transaction signing
event processing
Enter fullscreen mode Exit fullscreen mode

rather than requiring an entirely different application architecture.


22. Onchain data can also become more real-time

For applications that require faster market-data reactions, Robinhood's current documentation also provides Chainlink Data Streams support on Robinhood Chain.

The documentation describes Data Streams as a pull-based oracle system designed for high-frequency applications, with TypeScript SDK support and sub-second data delivery.

This creates an interesting progression:

Basic Bot
    ↓
REST API
    ↓
Onchain Oracle
    ↓
Event Monitoring
    ↓
Real-Time Data Streams
Enter fullscreen mode Exit fullscreen mode

The appropriate data architecture depends on the strategy's actual latency requirements.


23. My implementation approach

I have been working with a TypeScript implementation around this type of architecture for Robinhood Chain Stock Tokens.

The important objective is not just:

detect price
Enter fullscreen mode Exit fullscreen mode

but:

DATA
 ↓
STRATEGY
 ↓
RISK
 ↓
EXECUTION
 ↓
STATE
 ↓
RECONCILIATION
Enter fullscreen mode Exit fullscreen mode

That architecture can then be reused for other trading products.

GitHub:

https://github.com/0xhamssog/robinhood-stock-token-arbitrage-bot

The public project is listed as a Robinhood Chain Stock Token arbitrage bot using Uniswap, Chainlink, and REST data sources.


24. What can be built on top of it?

Once the core system is working, the same infrastructure can support:

Stock Token Trading Bot

Automated trading based on configurable strategies.

Stock Token Arbitrage Bot

Compare normalized external/reference prices with executable onchain prices.

Stock Token Portfolio Tracker

Track balances, positions, exposure, and P&L.

Robinhood Chain Trading Terminal

Provide market discovery, wallet management, charts, orders, and portfolio views.

Multi-Wallet Trading System

Coordinate multiple execution wallets under centralized strategy and risk controls.

Trading API

Expose the execution engine to external applications.

The bot becomes the foundation rather than the final product.


Conclusion

Building a Stock Token trading bot is not primarily a matter of writing a buy() function.

The harder problems are:

How fresh is the data?

Is the price normalized?

Is the asset currently tradable?

Is there enough liquidity?

What does the strategy actually want to do?

Does the trade pass risk?

What was submitted?

What actually happened?

What is the resulting position?
Enter fullscreen mode Exit fullscreen mode

A robust architecture answers those questions independently.

The resulting system looks like:

Stock Token Data
      ↓
Price Normalization
      ↓
  Strategy
      ↓
     Risk
      ↓
Execution
      ↓
Transaction State
      ↓
  Position
      ↓
Reconciliation
Enter fullscreen mode Exit fullscreen mode

That same foundation can grow from a small trading-bot MVP into a multi-wallet automated trading platform.

That is the type of trading infrastructure I build on Robinhood Chain: strategy-specific automation with real execution, risk controls, persistent state, and reconciliation-not just a script that watches prices.

Need a custom Stock Token trading bot?

I build custom Robinhood Chain trading systems ranging from focused trading-bot MVPs to larger automated trading applications.

GitHub:
https://github.com/0xhamssog/robinhood-stock-token-arbitrage-bot

Top comments (0)