< Blogs
Terra API

Terra API

January 29, 2024

How to Build a Move to Earn App with Terra and ChatGPT

Introduction

Terra is an API that delivers wearable data to your applications via Webhooks. In this article, you will learn how to use the Terra API to create a simple Move to Earn app. You will also use ChatGPT to analyze the intensity of physical activity.

What is a Move to Earn App?

A Move to Earn app rewards users with cryptocurrency or other incentives for engaging in physical activities, such as walking, exercising, or completing health-related challenges.

Users earn rewards based on their level of activity or achievement within the app. In our example, we'll display a reward for the user based on their "wattage", also known as "power output". Wattage measures the amount of mechanical power generated during physical exercise.

We will be using Python and Flask to consume data from the Terra Webhook, store it in a database, show it using a ChartJS graph, and then use the OpenAI API to analyze it.

Here is an image of the web application we are going to build:

move1.png

Step 1 - Obtain Your Credentials from The Terra Dashboard

To communicate with a wearable through the Terra API, you need the following:

  • Your API Key
  • Your Dev ID

Go to your Terra Dashboard, under Connections, you will find your API Key and Dev ID in the bottom right corner in the API Credentials Popup. They are used in virtually every interaction with the API - keep them safe!

Step 2 - Create a Flask Webhook Consumer and Serve it with Ngrok

Create a virtual environment and activate it:

python -m venv env

source env/bin/activate

Install the necessary packages:

pip install Flask Flask-SQLAlchemy terra-python openai

The terra-python package is a wrapper for the Terra endpoints and models. We'll use it to verify and authenticate incoming webhook data.

Next, create a small Flask app file called app.py:

import logging
from flask import Flask, Response, request, render_template
from terra.base_client import Terra

logging.basicConfig(level=logging.INFO)
_LOGGER = logging.getLogger("app")

terra = Terra(api_key='<API-KEY>',
              dev_id='<DEV-ID>',
              secret='<SIGNING-SECRET>')

app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def consume_terra_webhook() -> Response:
    if request.method == 'POST':
        # Get data from the Terra Webhook
        body = request.get_json()

        # Log that the Webhook was recieved
        _LOGGER.info(
            "Received webhook for user %s of type %s",
            body.get("user", {}).get("user_id"),
            body["type"])

        # Just a health check, return 200
        if body["type"] == 'healthcheck':
            return Response(status=200)

        # Verify Terra Signature
        verified = terra.check_terra_signature(request.get_data().decode("utf-8"),
                                            request.headers['terra-signature'])

        # The data is verified
        if verified:
            return Response(status=200)

        else:
            return Response(status=403)

    # Handle GET requests
    if request.method == 'GET':
        return render_template('index.html')

if __name__ == "__main__":
    app.run(host="localhost", port=8080)

Make sure to fill in <API-KEY> and <DEV-ID> with your API key and developer ID. The signing secret will be added later in this article.

This simple Flask web application serves as a webhook endpoint for Terra.

The code performs the following:

  1. Initializes logging with INFO level.
  2. Initializes a Terra client with the provided API key, and developer ID.
  3. Creates a Flask web application instance app.
  4. Defines a route ("/") that handles both GET and POST requests.

In the consume_terra_webhook() function, you have two conditions, one for handling POST requests and one for GET requests.

If the request is a POST request, this means that the Terra API has sent some data through a Webhook. So, the code does the following:

  1. Retrieves JSON data from the request body.
  2. Logs information about the received webhook, including user ID and data type.
  3. If the webhook type is a "healthcheck" respond with a 200 status.
  4. Verifies the Terra signature by comparing it with the calculated signature using the check_terra_signature() method. This will not work for now, as we haven't yet added a signing secret.
  5. In the if verified: condition, the signature is verified, so we respond with a 200 status for now; you will later modify this part of the code, so that it inserts data into a database. We will also respond with an HTTP 201 status code instead of 200 to signify that a resource was added to the server.
  6. If the signature is not verified, respond with a HTTP 403 FORBIDDEN status.

