I wanted to see what else would move.
A prediction market gives you a price for one outcome. I was interested in the other markets around it. If one outcome suddenly becomes certain, which other questions would you want to look at?
I built Polyverse to explore that. You pick a market, choose an outcome, and get a graph of proposed connections. You can follow a branch, inspect the reasoning, and see how the markets have moved together in the past.
The hard part is deciding which connections deserve to appear. A model can come up with a convincing explanation for almost any pair of events. I wanted each proposed link to go through a price history check before it reached the graph.
Finding a useful set of markets
Polymarket has a lot of questions, and most have nothing to do with the outcome you selected. Sending the entire catalog to a model would make it harder to find a useful relationship.
I first narrow the candidates to markets with at least $100,000 in volume. A Llama 3.3 model running on Groq extracts names and concepts from the trigger question. The code matches those terms against market questions and categories using word boundaries, so a term like “AI” doesn’t match the middle of an unrelated word.
The candidates are sorted by volume, and the top 100 go into a second request. This time, GPT OSS 120B proposes a few relationships and returns structured JSON with the market ID, an explanation, direction, strength, and expected lag. The engine resolves those IDs against the market catalog before doing anything with them.
There are fallbacks for both steps. If keyword extraction fails, I use names and longer words from the question. If nothing matches, I use the largest markets by volume. That keeps the request running, but it can also give the model a less relevant set to work with.
Checking the price history
Market metadata and market history come from different APIs. Gamma gives me the questions, outcomes, prices, and token IDs. I use the CLOB token IDs to request a week of historical prices at hourly fidelity.
Those histories don’t necessarily have matching timestamps. I sort both series and walk through them with two pointers. Points within an hour of each other become a pair. Otherwise, the pointer with the earlier timestamp advances until the series line up again.
Once I have at least ten pairs, I calculate Pearson correlation on the price levels. A flat series returns zero because there’s no variation to compare. A candidate needs an absolute correlation of at least 0.20 and a model strength score of at least 0.5 to pass. Candidates without enough history are excluded from the final graph.
This gave me a concrete filter for the model’s suggestions. It still leaves plenty of uncertainty. Two markets can move together because they react to the same news, and ten observations are a small sample. Passing this check doesn’t establish that one event causes the other.
Turning a connection into an estimate
The selected outcome is assumed to happen, so its probability becomes 100%. The difference from its starting probability is the shock. An outcome already priced at 90% produces a much smaller shock than one priced at 10%.
The prototype estimates a related market’s movement as the absolute correlation multiplied by that shock, then multiplied by 0.5. The model chooses whether the movement is positive or negative. I add that change to the market’s current probability and clamp the result between 1% and 99%.
The factor of 0.5 is a fixed scaling choice. It hasn’t been fitted to observed outcomes, and the result isn’t a calibrated forecast. The interaction below shows exactly how that calculation behaves.
0.60 × (1 − 0.40) × 0.5
Building out the graph
I process markets through a queue. The trigger can add up to three markets. Each of those can add up to two more, and the next layer can add one per parent. A set of processed market IDs prevents revisiting the same market and keeps the graph from looping back on itself.
The search stops after three layers. Each accepted market becomes a node, and each proposed relationship becomes an edge with its explanation and correlation attached. React Flow handles the interaction, while Dagre lays the graph out from top to bottom.
There’s an important detail in this version: later layers still compare their price histories with the original trigger. The parent market helps discover more candidates, but the engine doesn’t recursively pass each parent’s estimated price change down to its children. That’s something I’d change before treating the branches as a model of how an effect spreads.
For storage, I keep the market catalog in Supabase and save simulations in the browser’s localStorage. Opening the app requests a catalog sync, which is skipped if the previous sync was less than an hour ago. That keeps browsing separate from the work of fetching and normalizing the catalog.
The prototype in use
Original recording / 1 minuteChoosing an outcome, exploring the resulting graph, and reading the explanation for a connection.
What I’d change next
I’d start with outcome handling. The history lookup currently takes the first token in a market, even when you select a different outcome. I’d map the selected outcome to its actual token before doing the comparison.
I’d also make direction consistent throughout the interface. Right now, the node’s estimate follows the model’s direction, while the edge metadata follows the sign of the correlation. When those disagree, the graph should make the disagreement explicit.
Then I’d evaluate the estimates on held out historical events, using only the information available before each event. That would let me test the thresholds and scaling factor against what happened afterward.