Gaining an edge on friend.tech with basic programming

Gaining an edge on friend.tech with basic programming

Preamble

Friend.tech is the latest trend on crypto Twitter. It is a progressive web app where users may purchase "shares" of other users using ETH. Holders of a share are granted exclusive access to a chat with the user. Shares are priced using, I believe, an exponential bonding curve (think $SOCKS or $FUMO). Naturally, shares may be sold to yield PnL. Thus, it is easy to understand why being early constitutes a considerable advantage.


Table of contents:

  1. Introduction

  2. Snooping around

  3. Filtering out the fluff

  4. Creating the monitor

  5. Going further

  6. Closing thoughts


1 - Introduction

If you are reading this article, I'm sure you are familiar with the rapidly changing nature of crypto; one day you're punting thousands on an ERC20 with the ticker $BITCOIN, and the next you're gambling on hamsters racing. Despite the vapid, ephemeral nature of many crypto trends, a considerable amount of alpha can be found by playing into the hype. You were late to $PEPE? No worries, just be early to $UNIBOT. Whilst some "alpha" groups offer all-inclusive solutions to this end, there is a sizeable edge to be gained as a solo actor by having basic comp-sci skills. In this article, we will explore how this is possible using friend.tech as a case study.

Throughout this article, I assume a foundational level of familiarity with programming (e.g. I will not be explaining what the Chrome dev tools are).

2 - Snooping around

Our goal is to better understand how the app works. Is there a public API? Do we need authentication to interact with the API? What are some endpoints that may be of interest?

The first thing to note is that friend.tech is a PWA (progressive web app). It enforces the use of a mobile device, AND for the app to be added to your home screen.

friend.tech landing page

Friend.tech landing page on a desktop browser

Shucks! Looks like a dead-end already. Maybe we'll have to find a way to use the Chrome dev tools on mobile, or use an emulator? In actuality, bypassing this is fairly simple. The app uses your User Agent to determine whether you are on mobile or not. We could use the Chrome dev tools to change our default User-Agent to something like:

Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1

Or, we could just set our dimensions to something like S20 Ultra and reload the page.

friend.tech landing page with "add to home screen" modal

Ta-da! Our next problem is this annoying "Add To Home Screen" modal. Luckily for us, we can just delete the "Home_modalCustomContainer___{some number}" div.

snippet of friend.tech HTML code

The modal will appear again throughout the signup/login process. Simply repeat the above to remove it again.

Fantastic. Now we can go ahead and sign in/create an account. When you reach the home page, you can revert your browser dimensions to normal and reload the page.

friend.tech home page with desktop aspect ratio

Ok, now that we're in the application we can start working with the Chrome networking tools to figure out what's going on. Let's clear the activity, filter by Fetch/XHR and click on, say, the "Your Shares" tab.

chrome dev tools network tab

From this simple gesture, we've discovered 4 potentially interesting endpoints:

Cool! Let's keep going. Clicking on the "Search" icon, we get the following:

And typing something in the search bar pings this endpoint:

3 - Filtering out the fluff

Ok, we've got some stuff to work with now so let's see if any of it is useful.

First off, some requests are sending an auth header whereas others aren't. Importing the requests that are sending a token into Postman (copy as cURL in Chrome -> import into Postman) and disabling the auth header, the requests go through just fine. We can ignore auth altogether.

Next up, we don't care about the most active host, nor the top by price. Perhaps global activity may be useful but there's too much noise in there. That leaves us with recently-joined, search/users and users/{Wallet Here}. Whether we need the third one depends on what information we can get out of the other two, so let's look at those first.

Recently-joined

This endpoint takes a simple GET request and returns a list of "User" objects.

{
  "users": [
    {
      "id": 25262,
      "address": "0x2d725841ab7200764784ef2ee8ceec1ad99f0329",
      "twitterUsername": "0xnacci",
      "twitterName": "nacci² 🍫📔",
      "twitterPfpUrl": "https://pbs.twimg.com/profile_images/1662773036607586305/87WWi2Lx.jpg",
      "twitterUserId": "1417558307032768513",
      "lastOnline": 0,
      "displayPrice": "5062500000000000",
      "holderCount": 6,
      "shareSupply": 9
    }
  ]
}

One issue jumps out immediately: the array is sorted monotonically by "id", but "id" is not contiguous.

Search/users

This endpoint takes a GET request with the "username" query parameter. Similarly to the above, we get an array of "User" objects, albeit in a slightly different format:

{
    "users": [
        {
            "address": "0x3cc0e528a2f7164efe115dcc88ea7e719a67fb2a",
            "twitterUsername": "hekarusO",
            "twitterName": "Bruh Man🧙🏻‍♂️",
            "twitterPfpUrl": "https://pbs.twimg.com/profile_images/1519225920703578112/wZJ4ij4K.jpg",
            "twitterUserId": "1471833645262389249"
        },
        {
            "address": "0x456f45aa09cf6cc8491afd8baea4c05d6a671a10",
            "twitterUsername": "aswangcollect",
            "twitterName": "Bruh",
            "twitterPfpUrl": "https://pbs.twimg.com/profile_images/1682348151381053440/kf2uZArD.jpg",
            "twitterUserId": "1390627659546193921"
        },
        {
            "address": "0x13a365316572df57a18db8668e85996dbfa5b04f",
            "twitterUsername": "Bruuhman1337",
            "twitterName": "bruhman",
            "twitterPfpUrl": "https://pbs.twimg.com/profile_images/1655536410638548992/DuE1QWbl.jpg",
            "twitterUserId": "1336902680036454400"
        }
    ]
}

Query = bruh

In either case, it seems we have all the information we need without calling /users/{wallet}, so we can ignore it.

Great! Now that we know what our points of interest are, we can look into giving ourselves an edge.

4 - Creating the monitor

Now, onto some coding. The easiest way to take advantage of the information we've gathered is to make a program that pings us on Discord when a specific user signs up. All we need is a Discord channel with a webhook we can use to post messages. I won't be telling you how to code the monitor step-by-step; it's fairly straightforward. Here is a link to the Discord webhook documentation.

A high-level overview of your monitor may look something like this:

  • Load a list of monitored entities

For each entity in your list:

  • Ping the search/user endpoint

  • Look for exact Twitter name matches

  • If an exact match is found, post a message to Discord

  • Remove the monitored user from your list.

💡
Note that we're using the search/user endpoint because, quite frankly, we don't have time to investigate the issues mentioned prior. We can always build on this in future iterations.

Here is a snippet of what this could look like in Rust:

loop {
        for target in monitor_list.clone().iter() {

            println!("LOG: Beginning monitor for: {}", target);

            let monitor_target = &target.clone();

            let user_info: Result<KosettoResponse, StatusCode> = kosetto_client::get_user(&client, monitor_target)
                .await;

            match user_info {
                Ok(user_info) => {
                    let res = monitor(&user_info, monitor_target.clone(), &client, &webhook_url).await;
                    match res {
                        Ok(1) => {
                            <Vec<String> as AsMut<Vec<String>>>::as_mut(&mut monitor_list).retain(|x| x != monitor_target);
                        },
                        Ok(2) => {
                            println!("LOG: Exact match not found. Continuing...");
                        },
                        Err(e) => {
                            println!("ERROR: {}", e);
                        }
                        _ => {}
                    }
                },
                Err(e) => {
                    println!("ERROR: status code => {}", e);
                }
            }

            println!("LOG: Sleeping for 1 second...");
            thread::sleep(Duration::from_secs(1));

        }

        println!("LOG: Finished monitoring all targets. Sleeping for 10 seconds...");
        thread::sleep(Duration::from_secs(delay));
        println!("LOG: Beginning new monitoring cycle.");
        println!("--------------------------------");
    }

And that's it! Simple, right? Now you'll be amongst the first to know when people of interest sign up.

5 - Going Further

This implementation is very naive; perchance as naive as friend.tech itself. For instance, we are doing everything in memory. This is perfectly fine as the scale of our application does not warrant a DB. Nonetheless, here are some additional things to consider for a more robust implementation:

  • Are you getting rate-limited? If so, is increasing delays between monitoring cycles enough or should you be using proxies?

  • Are you getting cached? Can you mess with the headers to avoid caching?

  • Can you host your monitoring application on a server to have it running 24/7? Which provider should you use and does it even matter?

  • Can we monitor each user concurrently? If not all, why not concurrent batches?

Furthermore, expanding the scope of our application to automatically snipe interesting shares is not a huge amount of work. The contract's buyShares function does not require a signature, as shown below.

etherscan contract write page

It's also conceivable to add very basic inventory management and TP based on PnL; these should constitute no more than a few hours' worth of work.

💡
You could completely switch things up and make an airdrop farming bot using Pupeeter/Selenium! The world is your oyster.

6 - Closing Thoughts

As we reach the end of this article, some of you may be thinking "This is quite basic stuff..." - and you'd be right! It took me around 2 hours to code a monitor from scratch. Sure, it's not the cleanest code of all time; it's also far from the most optimal. Notwithstanding, there was a real demand for friend.tech monitors, to the point where people were paying for them. You don't need to spend money on stuff like this. You just need to be fairly familiar with a programming language. Of course, it's not always as easy as it was in this example. When it is, however, you should make the most of it.

That's all! Hopefully, you found this useful/interesting and please do leave feedback.