Logo

dev-resources.site

for different kinds of informations.

Build a Crypto Price Alert System with Telegram and AWS Lambda

Published at
1/2/2025
Categories
crypto
telegram
awslambda
notifications
Author
radzion
Author
7 person written this
radzion
open
Build a Crypto Price Alert System with Telegram and AWS Lambda

πŸ™ GitHub

Introduction to the Project

In this post, we'll build a simple system that sends a Telegram message when a cryptocurrency's price goes above or below a specified threshold. We'll run this system as an AWS Lambda function to keep it lightweight and cost-effective. To kickstart the project, we forked RadzionKit and used it as a base. You can find all the source code in the GitHub repository.

Why You Need a Price Alert System

If you occasionally trade crypto or have a specific price threshold at which you’d like to sell your holdings to make a tangible purchase, a simple notification system can provide peace of mind. With such a system, you won’t need to constantly monitor prices, freeing up your time and reducing stress.

Defining a Price Alert

To represent a price alert, we define it with the following fields:

  • id: A unique identifier for the alert, used in the database.
  • asset: The ID of the cryptocurrency to monitor.
  • targetPrice: The price at which the alert should trigger.
  • condition: Specifies whether the alert triggers when the price goes above or below the target price.
  • isNotified: A flag to indicate if the user has already been notified about this alert, preventing multiple notifications for the same condition.
import { EntityWithId } from "@lib/utils/entities/EntityWithId"

type PriceAlertCondition = "more" | "less"

export type PriceAlert = EntityWithId & {
  asset: string
  targetPrice: number
  condition: PriceAlertCondition
  isNotified: boolean
}
Enter fullscreen mode Exit fullscreen mode

Supporting Multiple Users (Optional)

Since we are the only user of this system, we don't need to include a field for the notification destination. However, you could easily extend this system to support multiple users by adding different Telegram chat IDs for each user.

Setting Up Price Alerts

To set and update price alerts, we use a TypeScript script called setPriceAlert.ts. In this script, we define two alerts for Ethereum: one to trigger when the price exceeds $4,000 and another for when it falls below $3,800. The script first clears all existing alerts from the database and then inserts the newly defined alerts.

import { deleteAllPriceAlerts, putPriceAlert } from "../db/priceAlerts"
import { PriceAlert } from "../entities/PriceAlert"

const rules: Omit<PriceAlert, "id" | "isNotified">[] = [
  {
    asset: "ethereum",
    targetPrice: 3800,
    condition: "less",
  },
  {
    asset: "ethereum",
    targetPrice: 4000,
    condition: "more",
  },
]

const priceAlerts: PriceAlert[] = rules.map((rule, index) => ({
  ...rule,
  id: index.toString(),
  isNotified: false,
}))

const setPriceAlerts = async () => {
  await deleteAllPriceAlerts()

  await Promise.all(priceAlerts.map((alert) => putPriceAlert(alert)))
}

setPriceAlerts()
Enter fullscreen mode Exit fullscreen mode

Managing Price Alerts in the Database

To perform CRUD operations on price alerts, we use a set of functions in the database-related file. These functions leverage utilities from RadzionKit, which simplify interactions with DynamoDB.

import { getPickParams } from "@lib/dynamodb/getPickParams"
import { totalScan } from "@lib/dynamodb/totalScan"
import { PriceAlert } from "../entities/PriceAlert"
import { getEnvVar } from "../getEnvVar"
import { DeleteCommand, PutCommand } from "@aws-sdk/lib-dynamodb"
import { dbDocClient } from "@lib/dynamodb/client"
import { updateItem } from "@lib/dynamodb/updateItem"

const tableName = getEnvVar("PRICE_ALERTS_TABLE_NAME")

export const getPriceAlertItemParams = (id: string) => ({
  TableName: tableName,
  Key: {
    id,
  },
})

export const getAllPriceAlerts = async <T extends (keyof PriceAlert)[]>(
  attributes?: T,
) => {
  return totalScan<Pick<PriceAlert, T[number]>>({
    TableName: tableName,
    ...getPickParams(attributes),
  })
}

export const deletePriceAlert = (id: string) => {
  const command = new DeleteCommand(getPriceAlertItemParams(id))

  return dbDocClient.send(command)
}