On the other hand, if the request is a GET request, the code renders an index.html template.

Now, create a templates folder inside your Flask app's directory and create a new index.html file inside it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Move to Earn Terra App</title>
    <!-- Add Chart.js CDN -->
    <script src="<https://cdn.jsdelivr.net/npm/chart.js>"></script>
</head>
<body>
    <div style="width:50%; margin:auto;">
        <h1>Move to Earn Terra App</h1>
    </div>
</body>
</html>

Save and close the file.

Here, you just display a simple <h1> header. You will later modify this to display a ChartJS graph.

Next Run the application on port 8080:

flask --app app run -p 8080

As Terra cannot send Webhook data to your local development server, you have to expose your server to the Internet using Ngrok. To install it, check out this page.

Once you install Ngrok, create an account, then obtain your authentication token from the Ngrok dashboard. Once you get your authtoken, add it to your Ngrok agent using the following command:

ngrok config add-authtoken <TOKEN>

This will allow you to access some features such as rendering HTML files.

Once you set up Ngrok, use it to expose your Flask application that is currently running on port 8080:

ngrok http 8080

This command will give you a URL under Forwarding. Use your browser to access this URL, you should see your index page.

Note: Copy your Ngrok URL. This is your Server URL, and you will use it to connect to a Terra Webhook in the next step.

Step 3 - Connect a Terra Webhook with Your Server

You will now connect a test wearable with your Flask app.

Note: We will use Garmin in this demo.

Go to your Terra Dashboard, then in Connections. Under Sources, click Add Sources, and select Garmin, then Save.

Next, under Destinations click Add Destination, then select Webhook, then Next.

Put your Ngrok Server URL under host.

The Connections dashboard should now look like so:

Garmin-conn.png

Now, you need to obtain your Signing secret. Click the three dots to the right of Webhook then Edit.

Copy the Signing secret. This is needed to authenticate and verify Webhook requests.

To use your Webhook's signing secret, modify the secret parameter in your Terra initiation inside your app.py Flask application:

terra = Terra(api_key='<API-KEY>',
              dev_id='<DEV-ID>',
              secret='<PASTE-SIGNING-SECRET-HERE>')

Once modified, remember to rerun your Flask app:

flask --app app run -p 8080

Note: Make sure Ngrok is still running. If you've stopped it and restarted it, your Server URL will change, so make sure to edit the Webhook host in your Terra Dashboard and replace the old Ngrok URL with the new one.

We will now test the Webhook connection. In the Terra Dashboard, go to Tools > Generate > Select Data Source > Garmin.

Then click on Activity, then click Generate test data.

Once data is generated. Click Send to Webhook.

Go back to your Flask server and wait for a few seconds. You should see the following message in the logs:

INFO:app:Received webhook for user <User-ID> of type activity
INFO:werkzeug:127.0.0.1 - - [23/Dec/2023 12:14:12] "POST / HTTP/1.1" 200 -

This means you have successfully connected a Terra Webhook with your Flask application and you are receiving data! Next, you'll modify app.py to store the data inside an SQLite database.

Step 4 - Storing Power Output in an SQLite Database

Now that you are receiving wearable data from the Terra Webhook, you can use Flask-SQLAlchemy to store this data in an SQL database. We will use an SQLite database in this demonstration.

Open your app.py file and modify it by importing and setting up Flask-SQLAlchemy, then adding a new Power database model, so that everything above the @app.route("/", methods=["GET", "POST"]) line looks as follows:

# app.py
import logging
from datetime import datetime

from flask import Flask, Response, request, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func

from terra.base_client import Terra

logging.basicConfig(level=logging.INFO)
_LOGGER = logging.getLogger("app")

terra = Terra(api_key='<API-KEY>',
              dev_id='<DEV-ID>',
              secret='<SIGNING-SECRET>')

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

db = SQLAlchemy(app)

# Database Table for Power Samples
class Power(db.Model):
    __tablename__ = 'power'
    id = db.Column(db.Integer, primary_key=True)
    timestamp = db.Column(db.String)
    watts = db.Column(db.Integer)

