Guides
How to Build a Copy Trading Bot on Arcus (Robinhood Chain) cover

How to Build a Copy Trading Bot on Arcus (Robinhood Chain)

Build a copy trading bot for Arcus on Robinhood Chain. Detect a target wallet's Stock Token trades on-chain over WebSocket, then mirror them through the Arcus spot flow.

LanguagePython
NetworkRobinhood Chain
ProtocolArcus

Copy trading is one of the clearest use cases for on-chain finance: pick a wallet whose trades you want to follow, watch what it does, and replicate it. On Arcus, the DEX built by the dYdX team on Robinhood Chain, that target is now trading tokenized stocks 24/7. Every spot trade settles on-chain, which means you can detect it from the blockchain itself, no permission required. This guide shows how to build a copy trading bot that watches a target wallet's Arcus spot trades in real time and mirrors them.

Quick answer: Arcus spot trades on tokenized stocks settle on-chain on Robinhood Chain (chain ID 4663), so you detect a target wallet's trades by subscribing to its Transfer event logs over a WebSocket RPC and pairing the USDG leg with the Stock Token leg in each transaction. You replicate through the Arcus spot flow. Dwellir provides the archive WebSocket endpoint the detection half needs.

Before You Start: Eligibility

Arcus is not available to users in the United States, Canada, or the United Kingdom, and it enforces compliance and KYC status at the protocol level. A bot's signing wallet and operator must be in an eligible jurisdiction and pass Arcus's compliance checks to place trades. The detection half of this tutorial (reading public on-chain data) works from anywhere. The replication half requires an eligible, compliant Arcus account. Build accordingly, and treat this as an educational walkthrough, not financial or legal advice.

How Copy Trading Works on Arcus

Arcus runs two products with different mechanics, and the distinction shapes the whole bot:

  • Spot (live today): 95 Stock Tokens, traded 24/7 with zero fees at launch. Arcus does not run its own spot order book. It acts as a router across on-chain AMM pools and a request-for-quote (RFQ) maker network, and each trade settles atomically on-chain on Robinhood Chain. Because settlement is on-chain, spot trades are publicly observable.
  • Perpetuals (waitlisted): 35 RWA perpetual markets running on a dYdX-style off-chain order book with a REST and WebSocket API. Access is gated behind a waitlist, so this guide targets the live spot product.

That gives us a clean architecture:

  1. Detect the target wallet's spot trades by subscribing to its on-chain Transfer events over a WebSocket RPC.
  2. Replicate each trade through the Arcus spot flow with your own wallet.

The detection side is fully on-chain and provider-agnostic. The replication side goes through Arcus.