export const deleteAllPriceAlerts = async () => {
  const alerts = await getAllPriceAlerts(["id"])

  return Promise.all(alerts.map(({ id }) => deletePriceAlert(id)))
}

export const putPriceAlert = (user: PriceAlert) => {
  const command = new PutCommand({
    TableName: tableName,
    Item: user,
  })

  return dbDocClient.send(command)
}

export const updatePriceAlert = async (
  id: string,
  fields: Partial<PriceAlert>,
) => {
  return updateItem({
    tableName: tableName,
    key: { id },
    fields,
  })
}
Enter fullscreen mode Exit fullscreen mode

Fetching Cryptocurrency Prices

To fetch cryptocurrency prices, we use the CoinGecko API. The getAssetPrices function accepts an array of asset IDs and an optional fiat currency.

import { addQueryParams } from "@lib/utils/query/addQueryParams"
import { FiatCurrency } from "../FiatCurrency"
import { queryUrl } from "@lib/utils/query/queryUrl"
import { recordMap } from "@lib/utils/record/recordMap"

type Input = {
  ids: string[]
  fiatCurrency?: FiatCurrency
}

type Response = Record<string, Record<FiatCurrency, number>>

const baseUrl = "https://api.coingecko.com/api/v3/simple/price"

export const getAssetPrices = async ({ ids, fiatCurrency = "usd" }: Input) => {
  const url = addQueryParams(baseUrl, {
    ids: ids.join(","),
    vs_currencies: fiatCurrency,
  })

  const result = await queryUrl<Response>(url)

  return recordMap(result, (value) => value[fiatCurrency])
}
Enter fullscreen mode Exit fullscreen mode

Constructing the API Request

To construct the URL, we start with the baseUrl and append the asset IDs and currency query parameters using the addQueryParams function from RadzionKit. The data is then fetched with the queryUrl helper function. Since we only need results in a single currency, we simplify the nested response structure by flattening it with the recordMap function.

Sending Telegram Notifications

To receive notifications about price changes, we use the node-telegram-bot-api library. The sendPriceChangeAlert function takes the price and asset as input, initializes a Telegram bot instance, and sends a message to the specified chat ID, ensuring timely alerts for significant price movements.

import { getEnvVar } from "../getEnvVar"
import TelegramBot from "node-telegram-bot-api"

type Input = {
  price: number
  asset: string
}

export const sendPriceChangeAlert = async ({ price, asset }: Input) => {
  const bot = new TelegramBot(getEnvVar("TELEGRAM_BOT_TOKEN"))

  const message = `${asset} price: ${price}`

  return bot.sendMessage(getEnvVar("TELEGRAM_BOT_CHAT_ID"), message)
}
Enter fullscreen mode Exit fullscreen mode

Managing Environment Variables

To ensure a single source of truth for environment variables, we use a getEnvVar function. This function throws an error if a required variable is missing.

type VariableName =
  | "TELEGRAM_BOT_TOKEN"
  | "TELEGRAM_BOT_CHAT_ID"
  | "SENTRY_KEY"
  | "PRICE_ALERTS_TABLE_NAME"

export const getEnvVar = (name: VariableName): string => {
  const value = process.env[name]
  if (!value) {
    throw new Error(`Missing ${name} environment variable`)
  }

  return value
}
Enter fullscreen mode Exit fullscreen mode

Running the Price Watcher

With all the essential components in place, we can bring them together in the core function of our system: runPriceWatcher.

import { match } from "@lib/utils/match"
import { getAssetPrices } from "../../../lib/chain/price/utils/getAssetPrices"
import { getAllPriceAlerts, updatePriceAlert } from "../db/priceAlerts"
import { sendPriceChangeAlert } from "./sendPriceChangeAlert"

export const runPriceWatcher = async () => {
  const items = await getAllPriceAlerts()

  const assets = items.map((item) => item.asset)

  const assetPrices = await getAssetPrices({ ids: assets })

  await Promise.all(
    items.map(async ({ id, isNotified, condition, asset, targetPrice }) => {
      const price = assetPrices[asset]
      const isConditionMet = match(condition, {
        more: () => price > targetPrice,
        less: () => price < targetPrice,
      })

      if (isConditionMet && !isNotified) {
        await sendPriceChangeAlert({ price, asset })

        return updatePriceAlert(id, {
          isNotified: true,
        })
      } else if (!isConditionMet && isNotified) {
        return updatePriceAlert(id, {
          isNotified: false,
        })
      }
    }),
  )
}
Enter fullscreen mode Exit fullscreen mode