Save and close the file.

Here, you set up a database URI that will point to an SQLite database called app.db. This database file will be created inside a new instance folder that will be automatically added to your Flask project folder.

You also add a Flask-SQLAlchemy database model that represents a table called Power with a column for an ID, a timestamp, and a watts value, these last two items are provided to us by Terra.

Next, inside your Flask application folder, with your environment activated, open the Flask Shell to create the database file and Power data table:

flask shell

>>> from app import db, Power
>>> db.create_all()
>>> exit()

You should see a new app.db file inside an instance folder in your Flask project folder.

Next, modify the if verified condition code in your app.py, where you handle POST requests, from this:

# ...
        # The data is verified
        if verified:
            return Response(status=200)

To this:

# ....

        # The data is verified
        if verified:
            power_samples = body['data'][0]['power_data']\\\\
                                ['power_samples']

            # Add timestamps & Power data to a DB
            _LOGGER.info("Adding timestamps and watts to the DB")
            for sample in power_samples:
                db_power_sample = Power(timestamp=sample['timestamp'],
                                        watts=round(sample['watts']))
                db.session.add(db_power_sample)

            # Apply changes to the database
            db.session.commit()

            return Response(status=201)

Save and close the file.

Here, you extract the power output data from the body variable, which holds the data sent by the Terra API. You log the database insertion, then loop through the provided samples and insert each timestamp and its corresponding power output sample into the database session.

Finally, you call db.session.commit() to apply changes to the database, then respond with an HTTP 201 status code.

The full file should now be like this:

# app.py

import logging
from datetime import datetime

from flask import Flask, Response, request, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func

from terra.base_client import Terra

logging.basicConfig(level=logging.INFO)
_LOGGER = logging.getLogger("app")

terra = Terra(api_key='<API-KEY>',
              dev_id='<DEV-ID>',
              secret='<SIGNING-SECRET>')

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'

db = SQLAlchemy(app)

# Database Table for Power Samples
class Power(db.Model):
    __tablename__ = 'power'
    id = db.Column(db.Integer, primary_key=True)
    timestamp = db.Column(db.String)
    watts = db.Column(db.Integer)

@app.route("/", methods=["GET", "POST"])
def consume_terra_webhook() -> Response:
    if request.method == 'POST':
        # Get data from the Terra Webhook
        body = request.get_json()

        # Log that the Webhook was recieved
        _LOGGER.info(
            "Received webhook for user %s of type %s",
            body.get("user", {}).get("user_id"),
            body["type"])

        # Just a health check, return 200
        if body["type"] == 'healthcheck':
            return Response(status=200)

        # Verify Terra Signature
        verified = terra.check_terra_signature(request.get_data().decode("utf-8"),
                                            request.headers['terra-signature'])

        # The data is verified
        if verified:
            power_samples = body['data'][0]['power_data']\\\\
                                ['power_samples']

            # Add timestamps & Power data to a DB
            _LOGGER.info("Adding timestamps and watts to the DB")
            for sample in power_samples:
                db_power_sample = Power(timestamp=sample['timestamp'],
                                        watts=round(sample['watts']))
                db.session.add(db_power_sample)

            # Apply changes to the database
            db.session.commit()

            return Response(status=201)

        else:
            return Response(status=403)

    # Handle GET requests
    if request.method == 'GET':
        return render_template('index.html')

if __name__ == "__main__":
    app.run(host="localhost", port=8080)

Step 5 - Adding a Power Output Graph and Coins Earned

To display the power output graph and show the total number of earned coins the user has accumulated, we will modify the part of code that handles GET requests.

Open app.py, then add a new date_time_format() function above the @app.route("/", methods=["GET", "POST"]) line.

import logging
from datetime import datetime

from flask import Flask, Response, request, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func

from terra.base_client import Terra

# ...
# ...
# ...

