Duolingo Automatic Streak Save

Have you heard of the Seinfeld Strategy? It’s the philosophy of acquiring a skill/habit by making sure that you practice a little bit every day. It doesn’t matter what is happening in the world or how shitty you’re feeling, you make a promise to yourself that you will spend at least some time on a skill of your choosing. You don’t break the streak! As the streak gets longer and longer, you have more of a tendency to keep doing it every day, and getting better each day.

I’ve been following this technique for learning languages the last few years. Coming to my help in implementing this is the amazing (free!) app Duolingo which enables gamification by showing how long you have had a continuous streak of daily usage in the platform. When you have a long streak, you have even more of a bonus to keep going!

Example of a 50 day long streak

However, one potential problem with streaks is that try as you might, life gets in the way, and you invariably end up forgetting/not being able to do what you set out to do daily. If When that happens, if your entire streak is lost, then you end up with negative motivation to continue since all your effort of keeping the long streak has gone down the drain.

However, duolingo offers a really smart alternative to make sure this doesn’t happen. Duolingo has the concept of a “streak freeze” in the in-app shop by which you can spend the Duo currency you’ve earned by advancing across levels to buy a component in-app which prevents you to from losing your hard-earned streak, just for that day. Once the streak freeze is used, you have to buy a new one from the market again in order to be able to use it.

Streak freeze in action

What if you’re out for multiple days in a row and you’re not in a position to play/buy the streak freeze? This happened to me once while I was travelling when I was stuck without internet for three days. I had a pretty long streak at the time and I ended up losing all the motivation to play the game for a while.

Automatic Streak Save

However, later I learned that Duolingo has a public API that allows you to buy the streak freeze programmatically. This gave me an idea to quickly wire up a small script in python using the unofficial Python API which bought the streak freeze by using my account creds every time it was running. All I had to do was to set it up as a cron job on a system and it will ensure that my precious streak was not lost.

The code for the streak save is quite simple and described below:

main

def main():
    import os

    usernames = os.environ['usernames'].split(',')
    passwords = os.environ['passwords'].split(',')

    list(map(process_single_user, usernames, passwords))


if __name__ == "__main__":
    main()

This wrapper function is quite simple. It takes in a list of usernames and passwords from the environment, splits and processes each username combination one by one.

process_single_user

def item_already_equipped(lingo, item):
    if item == 'streak_freeze':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['num_item_streak_freeze']
    if item == 'rupee_wager':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['has_item_rupee_wager']

def process_single_user(username, password):
    import duolingo
    print(f"Processing {username}")
    try:
        lingo = duolingo.Duolingo(username, password)
    except ValueError:
        raise Exception("Username Invalid")

    stuff_to_purchase = ['streak_freeze', 'rupee_wager']

    for item in stuff_to_purchase:
        if(item_already_equipped(lingo, item)):
            print(f"Item: {item} Already Equipped")
            continue
        try:
            print("Trying to Buy " + item + " for " + username)
            lingo.buy_item(item, 'en')
            print("Bought " + item + " for " + username)
        except duolingo.AlreadyHaveStoreItemException:
            print("Item Already Equipped")
        except Exception:
            raise ValueError("Unable to buy double or nothing")

The process_single_user function takes does the following for a single user

  1. Create the Duolingo class by logging into the API for the user.
  2. For each item to purchase
    1. See if the item is already equipped
    2. If not, try to purchase the item
    3. Show appropriate error/info message

Running the script

Running the script is as simple as downloading the dependencies

Command:

usernames='username1,username2' passwords='pass1,pass2' python3 main.py

Output:

Processing username1
Item: streak_freeze Already Equipped
Item: rupee_wager Already Equipped
Processing username2
Trying to Buy streak_freeze for usernam2
Bought streak_freeze for username2

Local/Server Deployment

The easiest way to deploy this would be as a cron job. If you have a server that is always running, this is probably the fastest-to-get-it-running solution - simple and sufficiently good enough.

Deployment in AWS

However, a prerequisite for the above is that you need an always running server (at least during the times that you want to run the job). Initially, I had deployed the job using the Amazon T instances that are quite cheap, but you barely need a few seconds of runtime each day. The instrumentation to bring up a full instance and taking them down after seemed too much of a hassle.

Enter serverless functions.

Lambda Function

AWS Lambda is the AWS serverless offering, made to cater to exactly these kinds of use cases. You provide the function and AWS takes care of the rest - the deployment and orchestration. You only pay of the time it is running (along with generous free minutes!). Creating a lambda function is quite a breeze and there are numerous tutorials available online, so I wont delve into the details here. Here is a snapshot of how my code which is stored in AWS S3 looks, fully setup.

AWS Lambda code

Env vars

Once you have the code uploaded, you need provide the credentials. Currently I’ve provided them as environment variables since this is a personal account, but there are standard ways to provide this information as a secret if necessary.

AWS Lambda code

Trigger

You can use the Test function capability in the Lambda console to make sure that the setup is correct and the streak freeze is getting purchased. However, to make sure that this runs automatically, we need to set a trigger to the lambda functions. AWS has extended documentation on how to do this as well.

Here is my setup as an example:

AWS Lambda code

You should be good to go now! Depending on how you set up the trigger, you should be able to the lambda function running periodically!

Alarms

Now that you’re depending on Lambda to do the right thing (in the rare case that you can’t), how do you make sure that the Lambda function is itself not failing for whatever reason? To capture that case, I have an alarm setup in AWS Cloudwatch. This alarm scrapes throught the logs of the Lambda function and can send out an email alert in case a threshold is breached. In my specific case, I have set the alarm to alert me if there is at least one alert from this function in the last 6 hours, as you can see below:

AWS Lambda code

Note: This kind of failure is not uncommon and has actually happened a few times in the last few years whenever Duolingo changes their APIs. An example error message is show below:

Duo Error

Costs

You might also want to know how much it costs to have this setup! It turns out that our functions are lite enough and Amazon’s free limits are generous events such that all of the above setup comes at literally zero cost!

My Streak

Thank you, AWS!

In action

As you can see above, using this setup as a helper, I have managed to maintain a streak on Duolingo which extends for more than half a decade now #humblebrag! Thanks to Duolingo for keeping the API public and allowing such shenanigans to happen!

My Streak

Caveat: I’m well aware that for some people, such a setup might end up doing the opposite and take out the motivation to play at all. However, it has worked splendingly for my case as you can see above. YMMV