One Market
One million self-driving traders. One shared market. Open to everyone.
One Market is a real synthetic financial market which contains one million persistent autonomous agents.
They carry out purchases and sales, react to changes in the market, respond to news, keep record of their profits and losses, reach their risk limits, and continue in their current state over time. Humans have the possibility of accessing the same public deployment, using a synthetic bankroll, and trading directly against that market.
There is not a separate simulation server concealed behind the database.
The market is contained within a Rust module that has been deployed to SpacetimeDB on Maincloud, the database serving as the official state, the execution engine, and the synchronization layer for all connected clients.
In the configuration we use, the market operates on a 4 Hz shared clock, carrying out approximately 50,000 persistent agents per tick—roughly 200,000 agent updates per second—among a population of one million traders.
Inspiration
We started with a simple question:
What is the maximum size that a truly shared and persistent simulated world can reach when the database also functions as the application runtime?
The markets provided a particularly severe method of testing that idea.
The market is stateful, adversarial, and highly interconnected since any participant can influence every other participant via a single clearing price. There is a need for orders to have counterparties, for balances to stay valid, for updates to be atomic, and for clients to perceive the same world.
If the scale was to have any real meaning at all, the agents could not merely be generated as numbers on a dashboard; each one would have to have persistent state and have to take part continuously in the simulation.
Therefore we continued to increase the population.
The number increased from thousands to tens of thousands.
The number went from tens of thousands to hundreds of thousands.
In the end, we had one million autonomous traders operating on the single shared deployment.
How we built it
The frontend uses React, TypeScript, and Vite and is deployed via Vercel.
The authoritative backend is a Rust module that has been compiled to WebAssembly and which is deployed within SpacetimeDB on Maincloud. Browsers call the reducers and obtain the committed state changes via WebSocket subscriptions, while the generated TypeScript bindings ensure that the client remains in sync with the Rust schema.
A clock for one million actors
The main problem was to arrange the schedule.
Since it is not possible to update all the agents at the same time, the simulation makes use of a shared tick/epoch model.
The population is split up into 20 indexed buckets and, on each tick, one of these buckets becomes due; with one million agents, about 50,000 agents are processed per tick.
The system thus carries out about 200,000 persistent agent updates each second when operating at a production cadence of 4 Hz and spreading the entire population over the longer epoch.
Each actor stores its own:
- cash and inventory
- strategy weights
- risk state
- peak equity and drawdown state
- cooldown / liquidation state
- lifetime trading statistics
The agent's policies decide independently whether to BUY, SELL, or PASS by taking into account momentum, mean reversion, contrarian pressure, news, and deterministic seeded noise.
Even the decisions regarding passing and cooldowns are permanent updates. The figure of one million agents is therefore not merely a decorative population counter since the agents actually exist in the database and move forward through the simulation.
One auction, one price
Trading takes place using a uniform-price batch auction.
In every tick both autonomous and human orders are entered into the same auction; the engine selects the clearing price which maximizes the volume that can be executed and then distributes the fills by using price priority with deterministic tie-breaking.
The clearing algorithm runs in approximately:
$$ O(K \log K) $$
for (K) active orders.
In each trade that has been carried out there is an actual buyer and a seller; we don't create liquidity or alter the displayed price independently of the execution.
The market will just retain the previous price if the orders don't match.
The financial system uses integers expressed in cents and includes checks on arithmetic operations; when an order is accepted, reserves are set aside for human cash and shares, and settlement, the release of reserves, the actor's state, and the tick evidence are all committed atomically.
Humans trade in the same world
The deployment can be opened by anyone and they will be able to see the same market.
A user is able to use a synthetic bankroll and place limit orders that take part in the same auctions as autonomous actors.
Accounts are based on identity, whereas private views show only the balances of the connected user, together with their outstanding order and recent fills.
Hence, various browsers are not carrying out separate simulations; instead they provide access to one authoritative world.
CHAOS
We also wanted to disturb the system without having to predefine the result.
CHAOS introduces a news signal that affects the entire market—for instance, a serious negative event—but does not directly change the prices.
Each agent receives the signal via its individual policy and then decides all by itself how to respond.
Any crash, recovery, liquidity event, or failure cascade is caused by the resulting orders and by the same auction mechanism that is used during normal trading.
Agents may also reach their drawdown limits, sell off their positions, enter cooldown periods, and later obtain recapitalization or distress-revival grants that are specifically accounted for.
The scaling challenge
The most difficult aspect of the project was getting the first market to work.
The design was intended to be able to survive with one million persistent actors.
The first of our major performance tests was carried out on a machine equipped with a Ryzen 9 9950X and we were able to qualify 325,000 persistent actors at a rate of 20 Hz, this being true for both NORMAL and CHAOS runs with no scheduling slots being skipped.
Then we deployed.
The level of production was not the same as that of local performance.
The CPU budget available in the Maincloud environment was materially less than that of our development machine, and therefore it was not possible for it to maintain our original target of 5 Hz under the one-million-agent workload.
That caused us to cease considering the clock rate as a constant.
We discovered that the simulation has a useful scaling relationship in that when the shared clock is stretched, the amount of work that can be carried out per cycle increases, enabling a bigger persistent population to be included within the available compute budget.
For the last deployment we switched to a 4 Hz market clock and then kept on optimising the workload until the million-agent population could run within it.
That process led to changes across almost every layer of the system:
- indexes and lookup paths
- transaction structure
- table schemas
- primary and secondary keys
- actor scheduling
- order handling
- serialization size
- client subscription boundaries
- benchmark instrumentation
A major improvement was achieved through the use of actor storage.
The number of bytes required to represent the common serialized actor was reduced from 132 to 74, with values being automatically promoted to a wider representation when the compact form's limits are reached. As a result, we were able to decrease both the demand on the database and on memory without compromising financial correctness or giving up the need for per-agent persistence.
Eventually, the project no longer seemed to be 'a market with lots of bots'.
It had turned into a task involving transaction budgeting, scheduling, data layout, and distributed application design.
What we learned
The most important lesson was that it is not a single optimization to scale a stateful simulation.
It is a budget.
Each time there is a row lookup, an index access, a write operation, an encoded byte is processed, a memory allocation, a subscription is set up, or a transaction is carried out, it has to compete for a portion of the same limited number of cycles.
We also discovered that local benchmarks only have significance when they accurately describe the environment and the workload in question. A figure obtained on a high-end desktop CPU cannot simply be taken as being valid without any changes in a production setting.
Which is why our benchmark results are separated by workload and cadence rather than putting them all into a single headline figure.
We also encountered synchronization problems outside of the market engine. Having reproduced a WebSocket message-ordering issue related to compression in our pinned SDK, we introduced an ordered adapter which processes the incoming frames one at a time and stops stale client caches from occurring.
The frontend gave us the contrary scaling lesson to the backend: a user looking at one million agents need not download all one million agents.
Instead, clients subscribe to bounded price histories, activity feeds, news, samples of representative actors, and aggregated benchmark data. The rates are calculated using the cumulative server counters over the actual elapsed time rather than being deduced from the render frequency.
What we're proud of
The number is obviously fun:
1,000,000 autonomous agents.
The aspect for which we are most proud is the meaning of that number.
They aren't made up of a million particles sent by the client and they aren't a precomputed dataset.
They regularly take part in a common market, are supported by a single SpacetimeDB deployment which is authoritative, and constantly alter their state by placing orders that interact both with other autonomous actors and with real users.
It is possible to access One Market from a different computer and in that case you are not beginning a new simulation.
You'll be entering the same group.
Built With
- css
- docker
- javascript
- maincloud
- node.js
- react
- rust
- spacetimedb
- typescript
- vite
Log in or sign up for Devpost to join the conversation.