# Function to format timestamps to '%Y-%m-%d %H:%M:%S'
# (Makes the ChartJS graph easier to read)
def date_time_format(timestamp):
    return datetime.fromisoformat(timestamp[:-6]).strftime('%Y-%m-%d %H:%M:%S')

@app.route("/", methods=["GET", "POST"])
# ...

Note: The rest of the code is omitted and referenced as # .... The date_time_format() function will be used to make timestamps easier to read.

Next, modify the GET request handler from this:

    # Handle GET requests
    if request.method == 'GET':
        return render_template('index.html')

To this:

    # Handle GET requests
    if request.method == 'GET':
        # Get the latest 10 Power samples from the database
        latest_power_samples = Power.query.order_by(Power.timestamp.desc()) \\\\
                                                    .limit(10).all()[::-1]

        # Extract timestamps & make them readable
        timestamps = [date_time_format(s.timestamp) for s in latest_power_samples]
        # Extract watts values
        watts  = [s.watts for s in latest_power_samples]

        total_watts = db.session.query(func.sum(Power.watts)).scalar()

        return render_template('index.html',
                               watts=watts,
                               timestamps=timestamps,
                               total_watts=total_watts)

Here, you get the latest power samples from the database, extract timestamps and make them readable, then extract watt values.

You use the func.sum() SQLAlchemy function to sum up the total number of watts the user has generated. This is equivalent to using the SQL SUM() function. This total number will act as the number of coins earned in our demo.

You then pass these values to the index.html template.

To display the power output graph and the number of accumulated coins. Open the index.html template and add the following <canvas>, <div>, and <script> tags. So that the <body> contents look as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Move to Earn Terra App</title>
    <!-- Add Chart.js CDN -->
    <script src="<https://cdn.jsdelivr.net/npm/chart.js>"></script>
</head>
<body>
    <div style="width:50%; margin:auto;">
        <h1>Move to Earn Terra App</h1>
        <canvas id="barChart" height=100></canvas>
        <div style="color: #8109f6">
            <h2>Coins Earned So Far:</h2>
            <h1>๐Ÿ’ธ {{ total_watts }} coins ๐Ÿ’ธ<h1>
        </div>
        <hr>
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            var ctx = document.getElementById('barChart').getContext('2d');
            var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: {{ timestamps | tojson | safe }},
                    datasets: [{
                        label: 'Watts',
                        data: {{ watts | tojson | safe }},
                        backgroundColor: 'rgba(75, 192, 192, 0.2)',
                        borderColor: 'rgba(75, 192, 192, 1)',
                        borderWidth: 1
                    }]
                },
                options: {
                    scales: {
                        y: {
                            beginAtZero: true
                        }
                    }
                }
            });
        });
    </script>
</body>
</html>

Save and close the file.

This new ChartJS code generates a line chart with the provided timestamps and power output values.

To test out the application, make sure your Flask server is running, and use your browser to access your Ngrok Server URL. You should see a new line chart for power output, and a heading that showcases the number of coins earned so far.

Note: If you see an empty chart, just go back to the Terra Data Generator, generate some data, then send it to the Webhook. This should populate the database with a few entries.

Next, you'll add a new feature using AI, and provide an exercise intensity analysis.

Step 6 - Adding an Exercise Intensity Analysis using the OpenAI API

We will now use the OpenAI API (also known as ChatGPT API) to provide an exercise intensity analysis.

Inside your Flask project, open a new file called gpt.py then add the following code to it:

# gpt.py
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY"),
)