How the Watcher Works

First, runPriceWatcher retrieves all the price alerts from the database and extracts the asset IDs to fetch their current prices. For each alert, it checks if the condition is met and whether the user has already been notified. If the condition is met and the user hasn't been notified, a Telegram message is sent, and the alert is updated accordingly. Conversely, if the condition is no longer met but the user was previously notified, the notification flag is reset to ensure the system behaves as expected.

We'll deploy our code as an AWS Lambda function, wrapping it with Sentry to receive notifications about any potential issues.

import { AWSLambda } from "@sentry/serverless"
import { getEnvVar } from "./getEnvVar"
import { runPriceWatcher } from "./core/runPriceWatcher"

AWSLambda.init({
  dsn: getEnvVar("SENTRY_KEY"),
})

exports.handler = AWSLambda.wrapHandler(runPriceWatcher)
Enter fullscreen mode Exit fullscreen mode

Setting Up Resources with Terraform

To set up the necessary resources for our watcher, we'll use Terraform. To schedule the function to run every 10 minutes, we'll configure a CloudWatch Event Rule. You can find the Terraform code in the GitHub repository.

Conclusion

By combining AWS Lambda, Telegram notifications, and Terraform for resource management, we’ve created a simple yet effective system to monitor cryptocurrency prices. With this setup, you can stay informed without constantly checking the market, and the flexibility of the code allows for easy customization and expansion. Happy coding!

awslambda Article's
30 articles in total
Favicon
Build a Crypto Price Alert System with Telegram and AWS Lambda
Favicon
Leveraging Docker with AWS Lambda for Custom Runtimes and Large Deployments
Favicon
Docker for Serverless: Customizing Functions and Scaling Flexibly
Favicon
Inventory Management with AWS Lambda Ξ»
Favicon
Lambda Power Tuning: Una comparativa entre arquitecturas x86_64 y arm64
Favicon
A Beginners Guide to Serverless API Gateway Authentication with Lambda Authorizer
Favicon
Serverless Functions: Unlocking the Power of AWS Lambda, Azure Functions, and More
Favicon
Mastering Serverless and Event-Driven Architectures with AWS: Innovations in Lambda, EventBridge, and Beyond
Favicon
Parse UserParameters sent from AWS CodePipeline to AWS Lambda in Go
Favicon
Leveraging Amazon Connect for Real-Time Incident Response Calls
Favicon
Lambda Code Execution Freeze/Thaw
Favicon
Efficiently Delete Inactive User Data Using TypeScript and AWS Lambda
Favicon
Unlocking Serverless: Build Your First Python AWS Lambda Function
Favicon
Lamba LLRT(Low Latency Runtime Javascript)
Favicon
Building Scalable Microservices with AWS Lambda and Serverless
Favicon
Serverless Architecture Best Practices
Favicon
Deep Dive on Writing and Reading Data to DynamoDB Table from Lambda Functions Using AWS Cloud Map Service Discovery
Favicon
AWS Lambda in Deno or Bun
Favicon
Lambda extension to cache SSM and Secrets Values for PHP Lambda on CDK
Favicon
Create a Fast Node.js Serverless Backend Using AWS Lambda and DynamoDB
Favicon
30-day Learning Challenge: Day 2β€” Learning AWS S3
Favicon
AWS Lambda Functions Failure Management
Favicon
Understanding Load Balancers: How They Work, Types, Algorithms, and Use Cases
Favicon
How to Deploy Dart Functions to AWS Lambda
Favicon
Using Custom Authorization - Request based for AWS Lambda
Favicon
How to generate a presigned url to upload images to S3
Favicon
Create an AppSync API using Terraform
Favicon
Creating a Cognito Trigger using CDK and TypeScript
Favicon
API Gateway REST API with Lambda Integration
Favicon
AWS Lambda Runtime debate

Featured ones: