<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Egor Gorbachev</title><author><name>Egor Gorbachev</name></author><id>https://teletype.in/atom/alteregor</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/alteregor?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@alteregor?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=alteregor"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/alteregor?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-03T22:48:30.414Z</updated><entry><id>alteregor:how-to-integrate-telegram-stars</id><link rel="alternate" type="text/html" href="https://teletype.in/@alteregor/how-to-integrate-telegram-stars?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=alteregor"></link><title>How to integrate Telegram Stars Payment to your bot</title><published>2024-06-25T04:50:48.795Z</published><updated>2024-10-13T05:56:33.259Z</updated><summary type="html">Telegram Stars is a new in-app currency introduced by Telegram for purchasing digital goods and services. Users can buy Stars directly within Telegram using Apple Pay or Google Pay. This article is a step-by-step guide on how to connect Stars payments to your bot. While the example will be in Node.js, I'll show you the general ideas for accepting Stars payments so you can apply them to any language and framework.</summary><content type="html">
  &lt;p id=&quot;UkOp&quot;&gt;Telegram Stars is a new in-app currency introduced by Telegram for purchasing digital goods and services. Users can buy Stars directly within Telegram using Apple Pay or Google Pay. This article is a step-by-step guide on how to connect Stars payments to your bot, as well as &lt;a href=&quot;https://github.com/kubk/telegram-stars-example&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt; example. While the code will be in Node.js, I&amp;#x27;ll show you the general ideas for accepting Stars payments so you can apply them to any language and framework.&lt;/p&gt;
  &lt;h3 id=&quot;Gu5v&quot;&gt;&lt;/h3&gt;
  &lt;h3 id=&quot;rByY&quot;&gt;Step 1. Create a bot&lt;/h3&gt;
  &lt;p id=&quot;HSsA&quot;&gt;If you haven&amp;#x27;t created a bot yet, go to &lt;a href=&quot;https://t.me/botfather&quot; target=&quot;_blank&quot;&gt;BotFather&lt;/a&gt;, create one, and grab your API bot token.&lt;/p&gt;
  &lt;p id=&quot;DubV&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;Ps14&quot;&gt;Step 2. Set up a project&lt;/h3&gt;
  &lt;p id=&quot;WRdQ&quot;&gt;You&amp;#x27;ll need a project that uses Telegram&amp;#x27;s bot API to issue invoices and handle successful payments. We&amp;#x27;ll use grammY, a bot framework for Node.js. Run the following commands:&lt;/p&gt;
  &lt;pre id=&quot;LkVC&quot; data-lang=&quot;bash&quot;&gt;mkdir telegram-stars-payment &amp;amp;&amp;amp; cd telegram-stars-payment
npm i grammy&lt;/pre&gt;
  &lt;p id=&quot;1G3a&quot;&gt;Copy the following code to your &lt;code&gt;index.mjs&lt;/code&gt; and insert your bot token:&lt;/p&gt;
  &lt;pre id=&quot;3MDW&quot; data-lang=&quot;javascript&quot;&gt;import { Bot } from &amp;quot;grammy&amp;quot;;

const bot = new Bot(&amp;quot;&amp;quot;); // &amp;lt;-- put your bot token between the &amp;quot;&amp;quot;

bot.command(&amp;quot;start&amp;quot;, (ctx) =&amp;gt; ctx.reply(&amp;quot;Welcome! Up and running.&amp;quot;));

bot.start();&lt;/pre&gt;
  &lt;p id=&quot;onaU&quot;&gt;Now run the bot via &lt;code&gt;node index.mjs&lt;/code&gt;. The bot should start responding to the &lt;code&gt;start&lt;/code&gt; command.&lt;/p&gt;
  &lt;p id=&quot;HvJ6&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;zxO7&quot;&gt;Step 3. Create an invoice&lt;/h3&gt;
  &lt;p id=&quot;KUsn&quot;&gt;In terms of Telegram, an invoice is a special message type used in the Telegram Bot API to make payments within the chat. Regardless of which framework you&amp;#x27;re using, you can find these two methods for creating invoices:&lt;/p&gt;
  &lt;ol id=&quot;WlsL&quot;&gt;
    &lt;li id=&quot;RMfW&quot;&gt;&lt;code&gt;sendInvoice&lt;/code&gt;: The bot sends a message with a payment interface to the user, allowing them to complete the transaction within the chat. It includes details like the title, description, currency, prices, and other optional parameters such as photos and payload.&lt;/li&gt;
    &lt;li id=&quot;35TL&quot;&gt;&lt;code&gt;createInvoiceLink&lt;/code&gt;: This command generates a link to an invoice. When users click the link, they are redirected to a payment interface to complete the transaction. This method is useful for sharing the payment link outside the Telegram chat, such as in Telegram Mini Apps, emails, or on websites.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;Cuwz&quot;&gt;We&amp;#x27;ll stick to the first method for this example. Let&amp;#x27;s add a command to send invoices:&lt;br /&gt;&lt;/p&gt;
  &lt;pre id=&quot;3PQI&quot; data-lang=&quot;javascript&quot;&gt;bot.command(&amp;quot;pay&amp;quot;, (ctx) =&amp;gt; {
  return ctx.replyWithInvoice(
    &amp;quot;Test Product&amp;quot;, // Product title
    &amp;quot;Test description&amp;quot;, // Product description
    &amp;quot;{}&amp;quot;, // Product payload, not required for now
    &amp;quot;XTR&amp;quot;, // Stars Currency 
    [{ amount: 1, label: &amp;quot;Test Product&amp;quot; }, // Product variants
  ]);
});&lt;/pre&gt;
  &lt;p id=&quot;uLdj&quot;&gt;You can now send &lt;code&gt;/pay&lt;/code&gt; to the bot, and it will respond with an invoice:&lt;/p&gt;
  &lt;figure id=&quot;2JdC&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/04/99/0499a283-7480-4882-9147-86e6261c91e0.jpeg&quot; width=&quot;1170&quot; /&gt;
    &lt;figcaption&gt;The result of calling &amp;#x60;replyWithInvoice&amp;#x60;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;q6dQ&quot;&gt;Step 4. Handle pre_checkout_query&lt;/h3&gt;
  &lt;p id=&quot;L0XQ&quot;&gt;Now we need to handle the payment. When a user buys Stars from Telegram and presses the pay button, the Bot API sends an update to the bot with all the order details in a field called &lt;code&gt;pre_checkout_query&lt;/code&gt;. The bot must respond with &lt;code&gt;answerPreCheckoutQuery&lt;/code&gt; within 10 seconds, or the transaction will be canceled.&lt;/p&gt;
  &lt;p id=&quot;yDdW&quot;&gt;If the bot can&amp;#x27;t process the order, it should send an error message explaining why in plain language (e.g., &amp;quot;Sorry, the item is out of stock. Would you like to order something else?&amp;quot;). Telegram will show this message to the user. For our example, we don&amp;#x27;t need that, so we&amp;#x27;ll just answer via &lt;code&gt;answerPreCheckoutQuery&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;rTXL&quot; data-lang=&quot;javascript&quot;&gt;bot.on(&amp;quot;pre_checkout_query&amp;quot;, (ctx) =&amp;gt; {
  return ctx.answerPreCheckoutQuery(true).catch(() =&amp;gt; {
    console.error(&amp;quot;answerPreCheckoutQuery failed&amp;quot;);
  });
});&lt;/pre&gt;
  &lt;p id=&quot;9ZfW&quot;&gt;Only after that Telegram will proceed with the payment.&lt;/p&gt;
  &lt;p id=&quot;UzZ5&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;KNBI&quot;&gt;Step 5. Handle successful payment&lt;/h3&gt;
  &lt;p id=&quot;2BmH&quot;&gt;At this point, the user has already paid, and their money has been sent to you. Telegram will notify you who paid and for what, so you can mark this user as paid in your database. To get notified, you only need to search for an event like &lt;code&gt;successful_payment&lt;/code&gt; in your bot framework API.&lt;/p&gt;
  &lt;pre id=&quot;xTfr&quot; data-lang=&quot;javascript&quot;&gt;// Map is used for simplicity. For production use a database
const paidUsers = new Map();

bot.on(&amp;quot;message:successful_payment&amp;quot;, (ctx) =&amp;gt; {
  if (!ctx.message || !ctx.message.successful_payment || !ctx.from) {
    return;
  }

  paidUsers.set(
    ctx.from.id,
    ctx.message.successful_payment.telegram_payment_charge_id,
  );

  console.log(ctx.message.successful_payment);
});

bot.command(&amp;quot;status&amp;quot;, (ctx) =&amp;gt; {
  const message = paidUsers.has(ctx.from.id)
    ? &amp;quot;You have paid&amp;quot;
    : &amp;quot;You have not paid yet&amp;quot;;
  return ctx.reply(message);
});&lt;/pre&gt;
  &lt;p id=&quot;2R21&quot;&gt;The basic version is ready! We&amp;#x27;ve saved the information about successful payments into a JavaScript Map. Make sure to use a real database for production so the data is persistent between app reloads. We&amp;#x27;ve saved the user&amp;#x27;s information alongside the &lt;code&gt;telegram_payment_charge_id&lt;/code&gt;. It&amp;#x27;s needed for refunds. Let&amp;#x27;s implement them:&lt;/p&gt;
  &lt;pre id=&quot;nVZv&quot; data-lang=&quot;javascript&quot;&gt;bot.command(&amp;quot;refund&amp;quot;, (ctx) =&amp;gt; {
  const userId = ctx.from.id;
  if (!paidUsers.has(userId)) {
    return ctx.reply(&amp;quot;You have not paid yet, there is nothing to refund&amp;quot;);
  }

  ctx.api
    .refundStarPayment(userId, paidUsers.get(userId))
    .then(() =&amp;gt; {
      paidUsers.delete(userId);
      return ctx.reply(&amp;quot;Refund successful&amp;quot;);
    })
    .catch(() =&amp;gt; ctx.reply(&amp;quot;Refund failed&amp;quot;));
});&lt;/pre&gt;
  &lt;p id=&quot;T6AV&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;RQMn&quot;&gt;Step 6. Adding Stars payment to Mini apps&lt;/h3&gt;
  &lt;p id=&quot;5eXy&quot;&gt;Now since you know how to create invoices in bot&amp;#x27;s conversation let&amp;#x27;s switch to mini apps. The idea is completely the same; you just need to use &lt;code&gt;createInvoiceLink&lt;/code&gt; instead of &lt;code&gt;replyWithInvoice&lt;/code&gt;. The method &lt;code&gt;createInvoiceLink&lt;/code&gt; will generate a link that you can open using Telegram&amp;#x27;s Mini App SDK. Let&amp;#x27;s create a simple API for that. Our mini app is going to ask this API for an invoice to display:&lt;/p&gt;
  &lt;pre id=&quot;obu8&quot; data-lang=&quot;javascript&quot;&gt;import express from &amp;quot;express&amp;quot;;
import { Bot } from &amp;quot;grammy&amp;quot;;

const app = express();
const port = 3000;

app.use(express.json());