Arcus also runs an off-chain API (wss://api.arcus.xyz/v1/ws) whose public trades channel carries the takerAddress and makerAddress on every fill, which is the cleanest way to follow a wallet. That channel is best documented for perpetual markets, though, and perps are waitlisted, so spot coverage there is worth confirming against the live docs. For a bot that works on the live spot product today, on-chain detection is the reliable path, and it does not depend on any single venue's API. That is what this guide builds.

Arcus copy trading flow in two bands: detect on-chain (target wallet, Dwellir WSS, pair USDG and stock legs per tx, reconstruct buy/sell) then replicate via Arcus spot RFQ.

Arcus copy trading, two bands:

DETECT (on-chain, works today, no Arcus account needed): Target wallet -> Dwellir WSS logs -> Pair USDG + stock legs per tx -> BUY / SELL reconstructed

then mirror the trade ->

REPLICATE (requires an eligible Arcus account; US / CA / UK excluded): Size vs your capital -> Arcus spot RFQ (EIP-712 + Permit2) -> Atomic on-chain settlement

Prerequisites

  • Python 3.10+ and pip install websockets web3 aiohttp
  • A Robinhood Chain archive WebSocket endpoint. Sign up for Dwellir to get one.
  • A target wallet address to follow.
  • For replication: an eligible, funded Arcus account, and the live Arcus API reference at docs.arcus.xyz.

The token contract addresses you need are published on the Robinhood Chain contracts page. USDG (the quote asset) is at 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, and each Stock Token (AAPL, NVDA, TSLA, and the rest) has its own ERC-20 address there.

Part 1: Detect the Target Wallet's Trades

A spot trade on Arcus shows up on-chain as a single transaction that moves two tokens for the trader: a USDG leg and a Stock Token leg. If the wallet sends USDG and receives NVDA in one transaction, that is a buy. The reverse is a sell. So detection has two parts: subscribe to the relevant Transfer events, then group the legs by transaction hash to reconstruct each trade.

Subscribe to the target's transfers

Open a WebSocket to Dwellir and eth_subscribe to logs for USDG and the Stock Tokens you track. The Transfer event indexes both from and to, so you can filter server-side for your target on the receiving side, and run a second subscription for the sending side.

Python
import asyncio
import json
import os
import websockets
from web3 import Web3

WSS_URL = os.environ["ROBINHOOD_WSS_URL"]
TARGET = Web3.to_checksum_address(os.environ["TARGET_WALLET"])

TRANSFER_TOPIC = Web3.keccak(text="Transfer(address,address,uint256)").hex()
USDG = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"

# Stock Tokens to follow (addresses from docs.robinhood.com/chain/contracts)
STOCK_TOKENS = {
    "0x...": {"symbol": "NVDA", "decimals": 18},
    "0x...": {"symbol": "AAPL", "decimals": 18},
    "0x...": {"symbol": "TSLA", "decimals": 18},
}
WATCHED = {USDG.lower(): {"symbol": "USDG", "decimals": 6}}  # USDG uses 6 decimals; Stock Tokens use 18
WATCHED.update({a.lower(): m for a, m in STOCK_TOKENS.items()})

def padded(addr: str) -> str:
    return "0x" + addr[2:].lower().rjust(64, "0")

async def subscribe_side(ws, sub_id, position):
    # position 1 = target is sender; position 2 = target is recipient
    topics = [TRANSFER_TOPIC, None, None]
    topics[position] = padded(TARGET)
    await ws.send(json.dumps({
        "jsonrpc": "2.0", "id": sub_id, "method": "eth_subscribe",
        "params": ["logs", {"address": list(WATCHED), "topics": topics}],
    }))

Filtering by the target address in the topics means the node only sends you logs that involve the wallet you follow. That keeps the stream small even on a busy chain.

Reconstruct trades from the legs

Each notification is one Transfer. Buffer legs by transaction hash, and when a transaction contains both a USDG leg and a Stock Token leg involving the target, you have a trade. The direction tells you buy or sell.

Python
pending = {}  # txHash -> list of legs

def add_leg(log):
    token = WATCHED[log["address"].lower()]
    sender = "0x" + log["topics"][1][-40:]
    recipient = "0x" + log["topics"][2][-40:]
    amount = int(log["data"], 16) / (10 ** token["decimals"])
    side = "out" if sender.lower() == TARGET.lower() else "in"
    pending.setdefault(log["transactionHash"], []).append(
        {"symbol": token["symbol"], "amount": amount, "side": side}
    )
    return log["transactionHash"]

def resolve_trade(tx_hash):
    legs = pending.get(tx_hash, [])
    usdg = next((l for l in legs if l["symbol"] == "USDG"), None)
    stock = next((l for l in legs if l["symbol"] != "USDG"), None)
    if not usdg or not stock:
        return None  # not a complete spot trade yet
    # A spot swap moves the two legs in opposite directions: USDG out + Stock Token in
    # is a buy, USDG in + Stock Token out is a sell. If both legs move the same way it is
    # not a swap (liquidity add/remove, a bundled transfer), so do not mirror it.
    if usdg["side"] == stock["side"]:
        return None
    action = "BUY" if stock["side"] == "in" else "SELL"
    return {
        "action": action,
        "symbol": stock["symbol"],
        "shares": stock["amount"],
        "usdg": usdg["amount"],
        "tx": tx_hash,
    }

The detection loop

Python
async def watch_target():
    async with websockets.connect(WSS_URL, max_size=None) as ws:
        await subscribe_side(ws, 1, 1)  # target as sender
        await subscribe_side(ws, 2, 2)  # target as recipient
        print(f"Following {TARGET} on Arcus spot...\n")

        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("method") != "eth_subscription":
                continue
            tx = add_leg(msg["params"]["result"])
            trade = resolve_trade(tx)
            if trade:
                pending.pop(tx, None)
                print(f"{trade['action']} {trade['shares']:.4f} {trade['symbol']} "
                      f"for {trade['usdg']:,.2f} USDG  (tx {tx[:12]})")
                await mirror_trade(trade)

At this point you have a live feed of the target's Arcus spot trades, reconstructed purely from on-chain data. This is the reliable half of the bot, and it works with any archive WebSocket endpoint.

Part 2: Mirror the Trade on Arcus

Detection is on-chain and standardized. Execution goes through Arcus, and here the details depend on Arcus's live API, so treat the wire format as something to confirm against the current docs rather than hard-coding.

Arcus spot execution uses an RFQ flow. Conceptually:

  1. Request a quote for the pair and size from the Arcus API.
  2. Build the order intent (which token, buy or sell, size, slippage bounds).
  3. Sign it. Spot RFQ orders are signed with EIP-712 typed data plus a Permit2 PermitWitnessTransferFrom that authorizes the token movement. This is a wallet signature per trade, so your bot holds a signing key, not a custodial API secret.
  4. Submit the signed order. Arcus combines the best maker quotes and settles the trade atomically on-chain.

A realistic skeleton looks like this, with the Arcus endpoints and payload pulled from the live API reference:

Python
import aiohttp

ARCUS_API = "https://api.arcus.xyz"  # confirm current base + paths in the docs

async def mirror_trade(trade):
    # 1. Size the mirror relative to your own capital, do not copy raw size blindly
    mirror = size_position(trade)
    if mirror is None:
        return

    async with aiohttp.ClientSession() as session:
        # 2. Request a quote (endpoint path per docs.arcus.xyz/api-reference)
        quote = await request_quote(session, mirror)
        # 3. Build + EIP-712/Permit2 sign the RFQ order with your wallet key
        signed = sign_rfq_order(quote, mirror)
        # 4. Submit the signed order for atomic on-chain settlement
        result = await submit_order(session, signed)
        print(f"  mirrored: {mirror['action']} {mirror['symbol']} -> {result}")

Because there is no official Arcus SDK yet, your bot calls the REST and WebSocket API directly and constructs the EIP-712 and Permit2 signatures itself using web3 and eth_account. The Arcus docs publish the OpenAPI spec with the exact endpoints and schemas.

An alternative that avoids the Arcus API entirely: since spot settles through on-chain AMM pools, you can execute a direct swap through the underlying router contract with a standard ERC-20 approve plus swap. That keeps the whole bot on-chain, at the cost of not using Arcus's RFQ price improvement.

Risk Controls and Production Notes

Copying trades blindly is how you lose money. Real controls matter more than the plumbing.

  • Size relative to your capital. A whale moving 10,000 shares should not translate to your bot trying to buy 10,000 shares. Scale by a fixed ratio or a fixed dollar cap in size_position.
  • Handle corporate actions. Stock Tokens scale balances with a UI multiplier, emitted as UIMultiplierUpdated, and expose both raw and adjusted amounts via TransferWithScaledUI. If you mirror share counts, track the multiplier so a split does not corrupt your sizing.
  • Guard against reorgs. Key each detected trade on (transactionHash, logIndex) and wait a few confirmations before mirroring, so a reorged trade does not trigger a real order.
  • Add latency and slippage limits. If the target's price has moved too far by the time you execute, skip the trade rather than chase it.
  • Watch your RPC cost. Reconnect logic plus historical backfill leans on eth_getLogs. Dwellir's flat per-request pricing keeps a busy bot predictable instead of racking up compute-unit charges.

FAQ

Can I copy trade Arcus perpetuals? Not yet through a public path. Perps are behind a waitlist. When they open, the Arcus public trades WebSocket channel exposes maker and taker addresses per fill, which makes following a wallet straightforward. Until then, target the live spot product.

Do I need the Arcus API to detect trades? No. Detection is entirely on-chain. You only need Arcus for the execution half. That is why the detector works with just a Robinhood Chain WebSocket endpoint.

Why pair USDG and Stock Token legs? A single Transfer only proves tokens moved, not that a trade happened. A spot trade pairs a USDG leg with a Stock Token leg in the same transaction, so matching them by transaction hash is what confirms a real buy or sell.

Is there an official SDK? Not at launch. Your bot calls the REST and WebSocket API directly and builds the EIP-712 and Permit2 signatures itself.

Getting Started

The detection engine is the part you can build and test today, from anywhere, with public data. You need one thing to run it: an archive WebSocket endpoint for Robinhood Chain. Sign up for Dwellir to get archive and WebSocket access on flat pricing, or contact the team for dedicated nodes if you are running a latency-sensitive strategy. Then wire up the Arcus spot flow against the live API reference once your eligible account is ready.


This content is for educational purposes only and does not constitute financial, legal, or investment advice. Trading involves significant risk, and copy trading can amplify losses.