The XYM airdrop experiment

As you may remember (and I reported on many times) @tsubasa_yozawa started a valiant quest back in December of 2021 which aimed to airdrop a small amount of XYM to new users. The aim was to grow the Symbol community and recruit new residents of XYM City. He has a big reach on social media and I think(?) that the offer was oversubscribed meaning that a lottery-style allocation method was used. The entrants just tweeted their Symbol addresses and 5,000 accounts were credited with 20 XYM each (100,000 in total distributed).

This experiment really interested me. I wondered what the new users would do. Would they send it straight to an exchange and cash in to buy a coffee, would they experiment and maybe try sending some small transactions to see how it worked or would they get the Symbol bug and start accumulating XYM? I thought I would go back a couple of weeks after the distribution had finished and see what had happened. I maybe should have waited a month or two but as I have a little time now I thought I would investigate, maybe I will come back to it in six months and see if anything has changed.

The distribution

All of the funds were sent from this account and as far as I can see a total of 5,201 transactions were sent in total. I thought that it should only be 5,000 but perhaps the total budget exceeded 100,000 or there were some additional test transactions sent from the account.

4,800 transactions were to newly created accounts

401 transactions were sent to accounts that had received transactions before – so they cheated 😡

195 new accounts and 21 cheating accounts received more than one 20 XYM payout, whether they tried to game the system or whether it was a distribution issue I can’t say.

⚠️ Warning ⚠️

If you don’t like to look at horrible code then please skip the next section 😂 I have already admitted that quick and dirty is my philosophy.

Also for those of you that do read it apologies for any mistakes, I think I think that the numbers should be correct but as always this is not rigorously tested!

The code 😳

OK, here we go…

import urllib.request
import json

# Set the node address that you will query
NODEURL = "http://xymharvesting.net:3000"

Get all transactions sent from the address performing the airdrop (NDEYAGFR4IEC3TZ55SVJ7D6DTMH5VV2DVUP6DBI) using the signer’s public key.

i = 1
total = 0
output = {}

url = NODEURL + '/transactions/confirmed'
params = {
    'signerPublicKey': '10B27BD9725857CC6B8CC4322F689D34E20E4CF24AA394E44B48C255FDE2BEC1',
    'order': 'desc',
    'pageNumber' : i,
    'pageSize' : 100
}

req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
with urllib.request.urlopen(req) as res:
    data = json.load(res)
#need to loop through pages
txs=[]
while data.get("data"):
    for d in data['data']:
        if (d['meta']['hash']):
            txs.append(d['meta']['hash'])
        else:
            continue
        
    i=i+1
    params = {
    'signerPublicKey': '10B27BD9725857CC6B8CC4322F689D34E20E4CF24AA394E44B48C255FDE2BEC1',
    'order': 'desc',
    'pageNumber' : i,
    'pageSize' : 100
    }
    req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
    with urllib.request.urlopen(req) as res:
        data = json.load(res)

Next get all of the recipient account addresses from the transaction IDs.

# now get recipient account addresses

addresses=[]
def hexToAddress(hexKey):
    return base64.b32encode(bytes.fromhex(hexKey)).decode("utf-8").replace("=","")



for t in txs:
    url = NODEURL + '/transactions/confirmed/' + t
    print (url)
    req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
        if "transactions" in data['transaction']:
            for d in data['transaction']['transactions']:
                print(hexToAddress(d['transaction']['recipientAddress']))
                addresses.append(hexToAddress(d['transaction']['recipientAddress']))
        else:
            addresses.append(hexToAddress(d['transaction']['recipientAddress']))

I want to separate the recipients into new accounts and those accounts that had received XYM before the airdrop was announced (the cheaters 😆).

#See if it was new account

new = []
old = []

for a in addresses:
    url = NODEURL + '/transactions/confirmed?address=' + a
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
        # Sorted by timestamp so first tx is oldest - if new account 1st tx should be from airdrop
        if data['data'][0]['transaction']['signerPublicKey'] == '10B27BD9725857CC6B8CC4322F689D34E20E4CF24AA394E44B48C255FDE2BEC1':
            print(a + "\t" + "NEW")
            new.append(a)
        else:
            print(a + "\t" + "OLD")
            old.append(a)

