How We Built Our TCGX Index
DimaWe recently released TCGX, a perpetual that tracks a basket of trading cards. Building an index around these kinds of assets comes with many challenges, from deciding which cards to include to making sure one outlier doesn't swing the entire index.
This article covers how we decided which cards to include in our index, how much each card should affect the index, and how we prevent outlier prices from moving the index too much.
The Problem with Indexing Cards
As you can imagine, when building an index from a basket of cards, we have far less data to work with than an index like the S&P 500. There is no easy data on the supply of cards, and we don't have a continuously executable price for the whole basket. Cards can be valued very highly while hardly trading at all. Using supply to determine each card's weight won't work because we don't know the supply. Using price to determine weight would simply give higher-value cards more influence on the index, which isn't ideal for a tradeable index either.
Selecting Cards Based on Data
If we tracked one big card, our oracle would rely on a single price for that card. Including more cards helps with this problem, but only if each card provides a good input for our price. The more bad inputs we include, the more susceptible we become to manipulation.
So we decided to pick cards based on how good their data was. Each card in our index is a specific variation of that card, defined by set, printing, condition and language. We wanted cards whose data updated frequently and fluctuated often. We also looked at price spikes, both up and down. To make sure we don't overweight certain cards, we limited how many cards can come from the same set or series.
We ended up with 160 cards that fit our criteria: 80 from Magic: The Gathering, 48 from Pokémon, 16 from One Piece and 16 from Lorcana. This list will stay constant unless we change our method for choosing cards. If a card's price begins to fall, or it stops updating as frequently, we won't replace it.
When initially selecting cards, we filtered them by price range. However, a minimum price doesn't tell us how expensive it would be to move that card's price. Someone buying one copy of a card may not move the price we are looking at. Frequent price fluctuations show repricing activity, but don't establish how often a card trades. Again, that doesn't mean its price is executable. If a card drops below our initial price range, it will stay in our index.
How Did We Decide Magic Shouldn't Make Up 50% of the Index?
The number of cards we chose from each game shouldn't dictate how much weight that game has. If we weighted each card equally, Magic would take up 50% of the index simply because we chose the most cards from that game.
Using the recorded price to weight each card also presents problems. Let g represent a game and pᵢ(t) be the price we accept for each card in that game. We could then find the share of the total that each game's recorded prices make up:

This is that game's share of the total recorded prices. As stated before, we don't know the supply of these cards or how much they trade, so this would skew our index toward more expensive cards.
By allocating a set percentage to each game, we can control how much exposure our index has to each game. Pokémon makes up 45% of our index, Magic 42%, One Piece 11% and Lorcana 2%. These are the exposures we decided our index should have. Once we allocated each game its weight Wg, we divided it by ng, the number of cards in that game, to get the weight of each card:

So each Pokémon card has a weight of 0.9375%, each Magic card 0.525%, each One Piece card 0.6875% and each Lorcana card 0.125%. Adding more cards to a game would lower each card's weight within that game. Dividing evenly doesn't make the cards independent, though. Similar cards can still move together, and many bad inputs at once can skew our index.

Scaling All Prices Equally
By taking the log of each card's price relative to an anchor price, we put all of our cards on the same playing field:

aᵢ is the anchor price we choose for each card. This keeps our index from being skewed by cards with higher nominal values. If a card goes up 10% today, its log ratio changes by the same amount whether it costs $10 or $1,000. Its effect on the index also depends on its weight and distance from the center.
Finding the Center of Our Prices
We now need to find one value from our 160 cards, all scaled the same way. There are many ways to do this. If we take a simple average of all the values, one bad input could affect our index drastically. Taking the median helps with bad inputs, but it may not capture every real price movement. Smoothing the prices dampens bad prices, but it also slows down the index's movement.
In previous versions of our formula, we capped how much each card's accepted price could move. This made our index path dependent: if a card hit its cap a couple of weeks ago, it would still affect our index today. So we decided not to cap how much the accepted price can move. Because we use only our accepted prices and anchor prices, our index will always be the same given the same accepted prices, anchors and weights.
What we came up with is a Huber estimator. It finds the center θ of our inputs, but once an input is a certain distance away from θ, it clips that input's distance from the center before applying its weight:

Where x = zᵢ − θ. We can then solve for θ:

All cards that fall within the threshold count with their full weighted distance. Those beyond the threshold count at most 0.8 × their weight, in either direction. The 0.8 threshold is measured in log distance from the center. If no cards are clipped, θ is simply the weighted mean of the logs.

Centering on the Index
Our index value becomes:

Where I₀ is a fixed number we choose to index from. It is not equal to the price of a basket of cards or the combined market cap of those cards.
By clipping these contributions, we limit the influence of any one big price move. But if all cards went up 5%, all of the log ratios would go up by ln(1.05), and so would our center. All of the residuals would stay the same, so our index would also go up 5%, regardless of how far each card is from its anchor price. The downside is that if only a few cards have a big price move, they may be clipped, even if the movement is valid.

A 10x Price Increase
Let's look at an example. Assume we have 160 cards, all with equal weight and all at their anchor prices. Now we move one of those prices to 10x its anchor. A simple log average would increase our index by about 1.45%. But since we are using a Huber estimator, that card will be clipped, and we end up with:

Which gives I/I₀ = e^(0.8/159), an increase of about 0.50%.

Using our current weights and prices from September 4, the largest single-card 10x price move we tested increased our index by 1.12%. This result applies to that snapshot; it isn't a universal bound or an estimate of manipulation cost.

Of course, if multiple cards moved at once, that would be difficult to distinguish from normal buying pressure. Someone could try to attack multiple prices at once, or many prices could be invalid at the same time. We have no way of telling these apart from the prices alone.
Low Price Action
Cards that are repriced less frequently will affect the index less frequently, but not necessarily by smaller amounts. The median Magic card in our basket had 76 price changes across 89 day-to-day movements. The median Pokémon card had 24 price changes over the same period. These counts measure how often recorded prices changed. Our basket was also chosen with hindsight, so our historical data carries hindsight-selection bias.
Even though our candles are set to 1 second, there can be long periods where no price updates. We will not artificially generate price movement.
We will monitor freshness using the weights of the constituents, as well as coverage by game. For example, if 10 Pokémon variants became stale, they would account for 9.375% of the index weight, while 10 Lorcana variants would account for 1.25%. One metric may not cover every case.
If we do not have fresh data, we will not reweight our basket. Our last accepted prices will stay in place and continue to age. If prices are invalid, we will stop accepting them. Failed freshness checks may immediately affect the ability to trade, and we may pause or limit trading if prices become unreliable. We will communicate with our traders if we make major changes to our basket or index.
What's Next
This is our first Discovery Market, and we're excited for people to start trading it. We'll continue to manage risk as the market grows and may update the index over time. As we see more activity, we plan to increase the OI cap and tighten spreads. Discovery Markets is only getting started, and we have a lot more exciting listings ahead. Stay tuned!
More articles

Introducing Discovery Markets: New Markets. Only on N1.
Introducing Discovery Markets, new lineup of markets you can't trade anywhere else, giving traders access to high-growth assets and sectors, starting with the TCGX trading-card index.

Introducing TCGX: The first trading-card index market
Introducing TCGX, the first trading-card index market, and Discovery Markets—N1’s new category bringing tradable markets to assets and sectors underserved elsewhere.

N1 Prime
We’re introducing N1 Prime, our answer to the question we’ve focused on since launching the exchange: what will make N1 the leading place to trade?