It goes without saying that the 3080 launch faced quite a big stir with an apparent high demand, low stock, botnet powered purchases and scalpers.

All major retailers, board partners and Nvidia itself came forward acknowledging the low launch stock vs the high demand so I was courious about the actual numbers.

During the first few launch minutes I was lurking the twitter account of the retailer I ordered my card from where everyone was spamming their order numbers asking for their order’s status. There you can notice the nomenclature of order numbers 0091714523877V:

The order counter seeming sequential (modulo 1000), we can easily infer the total number of orders and maybe even available stock using the estimator from the German Tank Problem.

First we need to get a hold of a bunch of order numbers

import requests
import re
import math
bearer_token = '[Redacted]'
p = re.compile('00917(\d{8}[A-Z])')
commandes = []
next_token = None
all_data = open('tweets', 'w')
for i in range(100):
    
    res = requests.get(
        'https://api.twitter.com/2/tweets/search/recent',
        {'query': 'to:TopAchat OR from:TopAchat', 'max_results': '100', 'next_token': next_token},
        headers={'Authorization': f'Bearer {bearer_token}'}
    )
    
    for t in res.json()['data']:
        all_data.write(t['id'] + ' : ' + t['text'] + '\n')
    
    commandes += [ p.findall(tweet['text']) for tweet in res.json()['data'] if p.search(tweet['text']) ]
    
    if 'next_token' not in res.json()['meta']:
        break
    
    next_token = res.json()['meta']['next_token']

We can study the distribution of orders in respect to board partners but I’m more interested in the raw total numbers for the time being

samples = [(int(i[:-1]), i[-1]) for l in commandes for i in l]
cards = set(i[1] for i in samples)

orders = {
    k: []
    for k in cards
}

for sample in samples:
    orders[sample[1]].append(sample[0])
for k in orders.keys():
    print(k, ':', len(orders[k]))
R : 17
V : 112
C : 33
J : 72

Just an estimation

Chances are we are most likely bundling up other unrelated orders in here. It’s fine we are not interested in getting the exact numbers, just a rough estimate. So I compute two “lower and upper bounds” which are more like “lower and upper estimates” based on the order times:

obs_inf = [int(str(num)[4:-1]) for num in sorted(set([int(i[:-1]) for l in commandes for i in l])) if 1430 < int(str(num)[:4]) < 1530]
obs_inf = obs_inf[:obs_inf.index(12)] + [i + 1000 for i in obs_inf[obs_inf.index(12):]]
obs_inf = [i - obs_inf[0] + 1 for i in obs_inf]
obs_sup = [int(str(num)[4:-1]) for num in sorted(set([int(i[:-1]) for l in commandes for i in l])) if 1400 < int(str(num)[:4]) < 1530]
obs_sup = obs_sup[:obs_sup.index(12)] + [i + 1000 for i in obs_sup[obs_sup.index(12):]]
obs_sup = [i - obs_sup[0] + 1 for i in obs_sup]
k = len(obs_inf)
m = max(obs_inf)

μ = (m - 1) * (k - 1) / (k - 2)
σ = math.sqrt((k - 1) * (m - 1) * (m - k + 1) / (k - 3) / (k - 2) ** 2)

print(f"borne inf N ≈ μ ± σ = {int(μ)} ± {int(σ)}")
borne inf N ≈ μ ± σ = 1605 ± 7
k = len(obs_sup)
m = max(obs_sup)

μ = (m - 1) * (k - 1) / (k - 2)
σ = math.sqrt((k - 1) * (m - 1) * (m - k + 1) / (k - 3) / (k - 2) ** 2)

print(f"borne sup N ≈ μ ± σ = {int(μ)} ± {int(σ)}")
borne sup N ≈ μ ± σ = 1645 ± 7

Total orders at launch hour: 1645 ± 7

So we have roughly 1645 ± 7 orders if we start couting orders comming in from 14:30 up to 15:30 (the site went down well before this timestamp)

Looking for the total number of available cards in stock

Searching through the twitter comments we find that order 0091714523877V was the last in line confirmed for shipping with the three next orders being marked as waiting awaiting replenishment and the previous two being of unknown status (most likely also shipped but at the time the retailer’s systems where overwhelmed by orders coming in): https://twitter.com/TopAchat/status/1306611398047412225

The retailer describes its stock as being considerably small for the current demand:

“Oui certaines sont en préparation mais comme je le dis ici nous avions tres tres peu de stock pour le nombre de commande” - @TopAchat

Let’s roughly estimate this number:

obs1_inf = obs_inf[:sorted(set([int(i[:-1]) for l in commandes for i in l])).index(14523877) - 1 + 1]
obs1_inf
[1, 7, 8, 12, 13, 17, 21, 25, 31, 33, 48, 49, 50, 51, 53, 55, 63, 68]
k = len(obs1_inf)
m = max(obs1_inf)

μ = (m - 1) * (k - 1) / (k - 2)
σ = math.sqrt((k - 1) * (m - 1) * (m - k + 1) / (k - 3) / (k - 2) ** 2)

print(f"borne inf N ≈ μ ± σ = {int(μ)} ± {int(σ)}")
borne inf N ≈ μ ± σ = 71 ± 3
obs1_sup = obs_sup[:sorted(set([int(i[:-1]) for l in commandes for i in l])).index(14523877) - 1 + 1]

k = len(obs1_sup)
m = max(obs1_sup)

μ = (m - 1) * (k - 1) / (k - 2)
σ = math.sqrt((k - 1) * (m - 1) * (m - k + 1) / (k - 3) / (k - 2) ** 2)

print(f"borne sup N ≈ μ ± σ = {int(μ)} ± {int(σ)}")
borne sup N ≈ μ ± σ = 108 ± 6

Total available inventory at launch day: 108 ± 6 (upper bound)

Given how this is supposed to be quite a big upper bound for the estimate I find it surprisingly tiny for the scale of such a launch.

However it is in line with all comments of stocks being considerably limited at launch. Demand was at least 15 times higher than offer.

And according to an anonymous source the retailer group has been allocated 70% of total retail launch volume of cards (https://overclocking.com/rtx-3080-rupture-vraiment/) which means that:

France was allocated roughly an upper bound of only 183 cards total at launch which is quite surprising in my opinion