Now we have two lists, one for the new accounts and one for the pre-existing accounts that received the airdrop funds. Next, I want to retrieve the account balances for all of the new accounts. This will tell us whether the new users had sold, held or accumulated more XYM.

params = {
'order': 'desc',
}

f = open("new.txt", "w")

for a in new:
    url = NODEURL + '/accounts/' + a
    req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
        #print(a)
        addressHeight = data['account']['addressHeight']
        if data['account']['mosaics']:
            balance = int(data['account']['mosaics'][0]['amount']) / 1000000
            print(a + "\t" + str(balance) + "\t" + addressHeight)
            f.write(a + "\t" + str(balance) + "\t" + addressHeight + "\n")
        else:
            print(a + "\t" +'0' + "\t" + addressHeight)
            f.write(a + "\t" +'0' + "\t" + addressHeight + "\n")
f.close()

Just for the hell of it, let’s look at the cheater accounts too 😅

# Old addresses

params = {
'order': 'desc',
}

o = open("old.txt", "w")


for a in old:
    url = NODEURL + '/accounts/' + a
    req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
        #print(a)
        addressHeight = data['account']['addressHeight']
        if data['account']['mosaics']:
            balance = int(data['account']['mosaics'][0]['amount']) / 1000000
            
            print(a + "\t" + str(balance) + "\t" + addressHeight)
            o.write(a + "\t" + str(balance) + "\t" + addressHeight + "\n")
        else:
            print(a + "\t" + '0' + "\t" + addressHeight)
            o.write(a + "\t" +'0' + "\t" + addressHeight + "\n")
o.close()        

Find how many transactions are to new and old accounts (and just check that they add up to the total number of transactions).

len(old)
len(new)
len(addresses)

Next, find how many duplicate addresses are in the list of new accounts (these have been paid more than once).

freq = {} # stores the frequency of elements
counting = [freq.update({x: new.count(x)}) for x in new]
d = dict((k, v) for k, v in freq.items() if v > 1)
len(d)

And the same for the old accounts.

freq = {} # stores the frequency of elements
counting = [freq.update({x: old.count(x)}) for x in old]
d = dict((k, v) for k, v in freq.items() if v > 1)
len(d)

The results

So after running that monstrously ugly and inefficient code we get a list of addresses showing the balance and age of the account. I then realised that I had to clean up the data as I am going on the list of original distribution transactions and with duplicates in the list, I am counting some accounts twice. After cleanup, I get 4,566 new accounts. What do we see? Well, I will attempt a basic summary of the unique new account data only below.

Sum of balances404,530.87 XYM
Mean balance88.60 XYM
Median balance20.00 XYM
Highest balance121,857.06 XYM
Lowest balance0.00 XYM
Over 20 XYM263 accounts
20 XYM or below4,303 accounts

I was going to make some plots etc but when I came to do it (i.e. now) I realised that there is not a huge amount that I can show apart from the numbers above so I guess I will leave it there 😆

Conclusion

Well, it looks like (even in these early days after distribution) the project has had some success. There are 12 new harvesting eligible accounts that were started with the original 20 XYM airdrop and 25 accounts with at least 100 XYM. The overall balance of new Symbol accounts from the airdrop now exceeds 400,000 XYM which is four times the amount of XYM airdropped!

Of course, the vast majority of accounts have the original 20 XYM or have sold or transferred somewhere else. I hope that some of these new users will become interested in learning more about Symbol, will get involved with the community and maybe even start thinking about increasing their XYM stashes 😊

So, maybe I will pick this up again in a few months just to see if there are any major changes and include it in one of the updates. It was a fun exercise and now I have some code that I can just rerun at a later date. I also want to thank tsubasa_yozawa for setting the experiment up and to gimre who doubled the distribution budget.

Data

If you want to run this yourself (it’s slow) then you can get my Jupyter Notebook here. Maybe change the node to save mine getting hammered by multiple people making thousands of requests 😅

The output file for new accounts is here and for active accounts that claimed the airdrop here.

Thanks for reading!

Tags:
Avatar photo
NineLives
admin@symbolblog.com

I'm a Symbol and NEM enthusiast and run this blog to try to grow awareness of the platform in the English-speaking world. If you have any Symbol news you would like me to report on or you have an article that you would like to publish then please let me know!

No Comments

Sorry, the comment form is closed at this time.