const bot = new Bot(&amp;#x27;&amp;#x27;); // Your bot token

app.post(&amp;quot;/generate-invoice&amp;quot;, async (req, res) =&amp;gt; {
  const title = &amp;quot;Test Product&amp;quot;;
  const description = &amp;quot;Test description&amp;quot;;
  const payload = &amp;quot;{}&amp;quot;;
  const currency = &amp;quot;XTR&amp;quot;;
  const prices = [{ amount: 1, label: &amp;quot;Test Product&amp;quot; }];

  const invoiceLink = await bot.api.createInvoiceLink(
    title,
    description,
    payload,
    &amp;quot;&amp;quot;, // Provider token must be empty for Telegram Stars
    currency,
    prices,
  );

  res.json({ invoiceLink });
});

app.listen(port, () =&amp;gt; {
  console.log(&amp;#x60;Server running on port ${port}&amp;#x60;);
});&lt;/pre&gt;
  &lt;p id=&quot;ACIT&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;jcd2&quot;&gt;Now we can open this invoice link using one of the following Telegram Mini App SDKs:&lt;/p&gt;
  &lt;ul id=&quot;8RvG&quot;&gt;
    &lt;li id=&quot;RnST&quot;&gt;&lt;a href=&quot;https://docs.telegram-mini-apps.com/packages/telegram-apps-sdk/components/invoice#opening-invoice&quot; target=&quot;_blank&quot;&gt;@telegram-apps&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;HbiP&quot;&gt;&lt;a href=&quot;https://github.com/twa-dev/SDK/blob/5112c5a339ace008e3791260370cd43865d9c50c/src/telegram-web-apps.js#L1756&quot; target=&quot;_blank&quot;&gt;@twa-dev/sdk&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;rS97&quot;&gt;An example for &lt;code&gt;@twa-dev/sdk:&lt;/code&gt;&lt;/p&gt;
  &lt;pre id=&quot;8NxE&quot; data-lang=&quot;javascript&quot;&gt;// Call the endpoint we&amp;#x27;ve just created
const response = await apiGetLink();

WebApp.openInvoice(response.invoiceLink, (status) =&amp;gt; {
  if (status === &amp;quot;paid&amp;quot;) {
    // Telegram notified us that the payment has been made
    // Refresh user&amp;#x27;s balance, plan, etc
  }
});&lt;/pre&gt;
  &lt;p id=&quot;o8PC&quot;&gt;In the future I&amp;#x27;ll add an example for &lt;code&gt;@telegram-apps&lt;/code&gt;. This is how it works in one of my &lt;a href=&quot;https://t.me/memo_card_bot&quot; target=&quot;_blank&quot;&gt;apps&lt;/a&gt;:&lt;/p&gt;
  &lt;figure id=&quot;XRIO&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/2c/a0/2ca0907e-4000-464a-a925-f6cd1ab89175.png&quot; width=&quot;770&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;lrm7&quot;&gt;The code with instructions for the demo payment is available on GitHub: &lt;a href=&quot;https://github.com/kubk/telegram-stars-example&quot; target=&quot;_blank&quot;&gt;https://github.com/kubk/telegram-stars-example&lt;/a&gt;&lt;/p&gt;
  &lt;h2 id=&quot;xYFl&quot;&gt;Common Questions&lt;/h2&gt;
  &lt;h3 id=&quot;U5vP&quot;&gt;&lt;strong&gt;How to buy Telegram Stars?&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;repc&quot;&gt;You can buy Telegram Stars in the conversation with the bot you&amp;#x27;ve just created while following the article. You can also purchase Stars via &lt;a href=&quot;https://t.me/premiumbot&quot; target=&quot;_blank&quot;&gt;@PremiumBot&lt;/a&gt; on the &lt;a href=&quot;https://telegram.org/android&quot; target=&quot;_blank&quot;&gt;direct version&lt;/a&gt; of Telegram for Android, as well as Telegram’s &lt;a href=&quot;https://telegram.org/apps#web-apps&quot; target=&quot;_blank&quot;&gt;Web&lt;/a&gt; and &lt;a href=&quot;https://telegram.org/apps#desktop-apps&quot; target=&quot;_blank&quot;&gt;Desktop&lt;/a&gt; apps.&lt;/p&gt;
  &lt;h3 id=&quot;jYZL&quot;&gt;How to withdraw Stars that I got from users?&lt;/h3&gt;
  &lt;p id=&quot;gKnY&quot;&gt;You can see your earned stars if you go to your Bot’s profile &amp;gt; Edit &amp;gt; Balance. This section is only visible after at least one payment is made (1 star is enough)&lt;/p&gt;
  &lt;p id=&quot;6N5L&quot;&gt;Then wait until 1000 stars have accumulated for 21 days and withdraw them to your TON wallet via Fragment. The process is pretty straightforward and can be done via 1 button. &lt;a href=&quot;https://telegram.org/blog/mini-app-bar-paid-media-and-more#toncoin-rewards&quot; target=&quot;_blank&quot;&gt;More details. &lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;5oXY&quot;&gt;You can only withdraw after 3 weeks following the stars payment. This feature is likely designed for users who would like to request a refund from you for the stars payment.&lt;br /&gt;&lt;br /&gt;1 star is always worth $0.013, regardless of current TON exchange rates. However once you exchange stars for TON, the $ price of your earnings will be affected by the current exchange rate.&lt;/p&gt;
  &lt;h3 id=&quot;5aau&quot;&gt;Why did you use real payments for testing?&lt;/h3&gt;
  &lt;p id=&quot;eSUT&quot;&gt;Because it&amp;#x27;s much easier than setting up a separate Telegram test account. 50 Telegram stars is around $1, and you can still refund them.&lt;/p&gt;
  &lt;h3 id=&quot;xmlI&quot;&gt;Any tips for production?&lt;/h3&gt;
  &lt;p id=&quot;sWzl&quot;&gt;The article shows a simple example so you can get the basic idea. For production, I&amp;#x27;d suggest a real database like PostgreSQL with 2 separate entities:&lt;/p&gt;
  &lt;ul id=&quot;218l&quot;&gt;
    &lt;li id=&quot;3dG3&quot;&gt;&lt;em&gt;Product&lt;/em&gt; with fields like name, price&lt;/li&gt;
    &lt;li id=&quot;x2Py&quot;&gt;&lt;em&gt;Order&lt;/em&gt; with fields like user_id (1-to-many), status (&amp;#x27;initial&amp;#x27;, &amp;#x27;paid&amp;#x27;, &amp;#x27;failed&amp;#x27;), product_id, product_price, product_name, telegram_payload for storing the Telegram data needed for refunds. Why denormalize the product&amp;#x27;s price and name? Because they may change in the future, and you want to save the real value at the moment of purchase to avoid confusion about how much users paid in the past. It&amp;#x27;s a common practice in such applications.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;punO&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;tgmv&quot;&gt;About the author&lt;/h3&gt;
  &lt;p id=&quot;GYLY&quot;&gt;I am Egor, full-stack developer and a &lt;a href=&quot;https://github.com/kubk&quot; target=&quot;_blank&quot;&gt;contributor&lt;/a&gt; to many popular open source libraries. I&amp;#x27;ve built &lt;a href=&quot;https://memocard.org&quot; target=&quot;_blank&quot;&gt;MemoCard&lt;/a&gt; - Award-winning Telegram mini app for improving memory with spaced repetition and AI. Give it a shot, it also accepts Telegram Stars.&lt;/p&gt;

</content></entry><entry><id>alteregor:memocard-telegram-contest-win</id><link rel="alternate" type="text/html" href="https://teletype.in/@alteregor/memocard-telegram-contest-win?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=alteregor"></link><title>How I built a project for myself and won a prize from Telegram</title><published>2024-04-28T07:33:59.882Z</published><updated>2024-10-13T05:50:01.913Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/4e/d9/4ed915de-67ca-49ec-9345-5cc11fe45996.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img3.teletype.in/files/a1/ed/a1ed8576-ff2c-4b93-bd00-5c0e81a88021.png&quot;&gt;It's the translation of my own article initially published on habr.com that gained over 20000 views.</summary><content type="html">
  &lt;figure id=&quot;1PXC&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a1/ed/a1ed8576-ff2c-4b93-bd00-5c0e81a88021.png&quot; width=&quot;2018&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;C2Lt&quot;&gt;&lt;em&gt;It&amp;#x27;s the translation of my own article initially published on &lt;a href=&quot;https://habr.com/ru/articles/779508/&quot; target=&quot;_blank&quot;&gt;habr.com &lt;/a&gt;that gained over 20000 views.&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;XRBL&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;Pnvt&quot;&gt;Hello everyone! Telegram held a contest for the development of mini-apps, where my work won a prize. The competition was intense, as the choice of technologies and ideas for the application was free. Due to this, Telegram even increased the total value of the prizes. In this article, we&amp;#x27;ll take a look at how the development process went, what difficulties I faced, and what I learned.&lt;/p&gt;
  &lt;h3 id=&quot;B4Eh&quot;&gt;&lt;/h3&gt;
  &lt;h3 id=&quot;5Ew4&quot;&gt;&lt;strong&gt;About the contest&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;QK6k&quot;&gt;Telegram allows you to embed mini-applications in the messenger. An example of such an application is &lt;a href=&quot;https://wallet.tg/&quot; target=&quot;_blank&quot;&gt;Wallet&lt;/a&gt; - a tool for storing and exchanging cryptocurrency within the messenger.&lt;/p&gt;
  &lt;p id=&quot;DLNH&quot;&gt;The task was to develop a useful mini-application, place code on GitHub. The application was evaluated both from the user&amp;#x27;s point of view and from the developer&amp;#x27;s point of view. According to the competition rules, it was necessary to create your own application, not to follow the provided design and instructions. This required not only working on the code but also on the idea and creating the user interface.&lt;/p&gt;
  &lt;p id=&quot;IM0j&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;ICJQ&quot;&gt;&lt;strong&gt;Why participate in contests?&lt;/strong&gt;&lt;/h3&gt;
  &lt;ul id=&quot;wNL3&quot;&gt;
    &lt;li id=&quot;f1Px&quot;&gt;Testing your competitiveness by independent evaluators. The contest doesn&amp;#x27;t care about your job title or how much work experience you have.&lt;/li&gt;
    &lt;li id=&quot;7ADB&quot;&gt;If you&amp;#x27;ve gotten into a comfort zone while solving familiar tasks at work, a contest is a great way to try something new.&lt;/li&gt;
    &lt;li id=&quot;3lqD&quot;&gt;A special feature of this contest is the opportunity to earn money by creating your own project from scratch, from idea to implementation.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;kZia&quot;&gt;&lt;/h2&gt;
  &lt;h3 id=&quot;sHZx&quot;&gt;&lt;strong&gt;Idea for the bot&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;CRhv&quot;&gt;People forget information if they don&amp;#x27;t repeat it. Memory follows certain patterns, as identified by the scientist Ebbinghaus.&lt;/p&gt;
  &lt;figure id=&quot;pbdt&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8e/2e/8e2eb94b-e0ec-45df-9cd2-f88861b51e6d.png&quot; width=&quot;1118&quot; /&gt;
    &lt;figcaption&gt;The forgetting curve by Ebbinghaus&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Nqko&quot;&gt;The concept is simple but powerful: learning and memorization will be better if it&amp;#x27;s distributed over time. This is where spaced repetition comes in — a method of memorizing information based on regularly repeating the material at certain intervals. The method is suitable for memorizing various materials, including words, texts, facts, and language grammar rules.&lt;/p&gt;
  &lt;p id=&quot;eQMt&quot;&gt;For spaced repetition, I use the Anki program. You create decks, add cards to them, and the app suggests repeating only the information that requires repetition. It&amp;#x27;s a good tool, but I&amp;#x27;ve found a number of significant drawbacks:&lt;/p&gt;
  &lt;ul id=&quot;qgcJ&quot;&gt;
    &lt;li id=&quot;GVhX&quot;&gt;A lot of useful functionality in Anki is only available in the form of plugins. Plugins only work in the desktop version, which makes it inconvenient to repeat from your phone. Examples of important plugins are automatic card voicing to hear pronunciation, Heatmap for tracking progress in the style of GitHub, and the ability to go through all the decks at once without having to switch between them.&lt;/li&gt;
    &lt;li id=&quot;iw3k&quot;&gt;Anki doesn&amp;#x27;t have automatic push notifications for repetition reminders. Because of this, people simply forget to use it. Setting an alarm for every day is not the best idea, since there aren&amp;#x27;t cards to repeat every day. I would like to receive smart notifications that only notify me when there is a need.&lt;/li&gt;
    &lt;li id=&quot;ppvz&quot;&gt;Subjective opinion — Anki simply lacks a human touch. You need to configure and customize it, instead of just using it. For this reason, my acquaintances abandoned this app.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;oZdU&quot;&gt;I&amp;#x27;ve long wanted to make an alternative for myself, but couldn&amp;#x27;t find the time. The contest provided enough motivation to get started.&lt;/p&gt;
  &lt;h2 id=&quot;GgXO&quot;&gt;&lt;/h2&gt;
  &lt;h3 id=&quot;CJMu&quot;&gt;&lt;strong&gt;General information about mini-apps&lt;/strong&gt;&lt;/h3&gt;
  &lt;figure id=&quot;gKiL&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/67/04/67047fce-e874-4f57-9a4b-1ef8d0bfbacc.png&quot; width=&quot;1572&quot; /&gt;
    &lt;figcaption&gt;How mini-apps work&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;wcfL&quot;&gt;Let&amp;#x27;s dive into each step of the scheme in detail.&lt;/p&gt;
  &lt;p id=&quot;Ka2m&quot;&gt;&lt;strong&gt;Step 1 - The Telegram client interacts with the mini-application frontend&lt;/strong&gt;. The Telegram client is any of the clients within which the mini-application runs. The mini-application is your website, publicly accessible with SSL configured. To make the messenger open this site, you need to create a bot through BotFather, get an API key, and specify which URL your bot will open. To do this, when communicating with BotFather, call /setmenubutton or go to Bot Settings &amp;gt; Menu Button to specify your URL.&lt;/p&gt;
  &lt;p id=&quot;QWqN&quot;&gt;&lt;strong&gt;Step 2 - The frontend interacts with the backend&lt;/strong&gt;. The frontend will interact with the backend to connect with the database. To get information about the user, the frontend must pass it to the server, and the backend must validate it. Validation is needed to prevent unauthorized access, so the user cannot forge the data when sending it to the backend.&lt;/p&gt;
  &lt;p id=&quot;Fxh4&quot;&gt;Here, a common question may arise - what&amp;#x27;s the benefit of mini-applications if the messenger simply opens my website? In fact, Telegram mini-applications offer many conveniences to both developers and users:&lt;/p&gt;
  &lt;ul id=&quot;lts4&quot;&gt;
    &lt;li id=&quot;L16k&quot;&gt;Authentication. A bot user is already logged in to Telegram, so you don&amp;#x27;t need to ask them to register. The developer doesn&amp;#x27;t need to develop or connect authentication or spend money on SMS.&lt;/li&gt;
    &lt;li id=&quot;CPeE&quot;&gt;Push notifications. They already work in Telegram, and the developer doesn&amp;#x27;t need to set them up.&lt;/li&gt;
    &lt;li id=&quot;jtXw&quot;&gt;The bot, like a website, doesn&amp;#x27;t need to be installed. Because it works inside Telegram, it&amp;#x27;s easy to share. Just send a link to the bot to your contacts, and they can use it. You can even share specific pages within the bot using the &amp;#x60;startapp&amp;#x60; parameter, for example, https://t.me/yourbot/app?startapp=product_id&lt;/li&gt;
    &lt;li id=&quot;oBsr&quot;&gt;A rich SDK that includes many helper elements: Main button, loader for the button, button to return to the previous screen, modal windows, QR code scanning. The full list is available in the &lt;a href=&quot;https://core.telegram.org/bots/webapps&quot; target=&quot;_blank&quot;&gt;documentation&lt;/a&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;Uqmo&quot;&gt;&lt;/h2&gt;
  &lt;h3 id=&quot;DYCc&quot;&gt;&lt;strong&gt;Technology Stack&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;15bL&quot;&gt;Many of my decisions were dictated by limited time. Therefore, I used familiar tools — TypeScript for the frontend and backend, React and Mobx libraries for the user interface. To save time on server setup, I used cloud technologies. Now I realize that this was the right decision, which helped me focus on developing useful functionality. Cloud services:&lt;/p&gt;
  &lt;ul id=&quot;wKw2&quot;&gt;
    &lt;li id=&quot;AWEV&quot;&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; for a Node.js full-stack application. This gave me a domain with a configured SSL certificate, automatic frontend and backend deployment after a &amp;#x27;git push&amp;#x27; to GitHub. To do this, it&amp;#x27;s enough to connect your repository in the Cloudflare Pages settings. The service provides 100,000 free API requests per day.&lt;/li&gt;
    &lt;li id=&quot;TKvP&quot;&gt;&lt;strong&gt;Supabase&lt;/strong&gt; — an Open-Source database built on top of PostgreSQL, with a user interface for editing both data and its structure. If necessary, Supabase allows the use of raw SQL queries. For me, this was critical, as previous attempts to use cloud databases like Firestore ended in failure due to the inability to use convenient SQL for complex calculations. The service also provides the free use of a cloud database, so I didn&amp;#x27;t need to host PostgreSQL myself.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;XeL0&quot;&gt;I quickly sketched out a database schema in the visual editor:&lt;/p&gt;
  &lt;figure id=&quot;PpLc&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/01/2a/012accec-5ff9-4273-8ebe-262592da846e.png&quot; width=&quot;1560&quot; /&gt;
    &lt;figcaption&gt;Supabase visual editor&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;7WJm&quot;&gt;Then, using the Supabase JavaScript SDK, you can send requests to save data to these tables. A pleasant surprise was that Supabase can generate TypeScript types based on the created schema, which further saved time:&lt;/p&gt;
  &lt;pre id=&quot;ClBk&quot;&gt;export interface Database {
  public: {
    Tables: {
      card_review: {
        Row: {
          card_id: number
          created_at: string
          ease_factor: number
          interval: number
          last_review_date: string
          user_id: number
        }
        Insert: {
          card_id: number
          created_at?: string
          ease_factor?: number
          interval: number
          last_review_date?: string
          user_id: number
        }
        Update: {
          card_id?: number
          created_at?: string
          ease_factor?: number
          interval?: number
          last_review_date?: string
          user_id?: number
        }&lt;/pre&gt;
  &lt;h2 id=&quot;3TEl&quot;&gt;&lt;/h2&gt;
  &lt;h3 id=&quot;U6Xn&quot;&gt;&lt;strong&gt;Development and Spaced Repetition Algorithm&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;ORmo&quot;&gt;The user interface of the mini-app consisted of 3 parts: a list of decks with cards, a form for creating and editing a deck, and a mode for learning/repeating cards.&lt;/p&gt;
  &lt;figure id=&quot;QIcx&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/20/46/204605de-502e-4863-939c-0e0c94a5639a.png&quot; width=&quot;1560&quot; /&gt;
    &lt;figcaption&gt;From left to right - the list of decks, editing a deck, repeating a deck&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;lhDt&quot;&gt;The essence of the spaced repetition algorithm:&lt;/p&gt;
  &lt;ol id=&quot;0u5U&quot;&gt;
    &lt;li id=&quot;mAm6&quot;&gt;Learning new material&lt;/li&gt;
    &lt;li id=&quot;UrYQ&quot;&gt;Repeating this material after a certain time (for example, after a day).&lt;/li&gt;
    &lt;li id=&quot;vkwK&quot;&gt;If the repetition was successful, the next repetition occurs after a longer time interval (for example, after a week).&lt;/li&gt;
    &lt;li id=&quot;7GWe&quot;&gt;If the repetition was not successful, the next repetition occurs after a shorter time interval (for example, after a few hours)&lt;/li&gt;
    &lt;li id=&quot;2O5X&quot;&gt;Repetition occurs until the information is memorized.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;I6o5&quot;&gt;To implement the algorithm, let&amp;#x27;s write a function. Let it take as input a card and the result of the pass (&amp;quot;remember&amp;quot;, &amp;quot;don&amp;#x27;t remember&amp;quot;), and return the next repetition time. In its simplest form, the algorithm should calculate the next repetition time of the card according to this scheme:&lt;/p&gt;
  &lt;figure id=&quot;ZliU&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/6d/eb/6debc1dd-d211-4577-807e-55bab87b01ff.png&quot; width=&quot;1738&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;6W3P&quot;&gt;We see that next to the card, we need to store an interval that will show how many days later the card needs to be repeated. What if the user forgot the card? This means that this interval needs to be reset to its initial value. Let&amp;#x27;s represent this in code:&lt;/p&gt;
  &lt;pre id=&quot;sZa3&quot; data-lang=&quot;typescript&quot;&gt;export type Result = {
  newInterval: number;
};

export type ReviewOutcome = &amp;quot;correct&amp;quot; | &amp;quot;wrong&amp;quot;;
const easeFactor = 2.5;
const startInterval = 0.4;

export const reviewCard = (
  currentInterval: number,
  reviewOutcome: ReviewOutcome,
): Result =&amp;gt; {
  const newInterval = reviewOutcome === &amp;quot;correct&amp;quot;
    ? newInterval * easeFactor
    : startInterval;

  return {
    newInterval: parseFloat(newInterval.toFixed(2)),
  };
};&lt;/pre&gt;
  &lt;p id=&quot;amS7&quot;&gt;However, this algorithm has a drawback. If the user forgets a card, it means that it is more difficult for them than others. Therefore, this word should be offered more frequently than words that the user has never forgotten. In the code, this means that for a word that the user forgets several times, the interval should grow more slowly. For example, like this:&lt;/p&gt;
  &lt;figure id=&quot;PKoP&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/7a/fd/7afd9eac-3c9f-425a-a825-da3e28503df1.png&quot; width=&quot;1744&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;6z1w&quot;&gt;Pay attention that at the end, we answered &amp;quot;Remember&amp;quot; 3 times in a row, but the interval grows more slowly than in the first example. To achieve this, we will also store in the card the coefficient by which we multiply the interval. With an incorrect answer, the coefficient decreases, which slows down the growth of the interval. With a correct answer, the coefficient increases, which increases the growth of the interval. We will also introduce an upper and lower limit for the coefficient to ensure that the cards do not appear too frequently or too rarely:&lt;/p&gt;
  &lt;pre id=&quot;Jys2&quot; data-lang=&quot;typescript&quot;&gt;export type Result = {
  interval: number;
  easeFactor: number;
};

export type ReviewOutcome = &amp;quot;correct&amp;quot; | &amp;quot;wrong&amp;quot;;

const startEaseFactor = 2.5;
const startInterval = 0.4;
const easeFactorDecrement = 0.15;
const minimumEaseFactor = 1.3;
const easeFactorIncrement = 0.1;

export const reviewCard = (
  interval: number | undefined = startInterval,
  reviewOutcome: ReviewOutcome,
  easeFactor: number | undefined = startEaseFactor
): Result =&amp;gt; {
  if (reviewOutcome === &amp;quot;correct&amp;quot;) {
    interval = interval * easeFactor;
    easeFactor = Math.min(easeFactor + easeFactorIncrement, startEaseFactor);
  } else if (reviewOutcome === &amp;quot;wrong&amp;quot;) {
    easeFactor = Math.max(easeFactor - easeFactorDecrement, minimumEaseFactor);
    interval = startInterval;
  }

  return {
    easeFactor: parseFloat(easeFactor.toFixed(2)),
    interval: parseFloat(interval.toFixed(2)),
  };
};&lt;/pre&gt;
  &lt;p id=&quot;mf9U&quot;&gt;After saving the result to the database, we can use an SQL query to calculate the cards that require repetition at the current moment:&lt;/p&gt;
  &lt;pre id=&quot;akFj&quot; data-lang=&quot;sql&quot;&gt;-- Cards that needs to be repeated
SELECT cr.card_id,
       dc.deck_id AS deck_id
FROM card_review cr
INNER JOIN deck_card dc ON dc.id = cr.card_id
INNER JOIN deck d ON d.id = dc.deck_id
WHERE user_id = usr_id
  AND cr.last_review_date + (cr.interval::text || &amp;#x27; days&amp;#x27;)::INTERVAL &amp;lt; now()
-- Add new cards from decks to which the user has access.
UNION
SELECT c.id,
       ud.deck_id AS deck_id
FROM user_deck ud
LEFT JOIN deck_card c ON c.deck_id = ud.deck_id
LEFT JOIN card_review cr ON cr.card_id = c.id
AND cr.user_id = usr_id
WHERE ud.user_id = usr_id
  AND card_id IS NULL&lt;/pre&gt;
  &lt;h2 id=&quot;PlcD&quot;&gt;&lt;/h2&gt;
  &lt;h3 id=&quot;2isz&quot;&gt;&lt;strong&gt;Card Voiceover&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;L7su&quot;&gt;For learning words, it is useful to hear how they are correctly pronounced. For example, in English, there are different pronunciations - American and British. In tonal languages like Thai, you may not be understood at all due to incorrect tone, even if you know how to write the word. Card voiceover is available in Anki only as a plugin, but I have long wanted to try the Speech Synthesis API built into browsers. It allows generating speech in different languages, including taking into account the accent. It is supported by 95% of users and it is very easy to work with:&lt;/p&gt;
  &lt;pre id=&quot;1bJa&quot; data-lang=&quot;typescript&quot;&gt;function speak(text, lang) {
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.lang = lang;
  window.speechSynthesis.speak(utterance);
}

// In German
speak(&amp;quot;Guten Tag, wie geht es Ihnen?&amp;quot;, &amp;quot;de-DE&amp;quot;);

// In Spanish
speak(&amp;quot;Hola, ¿cómo estás?&amp;quot;, &amp;quot;es-ES&amp;quot;);

// In French
speak(&amp;quot;Bonjour, comment ça va?&amp;quot;, &amp;quot;fr-FR&amp;quot;);&lt;/pre&gt;
  &lt;figure id=&quot;C3ui&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/82/1e/821e20d9-86f8-4e97-8812-07fd5e929252.png&quot; width=&quot;1400&quot; /&gt;
    &lt;figcaption&gt;Voiceover language selection&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;I9ja&quot;&gt;&lt;strong&gt;Challenges&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;2CRV&quot;&gt;&lt;strong&gt;Challenge 1&lt;/strong&gt; - &lt;strong&gt;Animation.&lt;/strong&gt; During card repetition, the user decides whether they remember the card or not. It was decided to make the choice of &amp;quot;yes&amp;quot; or &amp;quot;no&amp;quot; using finger swiping. Swiping right would mean that the user remembers the card, swiping left - does not remember. The motivation was that the Telegram interface is well-animated, so I wanted to stand out. This was a mistake - not only did I spend a lot of time synchronizing finger movements and cards, but it also led to problems in the mini-app environment. In mobile clients, swiping left was recognized as swiping down, which unintentionally closed the mini-app. In the contest participants&amp;#x27; chat, I learned that this is a common problem and developers use different workarounds, the reliability of which is questionable. I decided not to complicate things and just made 2 buttons - &amp;quot;I got it right&amp;quot; and &amp;quot;Need to review&amp;quot; similar to Anki. The lesson for contests is to first make the important functionality, and only then add details if possible.&lt;/p&gt;
  &lt;p id=&quot;Wzq7&quot;&gt;&lt;strong&gt;Challenge 2&lt;/strong&gt; - &lt;strong&gt;Authentication.&lt;/strong&gt; The Telegram SDK passes basic user information to the frontend in the form of global JavaScript variables. However, blindly trusting them on the backend is not safe - an attacker can spoof this data and impersonate another person. Therefore, user data needs to be validated on the backend. Along with the user information, Telegram generates a hash of this information, signed with your bot token. On the backend, you need to recalculate the hash and if they match, then the data can be trusted.&lt;/p&gt;
  &lt;p id=&quot;tmKc&quot;&gt;It was a surprise that to work with cryptography in Cloudflare Pages, you need to use the Web Crypto API instead of Node.js modules. The fact is that Cloudflare Workers operate in a unique environment that is neither a browser nor a Node.js server environment. The code runs on the Cloudflare network, whose runtime environment resembles a web browser&amp;#x27;s Service Worker. I was not familiar with this API, so I had to spend time writing a hash validation module instead of using a ready-made library. In order not to complicate the application with JWT tokens like other participants, it was decided to simply pass user data on each request in the form of an HTTP header:&lt;/p&gt;
  &lt;pre id=&quot;evc7&quot; data-lang=&quot;typescript&quot;&gt;const response = await fetch(endpoint, {
  method,
  body: bodyAsString,
  headers: {
    hash: WebApp.initData,
  },
});&lt;/pre&gt;
  &lt;p id=&quot;uG9l&quot;&gt;Next, the backend validates this data and either creates or updates the user information in the database.&lt;/p&gt;
  &lt;p id=&quot;LZFt&quot;&gt;&lt;strong&gt;Challenge 3&lt;/strong&gt; - &lt;strong&gt;How to share decks.&lt;/strong&gt; It seemed like a good idea to me to share decks with someone. For example, you can go through a deck with someone, or a teacher can share decks on a specific topic with students. Telegram allows you to link to a specific entity inside a mini-app using the &lt;code&gt;startapp&lt;/code&gt; parameter. Example link: &lt;code&gt;https://t.me/memo_card_bot/app?startapp=&amp;lt;deck_id&amp;gt;&lt;/code&gt;&lt;/p&gt;
  &lt;p id=&quot;4ojo&quot;&gt;From the documentation, I learned that in Telegram there is an option to request a list of contacts to send text. If the user wants to share something, they can click on a button that will open a list of contacts. However, this button only worked in Telegram mobile clients. I found the same problem in the bots of other participants. However, thanks to a search, a workaround was found - if you open a link like &lt;code&gt;https://t.me/share/url?text=&amp;amp;url=url&lt;/code&gt; through a mobile app, Telegram will recognize it and offer a list of contacts to send to. Thus, the full link looks like this: &lt;code&gt;https://t.me/share/url?text=&amp;amp;url=https://t.me/memo_card_bot/app?startapp=&amp;lt;deck_id&amp;gt;&lt;/code&gt;&lt;/p&gt;
  &lt;figure id=&quot;qT4g&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/26/4b/264b6076-a060-492a-b50f-f40e7b0bcc68.png&quot; width=&quot;1404&quot; /&gt;
    &lt;figcaption&gt;How the deck sharing works&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;GZz5&quot;&gt;Unfortunately, desktop clients open this link in a browser, which requires an extra click to return to the client. But this is the only approach that works in both desktop and mobile clients.&lt;/p&gt;
  &lt;h3 id=&quot;RGNB&quot;&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;P0QG&quot;&gt;The contest conditions stated that works with bugs could be penalized or even excluded from the contest. To check the correctness of individual parts of the code, I used unit testing. This made it faster to detect errors after changes. I consider the use of such tests acceptable even under time constraints. Especially if you need to test code that performs date calculations: it&amp;#x27;s easier to run a test than to wait for the right day to come to test the algorithm.&lt;/p&gt;
  &lt;p id=&quot;sWxm&quot;&gt;On the frontend, thanks to the separation of business logic from presentation, I was able to quickly cover the functionality with unit tests, because isolated code is easy to test. An example of a Mobx store test:&lt;/p&gt;
  &lt;pre id=&quot;dzKs&quot; data-lang=&quot;typescript&quot;&gt;it(&amp;quot;basic review&amp;quot;, () =&amp;gt; {
  const reviewStore = new ReviewStore();
  reviewStore.startDeckReview(deckCardsMock);
  expect(reviewStore.isFinished).toBeFalsy();

  reviewStore.open();
  expect(reviewStore.currentCard?.isOpened).toBeTruthy();
  reviewStore.changeState(CardState.Remember);

  expect(reviewStore.isFinished).toBeFalsy();
  expect(reviewStore.currentCard?.id).toBe(4);

  reviewStore.open();
  reviewStore.changeState(CardState.Forget);

  expect(reviewStore.isFinished).toBeFalsy();

  reviewStore.open();
  reviewStore.changeState(CardState.Remember);
  expect(reviewStore.isFinished).toBeFalsy();
  expect(reviewStore.cardsToReview).toHaveLength(1);

  reviewStore.open();
  reviewStore.changeState(CardState.Remember);
  expect(reviewStore.isFinished).toBeTruthy();

  expect(reviewStore.result.forgotIds).toEqual([4]);
  expect(reviewStore.result.rememberIds).toEqual([3, 5]);
});&lt;/pre&gt;
  &lt;p id=&quot;jFK9&quot;&gt;I also organized usability testing with minimal effort - I gave the application to a person outside IT and watched how they used it, without my hints. My wife turned out to be an excellent assistant in this matter.&lt;/p&gt;
  &lt;h3 id=&quot;u8JB&quot;&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;YNCU&quot;&gt;The development took 30 hours. The contest was ending at 23:59 Dubai time, but the results were announced only a few hours later. Due to the difference in time zones, some participants waited late into the night, while others woke up for the results at 4 am. Upon waking up, I ran excitedly to study the list of winners. The bot took second place, the prize was $1000. For developers, this is a small amount, but this is money for developing a completely own product, which can even be spent on its development. The plans are to continue developing the product - add statistics, image support, quick card adding through a browser extension, automatic card generation via ChatGPT.&lt;/p&gt;
  &lt;p id=&quot;7KVu&quot;&gt;During development, it was necessary to concentrate on the main thing, avoiding immersion in insignificant details: instead of complex card swiping - two simple buttons, instead of lengthy infrastructure setup - ready-made cloud services. It was an interesting contest in which it was necessary to be a one-man band - drawing a logo in Figma, writing markup, studying the mini-app SDK, designing the database structure, writing the API, and finding what annoys users the most when using it.&lt;/p&gt;
  &lt;p id=&quot;OWht&quot;&gt;Try it and you will definitely succeed.&lt;/p&gt;
  &lt;figure id=&quot;1e6B&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/6e/58/6e58de6b-16e5-4617-8c89-e372c60d6a06.png&quot; width=&quot;898&quot; /&gt;
    &lt;figcaption&gt;A letter from Telegram that I had to sign&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;vkg4&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;fUue&quot;&gt;Links&lt;/h3&gt;
  &lt;p id=&quot;fP6x&quot;&gt;Telegram bot - &lt;a href=&quot;https://t.me/memo_card_bot&quot; target=&quot;_blank&quot;&gt;https://t.me/memo_card_bot&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;YSJX&quot;&gt;Website: &lt;a href=&quot;https://memocard.org&quot; target=&quot;_blank&quot;&gt;https://memocard.org&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;ng3a&quot;&gt;GitHub repository - &lt;a href=&quot;https://github.com/kubk/memo-card&quot; target=&quot;_blank&quot;&gt;https://github.com/kubk/memo-card&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>alteregor:rkPlgmQz8</id><link rel="alternate" type="text/html" href="https://teletype.in/@alteregor/rkPlgmQz8?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=alteregor"></link><title>The difference between type and interface in TypeScript</title><published>2020-02-01T18:27:20.484Z</published><updated>2025-08-19T06:49:26.904Z</updated><category term="typescript" label="typescript"></category><summary type="html">Type and interface in TypeScript often confuse people because they look similar on the surface. The situation gets worse with outdated articles, biased comparisons, and style guides from some frameworks. For example, Angular has the tslint rule interface-over-type-literal enabled by default, which forces you to use interfaces instead of types wherever possible. In this article, we'll look at the difference between type and interface in TypeScript and figure out what you should actually use.</summary><content type="html">
  &lt;p id=&quot;RuY8&quot;&gt;Type and interface in TypeScript often confuse people because they look similar on the surface. The situation gets worse with outdated articles, biased comparisons, and style guides from some frameworks. For example, Angular has the tslint rule &lt;code&gt;interface-over-type-literal&lt;/code&gt; enabled by default, which forces you to use interfaces instead of types wherever possible. In this article, we&amp;#x27;ll look at the difference between type and interface in TypeScript and figure out what you should actually use.&lt;/p&gt;
  &lt;h2 id=&quot;k1Cn&quot;&gt;Similarities&lt;/h2&gt;
  &lt;p id=&quot;WL2r&quot;&gt;Interfaces and types can be used to describe data structures:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;BXl8&quot;&gt;type Employee = {
  salary: number;
}

// Or
interface Employee {
  salary: number;
}&lt;/pre&gt;
  &lt;p id=&quot;rbRf&quot;&gt;Can be used for typing functions:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;Yusb&quot;&gt;interface CalculateSalary {
  (employee: Employee): number; 
}

// Or

type CalculateSalary = (employee: Employee) =&amp;gt; number;&lt;/pre&gt;
  &lt;p id=&quot;EhZZ&quot;&gt;Interfaces and types can be implemented by classes:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;NopA&quot;&gt;type Employee = {
  giveEstimate(task: Task): number;
}

// Or

interface Employee {
  giveEstimate(task: Task): number;
}

class YoungDeveloper implements Employee {
  giveEstimate(task: Task): number {
    return task.complexity * 0.01;
  }
}

class MatureDeveloper implements Employee {
  giveEstimate(task: Task): number {
    return task.complexity * random(10, 1000) * Math.PI;
  }
}&lt;/pre&gt;
  &lt;p id=&quot;1XZx&quot;&gt;Interfaces and types allow expressing type intersections:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;CztR&quot;&gt;type TwitterProfile = Photographer &amp;amp; Musician &amp;amp; Entrepreneur &amp;amp; CoffeeDrinker;

// Or

interface TwitterProfile extends Photographer, Musician, Entrepreneur, CoffeeDrinker {};&lt;/pre&gt;
  &lt;h2 id=&quot;sJwr&quot;&gt;Difference 1 - Mapped Types&lt;/h2&gt;
  &lt;p id=&quot;oAtR&quot;&gt;Interfaces cannot be combined with mapped types (Required, Pick, Readonly, Partial, and others):&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;BApO&quot;&gt;// Works with type
type RealProfile = Pick&amp;lt;TwitterProfile, &amp;#x27;drinkCoffee&amp;#x27;&amp;gt;;

// Doesn&amp;#x27;t work with interface
interface RealProfile extends Pick&amp;lt;TwitterProfile, &amp;#x27;drinkCoffee&amp;#x27;&amp;gt; {};&lt;/pre&gt;
  &lt;p id=&quot;DhQv&quot;&gt;An interface can only extend interfaces, classes, or other types, so the code needs to be rewritten like this:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;9WVi&quot;&gt;type OnlyDrinksCoffee = Pick&amp;lt;TwitterProfile, &amp;#x27;drinkCoffee&amp;#x27;&amp;gt;;

interface RealProfile extends OnlyDrinksCoffee {}&lt;/pre&gt;
  &lt;p id=&quot;Kgvb&quot;&gt;For the same reason, only with a type can you require that all properties be mandatory or conversely optional::&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;3wB3&quot;&gt;type TraineeDeveloper = Partial&amp;lt;{
  salary: number;
  sleep: boolean;
  eat: boolean;
}&amp;gt;

// Can exist without salary, food, and sleep
const trainee: TraineeDeveloper = {} &lt;/pre&gt;
  &lt;h2 id=&quot;PP7v&quot;&gt;Difference 2 - Union&lt;/h2&gt;
  &lt;p id=&quot;ucFe&quot;&gt;Interfaces allow expressing type intersections, but don&amp;#x27;t allow expressing unions. Example of a constraint with types that&amp;#x27;s unrealizable with interfaces::&lt;/p&gt;
  &lt;pre id=&quot;6f9P&quot; data-lang=&quot;typescript&quot;&gt;type Wish = 
  | { fast: true, quality: true, cheap: false } // Expensive
  | { fast: true, quality: false, cheap: true } // Poor quality
  | { fast: false, quality: true, cheap: true } // Slow 
  
// Won&amp;#x27;t compile
const wish: Wish = { fast: true, quality: true, cheap: true }&lt;/pre&gt;
  &lt;h2 id=&quot;XpBy&quot;&gt;Difference 3 - Declaration merging&lt;/h2&gt;
  &lt;p id=&quot;MSFD&quot;&gt;Interfaces support declaration merging - merging interfaces with the same names:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;Rykk&quot;&gt;interface Employee {
  salary: number;
}

interface Employee {
  age: number;
}

// Compilation error, as salary wasn&amp;#x27;t provided
const employee: Employee = { age: 23 };&lt;/pre&gt;
  &lt;p id=&quot;3p38&quot;&gt;This feature can be used if third-party library typings are outdated and you need to extend the interface with missing properties and methods. If you&amp;#x27;re making your own library, then it&amp;#x27;s up to you whether to provide such extension points. If the library is in TypeScript - then you shouldn&amp;#x27;t, as the type declarations in the output will always be up-to-date and users won&amp;#x27;t need to fix discrepancies between types and runtime. Nowadays, more and more libraries are written directly in TypeScript or come with typings, so the need to use declaration merging arises less frequently. Additionally, not everything can be fixed in an outdated interface - you can&amp;#x27;t remove a property or change the type of an existing one::&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;U3q6&quot;&gt;interface Employee {
  salary: number;
}

interface Employee {
  salary?: number;
}

// Error, an employee still demands salary
const employee: Employee = {};&lt;/pre&gt;
  &lt;h2 id=&quot;Wl3X&quot;&gt;Difference 4 - Recursive types&lt;/h2&gt;
  &lt;p id=&quot;adSY&quot;&gt;Before TypeScript 3.7, there were differences in how recursive types and interfaces worked. With types, you couldn&amp;#x27;t type recursive structures, but in newer versions of the language, there&amp;#x27;s no problem. Details in the language release notes. Check &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; for more details.&lt;/p&gt;
  &lt;h2 id=&quot;gXQr&quot;&gt;Difference 5 - Compatibility with Record Type&lt;/h2&gt;
  &lt;p id=&quot;MQW1&quot;&gt;Because interface supports declaration merging (can be extended anywhere), it cannot be used where &lt;code&gt;Record&amp;lt;string, any&amp;gt;&lt;/code&gt; is expected. This can be a problem in situations where you need to use URLSearchParams or other browser APIs expecting &lt;code&gt;Record&amp;lt;string, any&amp;gt;&lt;/code&gt;:&lt;/p&gt;
  &lt;pre data-lang=&quot;typescript&quot; id=&quot;Ebdb&quot;&gt;interface Employee {
  name: string;
}

const employee: Employee = { name: &amp;#x27;&amp;#x27; }

new URLSearchParams(employee);&lt;/pre&gt;
  &lt;p id=&quot;kByz&quot;&gt;This code won&amp;#x27;t compile with an interface, but will work if you change the interface to a type.&lt;br /&gt;&lt;/p&gt;
  &lt;h2 id=&quot;s6Gd&quot;&gt;Conclusion&lt;/h2&gt;
  &lt;p id=&quot;ndxS&quot;&gt;Types are the more preferable option, as you can replace interfaces with types, but not vice versa. For using TypeScript&amp;#x27;s advanced functionality - mapped types, union types, and conditional types - interfaces won&amp;#x27;t work.&lt;/p&gt;

</content></entry></feed>