def ask(question):
    completion = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": """
         You are a physical activity app.
         You analyze watt values that measure how much energy is being exerted.
         You provide brief straightforward analysis with minimal, to-the-point,
         and concise information
         on whether a sample is at low, moderate, or high intensity
         based on the number of watts.
         Your answer should be in safe HTML code inside one <div>,
         with pretty blue CSS styling.
         Use the following template:
         [timestamp] | [watts number] Watts | [output]
         The output should be one of:
            'Low Intensity', 'Moderate Intensity', 'High Intensity'.
         """},
        {"role": "user", "content": question}
    ]
    )

    return completion.choices[0].message.content

Here, you set up an OpenAI client, passing your OpenAI API key as an environment variable.

In the ask() function, you set up a question that essentially provides an exercise intensity level in HTML, which you'll later display in your index.html template.

Now, set your Open API key in an environment variable.

For Linux & MacOS:

export OPENAI_API_KEY='your Open API key'

For Windows:

set OPENAI_API_KEY='your Open API key'

Next, inside your app.py file, add a gpt import at the top:

import logging
from datetime import datetime

from flask import Flask, Response, request, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func

from terra.base_client import Terra

import gpt

Then use the gpt.ask() function in the part of code where you handle GET requests, just before you render the index.html template, and pass the answer to this same template:

        watts_analysis = gpt.ask(f"""Analyse the following data:
                                     Timestamps: {timestamps},
                                     Watts: {watts},
                                     """)

        return render_template('index.html',
                               watts=watts,
                               timestamps=timestamps,
                               total_watts=total_watts,
                               watts_analysis=watts_analysis
                               )

Here you ask ChatGPT using the ask() function to provide an intensity level analysis for each timestamp.

Next, modify your index.html template to display the intensity level analysis. To do so, add a {{ watts_analysis | safe }} line below the coins earned <div>:

    <div style="width:50%; margin:auto;">
        <h1>Move to Earn Terra App</h1>
        <canvas id="barChart" height=100></canvas>
        <div style="color: #8109f6">
            <h2>Coins Earned So Far:</h2>
            <h1>๐Ÿ’ธ {{ total_watts }} coins ๐Ÿ’ธ<h1>
        </div>
        <hr>
        {{ watts_analysis | safe }}
    </div>

With this, you should see your application fully functioning:

move3.png

Conclusion

Congrats! You've learned how to use the Terra API to make a Move-to-Earn application.

To learn more about the awesome things you can build with Terra, check out the following pages:

More Topics

All Blogs
Team Spotlight
Startup Spotlight
How To
Blog
Podcast
Product Updates
Wearables
See All >
Strava Pulls the Plug on their API: What This Means for Developers

Strava Pulls the Plug on their API: What This Means for Developers

Strava discontinued their API service, changing the ecosystem of third-party apps that have relied on their platform. How can developers react to this?

Terra APITerra API
November 21, 2024
Alternatives to the latest changes in the Strava API

Alternatives to the latest changes in the Strava API

Strava just introduced big changes to their API program. These changes will basically kill off a lot of apps. Use Terra API instead to avoid this

Kyriakos EleftheriouKyriakos Eleftheriou
November 19, 2024
Cycling Legend, Investor, and Podcaster - Lance Armstrong

Cycling Legend, Investor, and Podcaster - Lance Armstrong

In this podcast, Kyriakos the CEO of Terra interviews Lance Armstrong about his journey as a young athlete, cycling champion, and successful investor and podcaster.

Terra APITerra API
November 8, 2024
Founder of Donโ€™t Die - Bryan Johnson

Founder of Donโ€™t Die - Bryan Johnson

In this podcast, Bryan Johnson shares his personal story of how he began his journey to becoming the the world's most measured human.

Terra APITerra API
October 25, 2024
CEO and Co-Founder of Veri - Anttoni Aniebonam

CEO and Co-Founder of Veri - Anttoni Aniebonam

In this podcast with Kyriakos the CEO of Terra, Anttoni Aniebonam shares his journey founding Veri, and his decision in the acquisition by Oura to further his vision.

Terra APITerra API
September 27, 2024
next ventures
pioneer fund
samsung next
y combinator
general catalyst

Cookie Preferences

Essential CookiesAlways On
Advertisement Cookies
Analytics Cookies

Crunch Time: Embrace the Cookie Monster Within!

We use cookies to enhance your browsing experience and analyse our traffic. By clicking โ€œAccept Allโ€, you consent to our use of cookies according to our Cookie Policy. You can change your mind any time by visiting out cookie policy.