<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Bolarinwa Owuogba]]></title><description><![CDATA[Bolarinwa Owuogba]]></description><link>https://bolarinwaowuogba.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!vvch!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbolarinwaowuogba.substack.com%2Fimg%2Fsubstack.png</url><title>Bolarinwa Owuogba</title><link>https://bolarinwaowuogba.substack.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 07 Aug 2026 11:58:32 GMT</lastBuildDate><atom:link href="https://bolarinwaowuogba.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Bolarinwa Owuogba]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[bolarinwaowuogba@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[bolarinwaowuogba@substack.com]]></itunes:email><itunes:name><![CDATA[Bolarinwa Owuogba]]></itunes:name></itunes:owner><itunes:author><![CDATA[Bolarinwa Owuogba]]></itunes:author><googleplay:owner><![CDATA[bolarinwaowuogba@substack.com]]></googleplay:owner><googleplay:email><![CDATA[bolarinwaowuogba@substack.com]]></googleplay:email><googleplay:author><![CDATA[Bolarinwa Owuogba]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[ Using Linear as a Second Brain for Software Engineering (SWE)]]></title><description><![CDATA[Introduction]]></description><link>https://bolarinwaowuogba.substack.com/p/using-linear-as-a-second-brain-for</link><guid isPermaLink="false">https://bolarinwaowuogba.substack.com/p/using-linear-as-a-second-brain-for</guid><dc:creator><![CDATA[Bolarinwa Owuogba]]></dc:creator><pubDate>Fri, 05 Jun 2026 05:33:14 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="2592" height="1944" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1944,&quot;width&quot;:2592,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A red brain sitting on top of a metal tray&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A red brain sitting on top of a metal tray" title="A red brain sitting on top of a metal tray" srcset="https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1737719435022-7329822e7bd6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMzN8fGJyYWlufGVufDB8fHx8MTc4MDQ0MjUwMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@jobarick">Ibrahim Jonathan</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><h2><strong>Introduction</strong></h2><p>First thought that probably comes to your mind is <strong>&#8220;What&#8217;s wrong with his first brain?&#8221;</strong></p><p>To answer that, my first brain is fine (AFAIK). Despite the brain in my head working fine, I depend on a second one in the form of Linear as part of my workflow. Agentic programming changed SWE, any single engineer can effectively act as a whole team and the implications of that are massive.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bolarinwaowuogba.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>What used to be one engineer, working on one task for hours/days with full context in their heads and regular repetition to drill the details in memory is now multiple parallel tasks which need to be completed today. Compound this by the need to align with team members, review implementation, explain decisions etc, you quickly realize your memory cannot hold everything.</p><p>You can only hold so much in memory when handling multiple tasks. Chances are you forget alot - to test an edge case you considered, a specific reason behind an implementation decision, a compromise the team agreed to, the commit/PR where the feature started from, etc. Even if your memory was a <em>steel-strap</em>, you&#8217;re only one person, with a fixed amount of time/energy. If you held all context, across a growing number of implementations in your head alone, you would become a bottleneck to other team members who need context for their work. God forbid you fall sick and your business logic heavy service goes down.</p><h2><strong>My workflow</strong></h2><p>Any new work I start, begins with a clarification session between me and my favourite coding agent (Claude Code right now). During this <strong>we&#8217;re</strong> making sure <strong>I</strong> have enough context to create a solution - issue reproduction, high level feature scoping, clarification questions etc. Once this is clear, I ask Claude to create a issue on linear using the <strong>linear mcp</strong> tool based off this conversation.</p><p>Once the issue has been created, I work through it - create an implementation plan, review plan rigorously, implement, review implementation, get feedback, repeat as needed till I&#8217;m satisfied with the work that has been done. During this process I note decisions I and the team (via feedback conversations) made, insights that surfaced etc</p><p>Finally, when an implementation is ready to go out, I do my best (still working on it) to add those notes I took during implementation to the Linear issue, add screenshots from my local development environment if relevant and just try to put as much useful information as possible on Linear. Doing this dramatically reduces the <em>cost</em> of getting context on this work, both for other team members and future me.</p><p>Recently, I was wondering why an implementation I made was missing in production. Working with Claude, I was able to pull context from Linear, identifying the disconnect that led to that outcome in minutes!</p><p>That&#8217;s it. My process is still a work in progress but I can already see the benefits.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://bolarinwaowuogba.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The Marvelous Mechanics of EVM: A Kid's Guide to Ethereum's Engine Room]]></title><description><![CDATA[Introduction]]></description><link>https://bolarinwaowuogba.substack.com/p/the-marvelous-mechanics-of-evm-a-kids-guide-to-ethereums-engine-room</link><guid isPermaLink="false">https://bolarinwaowuogba.substack.com/p/the-marvelous-mechanics-of-evm-a-kids-guide-to-ethereums-engine-room</guid><dc:creator><![CDATA[Bolarinwa Owuogba]]></dc:creator><pubDate>Tue, 13 Jun 2023 09:12:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/bc9fad0a-a3ab-45cf-a1bf-a7954daac07f_1600x1067.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1><p>Have you ever played on a giant playground full of exciting games, where you can make your own rules and create your own adventures? Well, what if I told you there's a massive playground just like this, but it's hidden inside our computers and smartphones? This incredible playground is called Ethereum.</p><p>Ethereum is kind of like your favourite online game. You know, the one where you can play with friends, trade special items, and even build your own virtual world? Just like a bunch of friends playing an online game together, Ethereum is actually made up of many, many computers from all over the world, all playing together at the same time.</p><p>And the most exciting part? Each of these computers has its very own game console! In the world of Ethereum, we call this special game console the 'Ethereum Virtual Machine', or 'EVM' for short. Each EVM can run lots of different games, help players trade items, and even keep score!</p><p>So, are you ready to dive in and learn all about this incredible playground and its game consoles? Let's get started on our adventure in Ethereum, the biggest playground in the world inside our computers!</p><h1>1: The Game Consoles (EVM) in Every Computer</h1><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kxYn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kxYn!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!kxYn!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!kxYn!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!kxYn!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kxYn!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!kxYn!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!kxYn!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!kxYn!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!kxYn!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0225bd3-56d8-469f-b87d-e7082f4bbf9e_480x270.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>You know how when you're playing with your game console at home, you can play all sorts of different games, right? Maybe you like to race cars in one game or hunt for treasure in another. And sometimes, you just want to play a simple game like checkers.</p><p>Now, let's think about our giant playground called Ethereum. This playground is super cool because every computer connected to it has its own special game console! We call this unique game console the Ethereum Virtual Machine, or EVM for short.</p><p>Like your console at home, these EVMs can run lots of different games too. But the games they play are called 'smart contracts'. Each smart contract is like a different game, with its own unique rules and objectives. One smart contract might be like a treasure hunt game, while another one might be more like trading cards with your friends.</p><p>But that's not all! These game consoles (EVMs) also do simple tasks, just like your game console can do more than just run games. Have you ever sent a message to a friend through your console, or maybe traded a game item with them? The EVMs can do something similar. They can move virtual coins from one player's account to another, just like how you might send a friend some game items.</p><p>Isn't that amazing? Thousands and thousands of game consoles, all playing different games and also doing simple tasks, all at the same time! And even though the games might be different, and the consoles are in different computers, they all work together to make the Ethereum playground a fun and fair place for everyone.</p><h1>2: The Games We Play (Smart Contracts)</h1><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UIni!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UIni!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!UIni!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!UIni!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!UIni!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UIni!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!UIni!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!UIni!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!UIni!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!UIni!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e31cd22-70fd-49b1-b634-df0a6ffc693a_480x270.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>You know when you're playing a video game, how each game has its own rules and challenges? Maybe one game has you rescue a princess, and another has you build the tallest tower. Well, the Ethereum playground has games like that too, but we call them 'smart contracts'.</p><p>Each smart contract is a special kind of game with its own set of rules. For example, one smart contract might be a game where you can trade virtual pets with your friends. Another smart contract could be a game that helps you save your virtual coins.</p><p>And guess what? All the people using Ethereum are like players in this big playground. They use their game consoles (the EVMs we talked about) to play these smart contract games.</p><p>Just think about it - you want to trade a shiny virtual dragon for your friend's cool virtual unicorn. You'd go to the 'Pet Trading' smart contract game on your EVM console, and your friend would do the same on theirs. You follow the game's rules, and voila! You've got a cool new unicorn, and your friend now owns a shiny dragon.</p><p>But it's not just about trading pets. There are tons of different smart contract games that do lots of fun and helpful things. Some might let you start a virtual lemonade stand and keep track of how many customers you have. Others might let you send virtual birthday presents to your friends.</p><p>Fun stuff!</p><h1>3: Playground Rules (Protocols)</h1><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9Z6y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9Z6y!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!9Z6y!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!9Z6y!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!9Z6y!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9Z6y!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!9Z6y!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!9Z6y!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!9Z6y!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!9Z6y!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F378b5db6-542e-4d0c-93fd-68c24973410f_480x270.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Every fun and safe playground has rules, right? Rules like taking turns on the swings, or not running too fast to keep everyone safe. In our giant Ethereum playground, we have rules too! These rules are called 'protocols', and they're super important to make sure everyone plays fairly and has fun.</p><p>You can think of protocols like playground rules that tell everyone how to play the games. So, no matter what game you're playing on your game console (EVM), the protocols make sure the game works the same way for everyone. That way, whether you're trading virtual pets, saving virtual coins, or sending virtual gifts, you know the rules will be the same no matter where you are or who you're playing with.</p><p>And guess what? These rules, or protocols, are followed by every game console (EVM) in our Ethereum playground. Whether the console is in your computer, your friend's computer, or a computer on the other side of the world, they all follow the same rules. This is what makes the games fair, and what makes our Ethereum playground a great place for everyone to play in.</p><p>Just think about how amazing that is! Thousands and thousands of game consoles (EVMs), all playing different games (smart contracts), but all following the same playground rules (protocols). This makes sure the games are fun, fair, and work the same way for everyone in our Ethereum playground!</p><h1>4: Powering the Consoles (Gas)</h1><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fIAk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fIAk!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 424w, https://substackcdn.com/image/fetch/$s_!fIAk!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 848w, https://substackcdn.com/image/fetch/$s_!fIAk!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 1272w, https://substackcdn.com/image/fetch/$s_!fIAk!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fIAk!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!fIAk!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 424w, https://substackcdn.com/image/fetch/$s_!fIAk!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 848w, https://substackcdn.com/image/fetch/$s_!fIAk!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 1272w, https://substackcdn.com/image/fetch/$s_!fIAk!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee33bbfd-b9c1-4b20-ac83-8bf4b17daebb_500x281.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Do you remember how your game console at home needs power to work? You might have to plug it into a wall socket, or maybe it runs on batteries. Without power, you wouldn't be able to play any games. In our Ethereum playground, the game consoles (EVMs) need power too! But instead of electricity or batteries, they use something called 'Gas'.</p><p>Gas is like the power that keeps all the game consoles (EVMs) running and the games (smart contracts) playing. But Gas isn't just about keeping things running; it also makes sure that everyone plays fair. By needing Gas to play a game or do something on Ethereum, it stops anyone from hogging all the game time.</p><p>Imagine if someone tried to play all the games in the playground without stopping, leaving no time for anyone else. It wouldn't be much fun, right? Well, Gas prevents that from happening in our Ethereum playground. Since everyone needs Gas to play the games (run smart contracts), it makes sure that everyone gets a turn, and that the game consoles (EVMs) don't get too tired.</p><p>Just like how a big, complex video game might use up more of your console's power, some tasks or 'games' in our Ethereum playground need more gas to run. That means when someone wants to do something big or complex in Ethereum, they need to pay more gas.</p><p>For example, if you're playing a simple game like checkers, your game console might not need much power. But what if you're playing a big, exciting adventure game with lots of characters and levels? That might use up a lot more power!</p><p>In the same way, if you're doing something simple on the Ethereum playground, like sending your friend a virtual coin, it might not need a lot of Gas. But if you're playing a more complex 'game', like trading a hundred different virtual pets all at once, that might need a lot more Gas! And because it's a bigger task, you would need to pay more Gas to get it done. <a href="https://rinwa.hashnode.dev/understanding-gas-in-ethereum-and-how-to-optimize-smart-contracts">You can read more on gas here.</a></p><p>So, the next time you're playing a game on your console at home, think about how it needs power to run. And remember, in the Ethereum playground, all the game consoles (EVMs) and the games (smart contracts) they run also need power - that power is Gas! And the more complex the game, the more Gas you'll need to pay to play!</p><h1>5: Remembering the Games (Blockchain)</h1><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Re_Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Re_Z!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!Re_Z!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!Re_Z!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!Re_Z!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Re_Z!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!Re_Z!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 424w, https://substackcdn.com/image/fetch/$s_!Re_Z!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 848w, https://substackcdn.com/image/fetch/$s_!Re_Z!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 1272w, https://substackcdn.com/image/fetch/$s_!Re_Z!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30057514-1893-471b-a8cb-2f0640b6ef30_480x270.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Imagine you're in a giant game arcade, full of many different game consoles. You and your friends are playing games, and there's a big scoreboard that keeps track of all the scores. But in this special arcade, whenever a game is played on one console, all the other consoles in the arcade play the same game too, to double-check the score. This is a bit like how Ethereum works!</p><p>In Ethereum's big playground, whenever a game (transaction or smart contract) is played on one game console (EVM), all the other game consoles (EVMs) in the playground run the same game too. They're all double-checking each other to make sure that every game is played correctly and the scores (transactions) are right.</p><p>This might sound like a lot of work, but it's very important because it makes sure everything is fair and nobody is cheating. Just like how referees in a game make sure that all players are following the rules, in Ethereum, all the game consoles (EVMs) are making sure every game is played correctly.</p><p>But how do all the consoles agree on the scores? Well, after all the game consoles have run the same game, they talk to each other. They all agree on what the scores are and write them down in their copy of the big game diary (blockchain). This process is called consensus, and it's a bit like when you and your friends agree on what the score is after a game.</p><p>So, the Ethereum blockchain is like a giant scoreboard or game diary that keeps track of all the games that have been played in the playground. But it's not just any diary; it's a diary that every console in the playground helps write and agree on! This keeps our playground fair, fun, and open for everyone to enjoy.</p><h1>Conclusion: Adventure in the Ethereum Playground</h1><p>Let's recap what we've learned about our amazing Ethereum playground:</p><ul><li><p>Ethereum is like a gigantic playground that's not in a park or a garden but inside our computers and smartphones!</p></li><li><p>Inside this playground, each computer has its very own game console called the Ethereum Virtual Machine, or EVM for short.</p></li><li><p>These EVMs are incredible! They can:</p><ul><li><p>Play lots of different games, each with its own rules and challenges. These games are known as 'smart contracts'.</p></li><li><p>Perform simple tasks, like moving virtual coins from one player to another.</p></li></ul></li><li><p>Just like a game console at home needs power to work, the EVMs also need power. In Ethereum, this power is called 'Gas'.</p><ul><li><p>Gas not only keeps the EVMs running and the games playing but also makes sure everyone gets a turn to play.</p></li><li><p>The more complex the game (smart contract), the more Gas it needs.</p></li></ul></li><li><p>Lastly, let's not forget about our giant game diary, the blockchain. This diary keeps track of every game played and every score made in our Ethereum playground.</p><ul><li><p>All the EVMs help write this diary and agree on what's written. This keeps the playground fair and fun for everyone!</p></li></ul></li></ul><p>Isn't it exciting to think about? A massive playground inside our computers, filled with game consoles (EVMs) playing different games (smart contracts), all powered by Gas and all following the same playground rules!</p><p>But our Ethereum adventure doesn't end here. There's so much more to discover and explore:</p><ul><li><p>You can find new games (smart contracts).</p></li><li><p>You could create your own games (smart contracts).</p></li><li><p>You're not just a player in the Ethereum playground; you're also part of creating this world.</p></li></ul><p>So, keep exploring, keep learning, and most importantly, keep having fun in the world inside our computers - Ethereum!</p>]]></content:encoded></item><item><title><![CDATA[From Traditional Exchanges to AMMs: A Shift in Liquidity Management]]></title><description><![CDATA[Photo by Shubham Dhage on Unsplash]]></description><link>https://bolarinwaowuogba.substack.com/p/from-traditional-exchanges-to-amms-a-shift-in-liquidity-management</link><guid isPermaLink="false">https://bolarinwaowuogba.substack.com/p/from-traditional-exchanges-to-amms-a-shift-in-liquidity-management</guid><dc:creator><![CDATA[Bolarinwa Owuogba]]></dc:creator><pubDate>Sat, 27 May 2023 11:52:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d835d063-32e7-4795-9759-2103d2e0c072_3840x2160.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Photo by <a href="https://unsplash.com/@theshubhamdhage?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Shubham Dhage</a> on <a href="https://unsplash.com/s/photos/crypto-trade?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p><h1>1. Introduction</h1><p>Today, my friends, we're diving into the mind-boggling world of Automatic Market Makers, or as the cool kids say, AMMs. Now, you might be scratching your head, going, "What the hell is all this about?" Well, hold onto your seats because we're about to demystify the whole damn thing!</p><p>In the simplest terms, AMMs are like your friendly neighbourhood shopkeepers, but for the cryptocurrency world. They're always there, waiting with open arms, ready to trade any combination of assets at any time of the day (or night). They don't judge or discriminate - whether you want to swap Ether for DAI, BAT for USDT, or any other trade you can dream up, they're there to help make it happen.</p><p>But how do they do it? Well, they utilize a mathematical formula to automatically determine the price of trades. No haggling, no lengthy negotiations, just quick and straightforward transactions. Sweet, right?</p><p>So where do AMMs fit in the grand scheme of things, you might ask? AMMs are dead centre in the world of decentralized exchanges, they've brought a revolutionary change to asset trading.</p><p>Before AMMs, trading could be a bit of a drag. You needed a buyer for every seller, a matching pair for every trade. Now, with AMMs, that's a thing of the past. They act as a constant party on the other side of the trade, providing liquidity and enabling trades to happen smoothly. Think of them as the fuel that keeps the DeFi engine running!</p><p>But don't worry, we'll dive deeper into all these topics in the coming sections. Ready?</p><h1>2. The Problem with Traditional Exchanges</h1><p>Let's imagine you're in a bustling marketplace. The air is filled with the noise of people haggling, cash registers ringing, goods exchanging hands - it's a vivid picture of supply meeting demand. This is what traditional exchanges are like. They operate on an order book model, where buyers and sellers list their desired prices and quantities. When a buyer's price matches a seller's, voila, a trade occurs!</p><p>Allow me to illustrate:</p><p><em>In a bustling marketplace, imagine you're a vendor with apples priced at $1 each, while a buyer is bidding $0.90 per apple. The mismatch, known as the 'spread', prevents a trade. However, a new buyer enters, willing to match your $1 ask price, and the trade occurs. This scenario epitomizes traditional exchanges operating on an order book model, where trades are based on matching a buyer's bid price with a seller's ask</em> <em>price, creating the dynamic and lively environment of the marketplace or financial exchange.</em></p><p>While this system has been serving us pretty well for quite a while, it does have its quirks and hitches. Centralized exchanges such as Binance, Coinbase, KuCoin and so on, for example, have often been likened to traditional banks. They act as middlemen, holding users' assets and managing trades. This centralized model, however, has several drawbacks. It's vulnerable to hacks and other security issues. It can also be a bottleneck during peak trading times, slowing down transactions and affecting the overall user experience.</p><p>On the other hand, decentralized exchanges (DEXs) eliminate the middleman, allowing peer-to-peer trades using smart contracts. This gives users more control over their assets and enhances privacy. But traditional DEXs also face challenges. Liquidity can be a major issue, especially for less popular tokens. If there's not enough interest on both sides of a trade, it could be tough to get your transactions through.</p><p>Here's where AMMs come in. Remember our marketplace analogy? Well, imagine now a marketplace that never sleeps, where you always find a willing buyer or seller, regardless of how rare or popular your goods are. Sounds amazing, right? This is the kind of environment AMMs create.</p><p>AMMs turn the concept of an order book on its head. They do this in two ways:</p><ol><li><p>Instead of waiting for a buyer's price to match a seller's, they use a pre-set formula to determine the price of a trade, no matter when you want to make it. You could think of AMMs as vending machines. They always have a set price for their goods, ready to trade anytime, any day!</p></li><li><p>AMMs allow anyone to become a liquidity provider by depositing assets into a pool. In return, these liquidity providers earn transaction fees, incentivizing more people to contribute and, thereby, solving the liquidity problem we often see in traditional DEXs.</p></li></ol><p>To sum it up, AMMs address the limitations of traditional exchanges by offering constant liquidity and enabling peer-to-peer trades without the need for an order book.</p><h1>3. Understanding Liquidity Pools</h1><p>Imagine a big, community chest where people throw in their spare coins. It's always available, and you can take coins out whenever you want, as long as you replace them with something of equal value. This, my friends, is a liquidity pool in a nutshell - a shared pot of tokens that powers an AMM.</p><p>Here's how it works. In an AMM, you're not directly trading with another person. Instead, you're trading with a smart contract, a piece of code that runs on a blockchain network (like Ethereum). This contract, powered by the liquidity pool, will always take your trade, no matter the hour or the demand.</p><p>The size and composition of this pool matter a lot. A larger pool means more liquidity, which in turn means you can make bigger trades without significantly affecting the price. In the world of DeFi, we love this thing called 'slippage' as much as a cat loves taking a bath. Slippage refers to the difference between the expected price of a trade and the price at which it actually executes. More liquidity equals less slippage and less slippage equals happier traders!</p><p>Now, where does this pool come from? Well, that's where our unsung heroes - the Liquidity Providers (LPs) - step in. LPs are regular folks like you and me who deposit their tokens into these pools. In essence, they're saying, "Here you go, AMM! Use my tokens to facilitate trades."</p><p>But why would anyone want to lend their precious tokens to a smart contract? What's in it for them? Well, by becoming LPs, people can earn passive income in the form of transaction fees. Every time someone makes a trade, a tiny fraction of it is taken as a fee, which then gets distributed among the LPs. It's like a reward for keeping the DeFi machine well-oiled and running.</p><p>However, providing liquidity isn't all sunshine and rainbows. It comes with its own set of risks, such as impermanent loss, price slippage, and so on.</p><h1>4. The Constant Product Market Maker Model</h1><p>Remember AMMs use a mathematical formula to automatically determine the price of trades? While there are variations of this formula, the constant thing is they make sure the total assets locked for trading remain constant.</p><p>This group of functions are referred to as constant function market makers (CFMMs), examples include: Constant Product Market Maker (CPMM), Constant Sum Market Maker (CSMM), Constant Mean Market Maker (CMMM).</p><p>Now, onto one of the most widely used mechanisms - the Constant Product Market Maker Model (CPMM).</p><p>So what's this fancy term, you ask? It's simpler than it sounds.</p><p>Imagine a seesaw, perfectly balanced. As one side goes up, the other side goes down. That&#8217;s basically what the constant product formula does, but with token prices instead of kids on a playground.</p><p>Let's break it down. The Constant Product Market Maker Model is based on a simple formula: <code>x*y=k</code>, where <code>x</code> and <code>y</code> represent the quantities of the two tokens in the liquidity pool, and <code>k</code> is a constant value. This constant, <code>k</code>, in theory at least, must always remain the same &#8211; hence the term "constant product". In practice, however, deviations can occur due to the dynamics of trading. These small variations in k are considered acceptable as long as they remain within an acceptable range and do not significantly impact the market's liquidity or stability.</p><p>Sounds a bit abstract, right? Don't worry! Let's break it down with a concrete example.</p><p><em>Let's say we have a liquidity pool with 100 ETH and 2000 DAI. Applying our formula,</em> <code>k</code> <em>would be</em> <code>100*2000 = 200,000</code><em>. This means that no matter how our token quantities change through trades, their product should always equal</em> <code>200,000</code><em>.</em></p><p><em>So, if someone comes along and buys 1 ETH for 20 DAI, our pool would have 99 ETH and 2020 DAI. If we do the math,</em> <code>99*2020 = 199,980</code><em>, which is close to our</em> <code>k</code><em>, but not exactly equal. The difference here, my friends, is due to something we call "slippage", which we've touched on earlier.</em></p><p>This model is great for keeping our AMM fair and balanced, but it does influence the price of tokens. How, you might ask? Well, the price in this model is essentially determined by the ratio of the tokens in the pool. If we go back to our previous example, initially, the price of 1 ETH is 20 DAI (2000 DAI/100 ETH = 20 DAI/ETH). But after that trade, the price becomes 20.40 DAI (2020 DAI/99 ETH = 20.40 DAI/ETH). So, you see, the act of buying ETH with DAI raised the price of ETH.</p><p>Here's a simple way to remember how this works:</p><ul><li><p>When the amount of a token in a pool decreases (because it's being bought), its price goes up.</p></li><li><p>Conversely, when the amount of a token in a pool increases (because it's being sold), its price goes down.</p></li></ul><p>With all these moving parts, it might seem like there's a lot going on. But here's a summary to help you keep track:</p><ol><li><p>The Constant Product Market Maker Model is based on the formula <code>x*y=k</code>.</p></li><li><p>The quantities of tokens in the liquidity pool can change, but their product (<code>k</code>) always stays the same.</p></li><li><p>The price of tokens is determined by their ratio in the pool.</p></li></ol><p>Whew! That was a bit of a marathon, wasn't it? But guess what? You've now got a solid understanding of how the Constant Product Market Maker Model works and how it determines the price of tokens in a liquidity pool.</p><h1>5. Popular AMMs: Uniswap, Balancer, and Curve</h1><p>It's time to explore some of the most popular Automatic Market Makers (AMMs) in the DeFi landscape. While several decentralized exchanges employ AMMs, these three platforms are sufficiently differentiated in their approach to handling AMMs. Let's jump right in!</p><h2>Uniswap: The Trailblazer</h2><p>Uniswap is undoubtedly the pioneer of AMMs, capturing the hearts and minds of DeFi enthusiasts worldwide. Launched in 2018, it introduced the concept of liquidity pools powered by smart contracts on the Ethereum blockchain. What sets Uniswap apart is its simple yet powerful approach:</p><ul><li><p><strong>Permissionless and Trustless</strong>: Anyone can participate in Uniswap without any intermediaries. No need for registration, KYC, or waiting for approvals. It's a truly open and permissionless platform.</p></li><li><p><strong>Constant Product Market Maker</strong>: Uniswap employs the Constant Product Market Maker Model we discussed earlier, enabling users to trade ERC-20 tokens directly from their wallets. The platform uses a deterministic algorithm to calculate token prices, ensuring fair and transparent trades.</p></li><li><p><strong>Liquidity Provider Incentives</strong>: Uniswap incentivizes liquidity providers by offering them a share of the trading fees generated by the platform. By staking their tokens in a liquidity pool, users can earn a portion of the trading fees proportional to their contribution.</p></li></ul><p>Uniswap's simplicity and user-friendly interface have made it the go-to choice for many traders and liquidity providers, contributing to its immense popularity and liquidity depth.</p><h2>Balancer: The Power of Customization</h2><p>Next up, we have Balancer, an AMM that takes the concept of liquidity pools to a whole new level. Balancer introduces a unique feature: the ability to create customizable pools with multiple tokens and varying weights. Here's what makes Balancer stand out:</p><ul><li><p><strong>Flexible Pools</strong>: Unlike Uniswap's 50/50 token ratio, Balancer allows users to create pools with different token compositions. For instance, you can set up a pool with 80% DAI, 15% ETH, and 5% MKR. This flexibility opens up a world of possibilities, enabling the creation of pools that cater to specific trading strategies.</p></li><li><p><strong>Smart Order Routing</strong>: Balancer optimizes trading by automatically splitting orders across multiple pools to achieve the best possible price. This feature enhances efficiency and minimizes slippage, providing users with more favourable trading experiences.</p></li><li><p><strong>Balancer Tokens</strong>: Balancer also introduces its native token called BAL. Holders of BAL tokens have governance rights, enabling them to participate in the decision-making process of the platform. These tokens can be earned by providing liquidity to Balancer pools.</p></li></ul><p>With its customizable pools and advanced trading capabilities, Balancer offers a rich trading experience for users seeking greater control over their strategies.</p><h2>Curve: The Stablecoin Specialist</h2><p>Last but not least, we have Curve, an AMM designed specifically for stablecoin trading. As we know, stablecoins play a crucial role in the DeFi ecosystem, providing stability and serving as a reliable medium of exchange. Curve takes this concept to new heights:</p><ul><li><p><strong>Low Slippage for Stablecoins</strong>: Curve is optimized for trading stablecoins, ensuring minimal slippage and reduced fees. By focusing on stable assets, Curve provides a reliable and efficient trading experience for users looking to swap between stablecoins.</p></li><li><p><strong>Low Volatility Pools</strong>: Curve utilizes specialized pool strategies, such as the StableSwap algorithm, to maintain low volatility. These strategies aim to keep the peg of stablecoins intact and minimize price fluctuations, resulting in a smoother trading experience.</p></li><li><p><strong>Enhanced Liquidity</strong>: Curve incentivizes liquidity providers with trading fees and CRV tokens, encouraging them to contribute to the platform's liquidity. This ensures ample liquidity for stablecoin trading, enhancing stability and reducing price impact.</p></li></ul><p>Curve's dedication to stablecoin trading has made it a popular choice for users seeking seamless and secure stablecoin swaps.</p><h2>6. Risks and Challenges with Automated Market Makers (AMMs)</h2><p>While the advent of AMMs has fundamentally revolutionized the DeFi space, it's not without its risks and challenges. Let's take a look at some of these potential pitfalls, particularly impermanent loss, price slippage, and smart contract risk.</p><p><strong>1. Impermanent Loss:</strong></p><p>Impermanent loss can be an unfamiliar concept for those new to the DeFi space, but it's of paramount importance if you're interacting with AMMs. Imagine you're providing liquidity to a pool and the relative price of your tokens changes compared to when you deposited them. The "loss" comes into play when the current price divergence causes your stake in the liquidity pool to be worth less than if you had just held onto the tokens.</p><p><em>For example, you've contributed 1 ETH and 100 DAI (assuming 1 ETH = 100 DAI) to a liquidity pool. If the price of ETH rises to 200 DAI, the AMM would balance itself, resulting in fewer ETH and more DAI in your pool share. If you decide to withdraw at this point, you end up with less ETH than you initially put in, experiencing what's known as 'impermanent loss'.</em></p><p><strong>2. Price Slippage:</strong></p><p>In a nutshell, price slippage refers to the difference between the expected price of a trade and the actual price at which the trade is executed. This phenomenon is particularly prevalent in AMMs due to the algorithmic nature of price determination.</p><p>Imagine wanting to trade 10,000 DAI for ETH in a pool. If the pool&#8217;s liquidity isn't sufficient, the initial DAI you trade, say the first few hundred, will get you a reasonable amount of ETH, per the prevailing market exchange rate. However, as you continue trading more DAI for ETH, the pool's DAI supply increases while its ETH supply decreases. This imbalance causes the pool's DAI/ETH exchange rate to shift against you ('slip' effectively), in accordance with the AMM's pricing algorithm. As a result, for each subsequent DAI you trade, you receive incrementally less ETH.</p><p><strong>3. Smart Contract Risk:</strong></p><p>Smart contracts are the backbone of AMMs and, as such, they carry their share of risk. From potential bugs in the contract code to more malicious exploits, the automatic and immutable nature of smart contracts can pose a significant threat.</p><p>The infamous DAO Hack in 2016 was due to a smart contract vulnerability where recursive calls were exploited, leading to a loss of around $60M worth of Ether.</p><h2>Mitigating AMM Risks</h2><p>As intimidating as these risks may seem, the DeFi community is constantly innovating and developing strategies to counteract these issues.</p><p><strong>1. Impermanent Loss &amp; Price Slippage::</strong></p><p>New iterations on the initial AMM models are emerging, models such as:</p><ul><li><p>Hybrid Automated Market Makers (HAMM)</p></li><li><p>Dynamic Automated Market Makers (DAMM)</p></li><li><p>Virtual Automated Market Makers (VAMM)</p></li><li><p>Proactive Market Makers (PMM)</p></li><li><p>Replicating Market Maker (RMM)</p></li></ul><p>These new models apply different mechanisms such as reducing price impact, distributing liquidity better, and proactively moving the price curve in an attempt to combat impermanent loss and price slippage</p><p>Feel free to explore<em>.</em></p><p><strong>2. Managing Smart Contract Risk:</strong></p><p>Smart contract risk is mitigated by robust testing, formal verification, and audits by reputable security firms. On top of this, some platforms are integrating insurance or bug bounty programs into their protocol to provide an additional layer of protection.</p><h1>7. Conclusion</h1><p>We've navigated quite a journey through the world of Automated Market Makers, haven't we? In our expedition, we explored several facets of AMMs, from their foundational principles to their influential role in the DeFi space. Let's revisit the key takeaways to solidify our newfound knowledge.</p><ul><li><p><em>AMMs are Game Changers:</em> AMMs have introduced a revolutionary shift in how asset exchange works. By eliminating the need for order books and enabling seamless token swaps, they've undeniably made the DeFi space more accessible and efficient.</p></li><li><p><em>The Power of Liquidity Pools:</em> At the heart of every AMM, we find liquidity pools. These are fueled by liquidity providers who deposit pairs of tokens and earn trading fees in return, creating a self-sustaining ecosystem of decentralized exchanges.</p></li><li><p><em>Modelling the Market:</em> We delved into the Constant Product Market Maker Model, the mathematical magic behind price determination in a liquidity pool. The elegant simplicity of this model is a key reason for its widespread adoption.</p></li><li><p><em>Variety of AMMs:</em> We acquainted ourselves with notable AMMs like Uniswap, Balancer, and Curve. Each one brings something unique to the table, making the DeFi landscape more robust and diverse.</p></li><li><p><em>Risk and Reward:</em> Like all financial systems, AMMs come with their share of risks. Impermanent loss, price slippage, and smart contract risk are significant hurdles. However, innovators in the space continue to develop solutions to these challenges.</p></li></ul><p>So, what lies ahead for AMMs and the broader DeFi landscape? Well, the potential is staggering. AMMs have just begun to scratch the surface of the possible disruptions in the financial world. They've already created a paradigm shift in how we trade and exchange assets, democratizing the process and making it more accessible to the masses.</p><p>However, as we peer into the future, the potential implications of AMMs extend beyond just trading. As the underlying technology evolves, we may see AMMs integrated into more complex financial instruments or operations. Imagine a world where complex derivatives or loans are handled completely by smart contracts and AMMs. The possibilities are truly endless.</p><p>In the grand scheme of things, we're still in the early stages of this exciting journey. As we continue to advance and innovate, AMMs will undoubtedly play a pivotal role in shaping the DeFi landscape and possibly the future of finance as we know it.</p><p>So, strap in and hold on tight. The future is looking mighty exciting, my friends, and I can't wait to continue learning, exploring, and sharing this journey with you all. Till our next exploration, stay curious and keep learning!</p>]]></content:encoded></item><item><title><![CDATA[Understanding Gas in Ethereum and How to Optimize Smart Contracts]]></title><description><![CDATA[1.]]></description><link>https://bolarinwaowuogba.substack.com/p/understanding-gas-in-ethereum-and-how-to-optimize-smart-contracts</link><guid isPermaLink="false">https://bolarinwaowuogba.substack.com/p/understanding-gas-in-ethereum-and-how-to-optimize-smart-contracts</guid><dc:creator><![CDATA[Bolarinwa Owuogba]]></dc:creator><pubDate>Tue, 16 May 2023 06:28:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e0f1372a-a6d7-4fb9-a99e-9f4fcb8ef401_577x433.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>1. Introduction</h2><p>Hello, world! Welcome to the fascinating world of Ethereum, blockchain's answer to Disneyland. A platform that has turned the game upside down for decentralized applications and smart contracts. These contracts, with their superpowers, can be executed flawlessly based on predefined conditions, automating tasks and are revolutionizing how we handle money, how we govern, and how we trace supply chains.</p><p>The star player in the Ethereum arena is gas &#8211; the secret sauce that keeps the Ethereum engine running. It's the driving force behind all transactions and smart contracts on this network. By ensuring fairness and preventing resource-hogging or rogue code, gas safeguards the network's integrity and security. So, gas isn't just a mundane concept; it's the superhero of the Ethereum show!</p><h2>2. Understanding Gas in Ethereum</h2><p>Have you heard of "gas" in Ethereum? It's not the kind that powers your car, but the "energy" that makes things happen. Every operation, every transaction, every little thing you do on Ethereum needs some of this gas. It's like the tokens you put into an arcade machine to play a game - no gas, no fun.</p><p>Ethereum uses gas to keep everyone playing fair. It's a rationing system, ensuring everyone gets a turn on the Ethereum playground. But here's the kicker - gas isn't free. It's priced in Ether (ETH), Ethereum's native cryptocurrency, specifically in a smaller unit of Ether called Gwei.</p><p>Let's break it down with an example. Imagine you're sending 1 ETH to a friend. For that transaction, you might set a gas limit of 21,000 (that's how much "energy" you think it'll take) and a gas price of 20 Gwei. The gas limit here is like your guess of how much "energy" your transaction will require, while the gas price is what you're willing to pay for each unit of that "energy".</p><p>If the Ethereum network is not too congested, your transaction gets processed, and the total gas cost will be the gas limit times the gas price, which is 21,000 (gas limit) * 20 (gas price) = 420,000 Gwei. This amount is deducted from your ETH balance.</p><p>However, if the network is busy and many people are trying to transact at the same time, your transaction might not get processed unless you're willing to pay a higher gas price. It's a bit like an auction where you're bidding for the network's computational resources!</p><p>This concept can be represented in pseudo-code as follows:</p><pre><code>function sendTransaction() {
  var gasLimit = 21000;
  var gasPrice = 20; // in Gwei
  var totalGasCost = gasLimit * gasPrice;

  if (totalGasCost &lt;= yourCurrentBalance) {
    // go ahead, send the transaction
  } else {
    // sorry mate, you need more Ether
  }
}
</code></pre><p>So that's the lowdown on gas in Ethereum - it's all about keeping the network running smoothly, ensuring everyone gets a fair go, and making sure you pay for what you use. And remember, the key is to find that sweet spot of gas price that gets your transaction through without emptying your Ether wallet!</p><h2>3. The Role of Gas in Smart Contract Execution</h2><p>Let's dive into the juicy part - how gas plays its part in smart contract execution. Imagine gas as the juice that powers up each action in your smart contract. You got it, every function you call, every operation you run, it all needs some gas to make it happen.</p><p>Think of it like a game arcade. If you want to play a game (run a function), you gotta put in some tokens (gas). Now, not all games (operations) in this Ethereum arcade cost the same. Let's break it down into categories:</p><ul><li><p>Reading Operations: Reading data from the blockchain is like playing the simplest arcade game, let's say a round of air hockey. Doesn't cost much gas because all you're doing is checking out data that's already there. You're not changing anything.</p></li><li><p>Arithmetic Operations: Doing some math in your contract, like adding or subtracting numbers, is like your old-school Pac-Man. Still pretty affordable in terms of gas, but a tad bit more than just reading data.</p></li><li><p>Storing Data: Now, if you're storing data or creating new contracts - that's like the deluxe VR game in the corner. Storing data means you're actively changing something on the blockchain. And that, my friend, will cost you a bit more gas.</p></li><li><p>Complex Operations: Then there are some operations that are the equivalent of that flashy, new racing game everyone's lining up to play. These could be things like calling other contracts or using loops in your functions. These will be the most gas-hungry operations.</p></li></ul><p>Before you start playing (run a transaction), you need to estimate how many tokens (gas) you'll need. That's your gas limit. If you run out of tokens before you finish the game, it's game over! In Ethereum, we call this an "out-of-gas" error. And just like in the arcade, there's no refunds or do-overs. Your tokens (gas) are spent, and you'll need more to keep playing.</p><p>In a nutshell, every operation in your smart contract is like a game in the arcade that costs a certain amount of tokens (gas). Choose your operations wisely, and always keep an eye on your gas limit!</p><h2>4. Estimating Gas Costs</h2><p>Alright, get ready, we're cruising into the land of estimating gas costs. Think of it like calculating how much gas you'd need for a cross-country road trip. You've got your destination, now you gotta work out the fuel.</p><p>So, how do you do it? Well, there are a few ways. Some folks use online platforms like Etherscan or ETH Gas Station to check out average gas costs. Others might go for MetaMask, a browser extension that estimates the gas for you when you're about to make a transaction. It's like having a buddy who's great at mental math in the passenger seat.</p><p>But if you really wanna dive under the hood, there's the 'estimateGas' function in web3.js, Ethereum's JavaScript API. This nifty tool is like your car's onboard computer. You give it the details of your journey (the transaction), and it calculates the fuel you'll need (the gas). Here's a quick peek:</p><pre><code>web3.eth.estimateGas({
    to: "0xSomeAddress",
    data: "0xSomeData"
})
.then(console.log);
</code></pre><p>So, what's going on here? Well, you're asking 'estimateGas' to take a look at a hypothetical transaction to a certain address ('0xSomeAddress'), with certain data ('0xSomeData'). It runs the numbers and then logs the estimated gas cost.</p><p>But here's the twist: the gas estimate doesn't give you the exact cost in Ether. Nope, it's more like a puzzle piece. You'll need to multiply the gas estimate by the current gas price (in Gwei) to get the actual cost in Ether. It's like calculating the total bill at a restaurant &#8211; the menu tells you the price per dish, and you do the math to get the final bill.</p><p>Let's take a look at a practical example to make things crystal clear:</p><pre><code>const gasEstimate = 50000; // Gas estimate received from a tool
const gasPrice = 20; // Gas price in Gwei
const actualCost = gasEstimate * gasPrice; // Actual cost in Ether

console.log(`The estimated gas cost is ${gasEstimate} units.`);
console.log(`To calculate the actual cost, multiply it by the gas price (${gasPrice} Gwei).`);
console.log(`The total cost will be ${actualCost} Ether.`);
</code></pre><p>But remember, it's like a weather forecast. It's a pretty good guess, but always bring an umbrella just in case!</p><h2>5. Optimizing Smart Contracts for Lower Gas Consumption</h2><p>Time to dive headfirst into the world of crafting lean, mean, gas-efficient smart contracts! We've got some tips, cool tools, and real-world examples to help you ace the game of gas optimization. So, buckle up and get ready for a wild ride!</p><p>Here are some best practices that can work wonders in writing efficient smart contracts:</p><ul><li><p>Minimize those pesky storage operations, just like tidying up your room to make it neat.</p></li><li><p>Avoid those never-ending loops that gobble up gas like the plague. Opt for elegant algorithms to keep things running smoothly.</p></li></ul><p>Now, let's explore some handy tools to analyze and optimize gas usage in smart contracts. These tools are like superheroes that can save the day:</p><ul><li><p>Gas Analyzers: They're like Sherlock Holmes, investigating your code to uncover any gas-hogging culprits. Some examples include: EthGasStation, GasNow, Blockscout, Gas Price Monitor, Gas Tracker, etc</p></li><li><p>Profilers: Think of them as personal trainers, helping you shed unnecessary gas weight and build leaner contracts. They work by running your contracts and collecting data on the gas used by each function. This data can then be used to identify areas where your contracts are using more gas than necessary. Some examples include: Remix IDE, Truffle Suite, Tracer, EthVM, Gasper</p></li></ul><p>But wait, there's more! Let's take a look at some optimized smart contract code to get your gears turning:</p><pre><code>// Voting System Example

contract Voting {
    mapping(address =&gt; bool) public hasVoted;
    uint256 public totalVotes;

    function vote() public {
        require(!hasVoted[msg.sender], "You've already cast your vote!");

        // Voting logic here

        hasVoted[msg.sender] = true;
        totalVotes++;
    }
</code></pre><p>Voting System: By using a mapping to track votes and avoiding redundant storage updates, you can create a lean and efficient voting contract.</p><pre><code>// Token Transfer Example

contract Token {
    mapping(address =&gt; uint256) public balances;

    function batchTransfer(address[] memory recipients, uint256 amount) public {
        for (uint256 i = 0; i &lt; recipients.length; i++) {
            balances[recipients[i]] += amount;
        }
    }
}
</code></pre><p>Token Transfer: Optimize token transfers by using batch operations and reducing the number of external calls. It's like sending a bunch of gifts in one go instead of individual parcels.</p><h2>6. Conclusion</h2><p>And just like that, we've reached the end of our gas adventure! Now, you're not just a casual Ethereum user, you're a gas guru! You know what gas is, why it's important, and how it's used in Ethereum. You're no stranger to gas prices and limits, and you know how to make your smart contracts as gas-efficient as possible.</p><p>Here's a quick recap of what we've covered:</p><ul><li><p>We learned about what gas is, why Ethereum uses it, and how it relates to Ether. Remember the car and fuel analogy? Gas is like the fuel that powers every operation on the Ethereum network.</p></li><li><p>We talked about gas in the context of smart contracts. Like how every operation in a contract requires gas, and how running out of gas can lead to an "out-of-gas" error.</p></li><li><p>We delved into estimating gas costs, with cool tools like web3's estimateGas function. It's like your gas gauge, telling you how much gas you'll need for a particular transaction.</p></li><li><p>And finally, we talked about optimizing smart contracts to use less gas. Because who doesn't like saving money, right?</p></li></ul><p>Remember, the world of Ethereum and smart contracts is always evolving. So keep exploring, keep learning, and keep optimizing. Who knows, maybe you'll be the one to come up with the next big gas-saving technique!</p><p>So, keep on cruising, fellow Ethereum enthusiast. The road to blockchain mastery is long, but with your newfound gas knowledge, I have no doubt you'll go far!</p><h2>7. References</h2><p>Well, you've made it to the end of our Ethereum gas journey, pal! But hey, the learning doesn't stop here. I've got a bunch of resources to keep you fueled up on your blockchain quest. Check these out:</p><ol><li><p><strong>Ethereum Whitepaper</strong>: The OG guide to Ethereum. A bit heavy, but worth the read. <a href="https://ethereum.org/en/whitepaper/">Check it out</a></p></li><li><p><strong>Ethereum Yellow Paper</strong>: This one dives deep into Ethereum's technical side. Keep this one handy. <a href="https://ethereum.github.io/yellowpaper/paper.pdf">Have a look</a></p></li><li><p><strong>Ethereum Gas Station</strong>: A site dedicated to real-time gas prices in the Ethereum world. Pretty cool, huh? <a href="https://ethgasstation.info/">Take a peek</a></p></li><li><p><strong>Solidity Documentation</strong>: A complete guide to write smart contracts. It's a must-have for every coder. <a href="https://docs.soliditylang.org/">Give it a read</a></p></li><li><p><strong>Etherscan</strong>: Check out Ethereum transactions, contracts, and everything else on the blockchain. <a href="https://etherscan.io/">Here you go</a></p></li><li><p><strong>StackExchange Ethereum</strong>: Stuck on a problem? Chances are someone else has been too. Find your answers here. <a href="https://ethereum.stackexchange.com/">Go on, ask away</a></p></li></ol><p>Remember, the blockchain world is always changing. So, keep up, stay curious, and keep learning. You're on your way to becoming a true Ethereum champ!</p>]]></content:encoded></item><item><title><![CDATA[Relational Database Design vs DynamoDB Single-Table Design]]></title><description><![CDATA[Cover Photo by Dietmar Becker on Unsplash]]></description><link>https://bolarinwaowuogba.substack.com/p/relational-database-design-vs-dynamodb-single-table-design</link><guid isPermaLink="false">https://bolarinwaowuogba.substack.com/p/relational-database-design-vs-dynamodb-single-table-design</guid><dc:creator><![CDATA[Bolarinwa Owuogba]]></dc:creator><pubDate>Mon, 25 Oct 2021 06:31:44 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cb1f151e-70cd-4e76-bcf7-fc33bdfa2ea8_2742x1828.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Cover Photo by <a href="https://unsplash.com/@dietmarbecker?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Dietmar Becker</a> on <a href="https://unsplash.com/s/photos/comparison?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p><p>Let's talk DB design.</p><h3>What is Relational Database Design?</h3><p>Relational Database(DB) Design organizes data in a database into relations, which are one or more tables of columns and rows, with a unique key identifying each row. Rows are also called records or tuples. Columns are also called attributes. Generally, each table/relation represents one "entity type" (such as customer or product). The rows represent instances of that type of entity (such as "John" or "chair") and the columns represent values attributed to that instance (such as address or price).</p><h3>What is DynamoDB?</h3><p>Amazon DynamoDB is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. It promises consistent single-digit millisecond performance at any scale. DynamoDB charges for reading, writing, and storing data in your DynamoDB tables so it is <strong>Pay-as-you-go</strong> which means you only pay for exactly what you use.</p><p>This performance along with its pricing model is among the factors which make DynamoDB so attractive. The tricky part with DynamoDB is what comes next.</p><p>Unlike the relational DB model where each entity gets a separate table and joins are performed to group related data across tables and fetch them together e.g fetching all the products bought by a customer, DynamoDB does not support joins. To tackle this issue, a common design pattern is the single-table model.</p><h3>What is the Single-Table Design?</h3><p>In single-table design, all the entities in the DynamoDB database use the same table and by making use of certain structures unique to DynamoDB, individual entities can still be fetched and several common relational access patterns can be performed.</p><p>Enough talk! Let's see some modelling</p><h3>Entities</h3><p>Let's take three entities to model. These entities could be part of an e-commerce application. They are: customer, product and order. The relationship between them can be defined as such:</p><ul><li><p>A one-to-many relationship exists between the customer and order entities.</p></li><li><p>A many-to-many relationship exists between the order and product entities.</p></li></ul><h3>Modelling a Relational Design</h3><p>To represent this in relational design, we'd have something like this:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!58Mi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!58Mi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 424w, https://substackcdn.com/image/fetch/$s_!58Mi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 848w, https://substackcdn.com/image/fetch/$s_!58Mi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 1272w, https://substackcdn.com/image/fetch/$s_!58Mi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!58Mi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!58Mi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 424w, https://substackcdn.com/image/fetch/$s_!58Mi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 848w, https://substackcdn.com/image/fetch/$s_!58Mi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 1272w, https://substackcdn.com/image/fetch/$s_!58Mi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09afad85-44ac-479f-8f8d-6e7421b1f572_632x420.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Here we have the classic schema with a different table for each entity and a join <code>OrderProduct</code> table for modelling the many-to-many relationship between the Order and Product entities. With this, we can get all the information we need about the various entities and perform joins to retrieve information about the relationship between entities like the names of products in an order, the orders a customer has made and so on.</p><h3>Modelling DynamoDB's Single-Table Design</h3><p>Unlike relational design where we start by representing the entities in tables and figure out the queries we need to retrieve the data from the table when modelling in DynamoDB, we need to first know the queries we need <em>before</em> creating the table. This is crucial in allowing us to perform queries efficiently because of DynamoDB's query structure.</p><ul><li><p>Listing data access requirements: Of course, you don't have to write out every possible access pattern your application might need, you just want to identify the core access patterns. Since these entities are being used in the context of an e-commerce application, a few queries we might need are:</p><ol><li><p>Get all orders that belong to a customer.</p></li><li><p>Get information about all the products in an order.</p></li><li><p>Get all the orders made for a particular product.</p></li><li><p>Sort a customer's orders by price.</p></li></ol></li></ul><p>In a real-world application, there would be much more access patterns depending on what the business requirements are but these three will do for now.</p><h4>DynamoDB's query structure</h4><p>DynamoDB tables consist of items that are themselves a collection of key-value pairs. The keys of the items are referred to as attributes, examples of attributes on an item could be; name, price, date etc. For the most part, like most NoSQL databases DynamoDB doesn't enforce strict conditions when creating tables save for <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html">a few rules</a>.</p><p>One of the important conditions though is the <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey">primary key</a>. Let me explain, the primary key of an item uniquely identifies it among other items in a table and you specify what the primary key of items in a table is when you create it. The primary key can either be:</p><ul><li><p>a simple primary key which consists of an attribute specified as a partition key, or</p></li><li><p>a composite key which consists of two attributes, a partition key and a sort key.</p></li></ul><p>When querying the data in a table, you can use a <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions"><code>key condition expression</code></a> to specify criteria to match the primary key on and an optional <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.FilterExpression"><code>filter expression</code></a> to further refine the results based on other attributes items might have. In DynamoDB, the pricing on a query depends on the number of items that match the key condition expression, regardless of how many items you decide to then filter out with a filter expression as such it becomes important to structure our data such that we can perform most if not all our access patterns with key condition expressions.</p><h4>Designing the DynamoDB table</h4><p>The key entities can be represented in a single table using the pattern below. Where PK means partition key and SK means sort key:</p><p>ENTITYPKSK CUSTOMERCUSTOMER#<code>[customer id]</code>CUSTOMERINFO#<code>[customer name]</code> ORDERCUSTOMER#<code>[customer id]</code>ORDER#<code>[order id]</code> PRODUCTPRODUCT#<code>[product id]</code>PRODUCTNAME#<code>[product name]</code></p><p>This table design allows us to get orders that belong to a customer by querying the partition key and sort key with the appropriate prefixes and the customer's id e.g with the key condition expression: <code>PK = CUSTOMER#[customer id] and begins_with(SK, "ORDER#")</code>, we can also get a customer's details by a combination of the customer's id and the <code>CUSTOMER_INFO#</code> prefix.</p><p>Having the entities represented here is nice but this design doesn't provide us with an efficient way of querying the orders made for a product or getting the details of products in an order per our required access patterns above.</p><p>To fix that we'll add an order-product representation to our table for the relationship between an order and a product. For the PK and SK we'll use the structures <code>CUSTOMER#[customer id]#ORDER#[order id]</code> and <code>PRODUCT#[product id]</code> respectively. This allows us to get the list of products in an order with the key condition expression: <code>PK = CUSTOMER#[customer id]#ORDER#[order id] and begins_with(SK, "PRODUCT#")</code>. An example of what our table looks like with some dummy data is shown below:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6979!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6979!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 424w, https://substackcdn.com/image/fetch/$s_!6979!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 848w, https://substackcdn.com/image/fetch/$s_!6979!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 1272w, https://substackcdn.com/image/fetch/$s_!6979!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6979!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;main.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="main.png" title="main.png" srcset="https://substackcdn.com/image/fetch/$s_!6979!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 424w, https://substackcdn.com/image/fetch/$s_!6979!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 848w, https://substackcdn.com/image/fetch/$s_!6979!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 1272w, https://substackcdn.com/image/fetch/$s_!6979!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eab03e7-95d2-4170-bd8b-344b18000bac_808x529.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>To fetch orders that a product is a part of, we need to be able to query the order-product representation by the product id directly. To do this, we add a <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.SecondaryIndexes">Global secondary index (GSI)</a> to the table. This allows us to create a partition key and a sort key different from those defined on the table. We can add new GSIPK and GSISK attributes to the order-product representation which will be the partition key and sort key for the new index respectively.</p><p>For the order-product representation, we'll store:</p><ul><li><p><code>PRODUCT#[product id]</code> in GSIPK, and</p></li><li><p><code>CUSTOMER#[customer id]#ORDER#[order id]</code> in GSISK</p></li></ul><p>Resulting in a new table that looks like this:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Vuc0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Vuc0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 424w, https://substackcdn.com/image/fetch/$s_!Vuc0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 848w, https://substackcdn.com/image/fetch/$s_!Vuc0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 1272w, https://substackcdn.com/image/fetch/$s_!Vuc0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Vuc0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a839869f-2e18-449f-981e-39f75f669bff_793x577.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;main.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="main.png" title="main.png" srcset="https://substackcdn.com/image/fetch/$s_!Vuc0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 424w, https://substackcdn.com/image/fetch/$s_!Vuc0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 848w, https://substackcdn.com/image/fetch/$s_!Vuc0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 1272w, https://substackcdn.com/image/fetch/$s_!Vuc0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa839869f-2e18-449f-981e-39f75f669bff_793x577.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Now we can perform a query with the key condition expression: <code>GSIPK = PRODUCT#[product id]</code> on the new GSI. This fetches all the order-product entries with that <code>product id</code>. Visualizing the new GSI, we have:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!S5r4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S5r4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 424w, https://substackcdn.com/image/fetch/$s_!S5r4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 848w, https://substackcdn.com/image/fetch/$s_!S5r4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 1272w, https://substackcdn.com/image/fetch/$s_!S5r4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S5r4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;GSI_main_GSI1.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GSI_main_GSI1.png" title="GSI_main_GSI1.png" srcset="https://substackcdn.com/image/fetch/$s_!S5r4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 424w, https://substackcdn.com/image/fetch/$s_!S5r4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 848w, https://substackcdn.com/image/fetch/$s_!S5r4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 1272w, https://substackcdn.com/image/fetch/$s_!S5r4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac888176-3891-4b68-8cda-ea0a1e779f78_793x257.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Finally, to sort a customer's orders by price, we need to add a sort key on the price value of an order. This is because, by default, query results are always sorted by the sort key value and optionally ordered in either ascending or descending order. Currently, the order representation has a partition key <code>CUSTOMER#[customer id]</code> and a sort key <code>ORDER#[order id]</code>. To add a new sort key here, we need to create another index, this time a <strong><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.SecondaryIndexes">Local Secondary Index (LSI)</a></strong> on the <code>orderPrice</code> attribute.</p><p>While a GSI creates a new primary key (partition and sort key), an LSI only creates a new sort key on the preexisting partition key. This allows us to sort the item collections under a partition key based on several attributes, in this case, based on the <code>orderPrice</code> attribute. The query to fetch a customer's orders sorted by price would now be a key condition expression: <code>PK = CUSTOMER#[customer id]</code> on the new LSI.</p><h3>Review</h3><p>At the end of our modelling, our table has:</p><ul><li><p>a main composite key: <code>PK</code> and <code>SK</code>.</p></li><li><p>a local secondary index (LSI): Partition key is <code>PK</code> and sort key is <code>orderPrice</code>.</p></li><li><p>a global secondary index (GSI): Partition key is <code>GSIPK</code> and the sort key is <code>GSISK</code>.</p></li></ul><p>Our access patterns have been handled:</p><ol><li><p>Get all orders that belong to a customer.<br>key condition expression: <code>PK = CUSTOMER#[customer id] and begins_with(SK, "ORDER#")</code></p></li><li><p>Get information about all the products in an order.<br>key condition expression: <code>PK = CUSTOMER#[customer id]#ORDER#[order id] and begins_with(SK, "PRODUCT#")</code></p></li><li><p>Get all the orders made for a particular product.<br>key condition expression: <code>GSIPK = PRODUCT#[product id]</code> on the new GSI</p></li><li><p>Sort a customer's orders by price.<br>key condition expression: <code>PK = CUSTOMER#[customer id]</code> on the new LSI.</p></li></ol><h3>Conclusion</h3><p>In conclusion, while relational database design keeps data normalized, DynamoDB isn't a slouch either. It does not allow for joins, but by modelling your data properly or using certain patterns like the single-table pattern join-like queries can be performed. There are certainly more hoops to jump through when designing an effective data solution for DynamoDB but the advantages once done can be considerable.</p><p>As such, if you're considering using DynamoDB over a relational database solution, like a lot of things in software engineering, it would depend on your particular use case, business requirements, access patterns, budget etc. So let me know in the comments, in which use cases would you favour DynamoDB over a relational database solution?</p><p>Cheers!</p>]]></content:encoded></item><item><title><![CDATA[Building a telegram food bot with TypeScript + Telegraf-Inline-Menu]]></title><description><![CDATA[Photo by Davide Cantelli on Unsplash]]></description><link>https://bolarinwaowuogba.substack.com/p/building-a-telegram-food-bot-with-typescript-telegraf-inline-menu</link><guid isPermaLink="false">https://bolarinwaowuogba.substack.com/p/building-a-telegram-food-bot-with-typescript-telegraf-inline-menu</guid><dc:creator><![CDATA[Bolarinwa Owuogba]]></dc:creator><pubDate>Thu, 07 Oct 2021 05:41:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1dc0ee1c-e318-472d-b3ae-a34defa5e90e_4592x3064.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Photo by <a href="https://unsplash.com/@cant89?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Davide Cantelli</a> on <a href="https://unsplash.com/s/photos/food?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></em></p><p>I find it fascinating how Telegram opens itself up to users and developers alike, its extensibility as a platform makes it possible to do <a href="https://telegramchannels.me/bots">so many things</a> with it. Recently, I've been working on a bot of my own. It's a bit large and I've enjoyed working on it so I thought I'd write about building one. To that end, we're going to be building a Telegram food bot. Our food bot is going to do a few things:</p><ul><li><p>Show a list of available cuisines.</p></li><li><p>Fetch recipes for a cuisine.</p></li><li><p>Get preparation instructions for a recipe.</p></li></ul><p>To accomplish these things we're going to be using the <a href="https://spoonacular.com/food-api">spoonacular API</a>. The code for this tutorial is <a href="https://github.com/RinwaOwuogba/telegram-food-bot/">on github</a> and the bot is deployed on Heroku if you want to check it out <a href="https://t.me/SimpleCuisineBot">on telegram</a>.</p><h3>Prerequisites</h3><p>To follow along with this tutorial, you need to have:</p><ul><li><p>Node v14.7.1 or above installed</p></li><li><p>A good understanding of TypeScript</p></li><li><p>Basic knowledge of Axios</p></li></ul><h3>Setup</h3><h4>Getting a bot token from <a href="https://core.telegram.org/bots#6-botfather">botfather</a></h4><p>Following the instructions on telegram's <a href="https://core.telegram.org/bots">bot page</a>, you'll need to create an access token for the bot:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qUKr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qUKr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 424w, https://substackcdn.com/image/fetch/$s_!qUKr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 848w, https://substackcdn.com/image/fetch/$s_!qUKr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 1272w, https://substackcdn.com/image/fetch/$s_!qUKr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qUKr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;creating-cuisine-bot.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="creating-cuisine-bot.png" title="creating-cuisine-bot.png" srcset="https://substackcdn.com/image/fetch/$s_!qUKr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 424w, https://substackcdn.com/image/fetch/$s_!qUKr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 848w, https://substackcdn.com/image/fetch/$s_!qUKr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 1272w, https://substackcdn.com/image/fetch/$s_!qUKr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4028aadb-5e35-4863-b92a-d2ca53bf1bf6_622x585.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p> Keep that safe you'll be needing it.</p><h4>Getting an API key from Spoonacular</h4><p>You need to first create an account on <a href="https://spoonacular.com/food-api/console#">Spoonacular</a>. Once logged in, you can get your API key under the profile section of your dashboard.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!p_Fc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!p_Fc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 424w, https://substackcdn.com/image/fetch/$s_!p_Fc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 848w, https://substackcdn.com/image/fetch/$s_!p_Fc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 1272w, https://substackcdn.com/image/fetch/$s_!p_Fc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!p_Fc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!p_Fc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 424w, https://substackcdn.com/image/fetch/$s_!p_Fc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 848w, https://substackcdn.com/image/fetch/$s_!p_Fc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 1272w, https://substackcdn.com/image/fetch/$s_!p_Fc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffda33e1b-4e18-4b10-acfc-8315d945c209_1282x627.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h4>Bootstrapping a TypeScript project</h4><p>Next, we have to set up a TypeScript project. Going into all the steps involved here would make this much longer so I created a project starter that we can just use instead. To get started you can clone <a href="https://github.com/RinwaOwuogba/ts-template-project.git">this repo</a> or if you're on a Linux terminal just run:</p><pre><code>git clone https://github.com/RinwaOwuogba/ts-template-project.git
mv ts-template-project/ telegram-food-bot/
cd telegram-food-bot/
rm -rf ./.git
rm README.md
</code></pre><p>These commands clone the project locally and delete git files from the cloned repository.</p><p>We can then rename the folder. In this case, we're renaming it to <code>telegram-food-bot</code>. And I know that's a bit on the nose but hey, <a href="https://en.wikipedia.org/wiki/KISS_principle">KISS</a>.</p><h4>Adding bot dependencies</h4><p>We need to add the dependencies we need for this specific project.</p><pre><code>yarn add telegraf telegraf-inline-menu axios express dotenv
yarn add -D @types/node @types/express
</code></pre><h4>Add .env file</h4><p>To load environmental variables in development, we're using the <code>dotenv</code> module. <code>dotenv</code> allows us to load environmental variables from a <code>.env</code> file. Create a <code>.env</code> file in the root directory of your project then add some of the variables we'll need:</p><pre><code>BOT_TOKEN=[YOUR_BOT_TOKEN_FROM_BOT_FATHER]
SPOONACULAR_API_KEY=[YOUR_API_KEY_FROM_SPOONACULAR]
# if you decide to deploy
APP_URL=[URL_FOR_YOUR_BOT]
</code></pre><h4>Create a configuration file</h4><p>Create a <code>src/config.ts</code> file. I prefer to use a config file to reference environment variables. It keeps it so that any general changes to those variables happen in one place and not all over the codebase. We'll populate it with this:</p><pre><code>import { config as dotenvConfig } from 'dotenv';

// loads environmental variables from .env file
// when in development
if (process.env.NODE_ENV !== 'production') {
  dotenvConfig();
}

const config = {
  appUrl: process.env.APP_URL,
  telegramBot: {
    token: process.env.BOT_TOKEN || 'xxxx',
    webhookUrl: `${process.env.APP_URL}${process.env.BOT_TOKEN}` || 'xxxx',
  },
  isProduction: process.env.NODE_ENV === 'production',
  port: process.env.PORT || 4000,
  spoonacular: {
    apiKey: process.env.SPOONACULAR_API_KEY || 'xxxx',
  },
};

export default config;
</code></pre><p>The structure of the config file is based on personal preference, the only thing here that might seem weird is the <code>webhookUrl</code> key which is a URL for Telegram to call when sending updates to the bot e.g new messages. Its structure is based on <a href="https://core.telegram.org/bots/api#setwebhook">telegram's suggestions</a> for security reasons.</p><h4>Add basic types</h4><p>Create a <code>src/types.ts</code> file. Here we'll define the custom type definitions we need for the bot.</p><pre><code>import { Context } from  'telegraf';


/**
* All bot commands.
*/
export enum FoodBotCommands {
    showCuisines = 'show_cuisines',
}

/**
* Additional data passed in context
*/
interface  ISessionData {
    page?: number;
    itemCount?: number;
}

/**
* Custom context to contain additional context fields
*/
export  interface  IFoodContext  extends  Context {
    session: ISessionData;
    match: RegExpExecArray | undefined;
}
</code></pre><p><code>FoodBotCommands</code> defines the list of telegram commands that the bot will make available to users, we only have one command but a larger bot would have a much larger list.</p><p>Telegraf creates a <code>Context</code> instance for every incoming update which like the name implies represents the context the update is coming from - details about the telegram user, message that triggered the update etc. <code>IFoodContext</code> defines additional fields that the bot is going to need but aren't available in the default <code>Context</code> type, more on it later.</p><p>The reason for the different casing styles in the <code>FoodBotCommands</code> enum is because of <a href="https://core.telegram.org/bots/api#botcommand">telegram's naming convention for bot commands</a> which doesn't allow uppercase characters thus restricting us to <a href="https://en.wikipedia.org/wiki/Snake_case">snake case</a>.</p><h4>Setup command handlers</h4><p>Messages sent to the bot trigger updates from telegram. We need to define handlers for those updates. Create a <code>src/telegram/handlers.ts</code> and populate with:</p><pre><code>import { Telegraf } from 'telegraf';
import { FoodBotCommands, IFoodContext } from '../types';

// set up bot update handlers
const botCommandHandlers = (bot: Telegraf&lt;IFoodContext&gt;): void =&gt; {
  // Sends a default response to text that doesn't match
  // any registered commands
  bot.on('text', async (ctx) =&gt;
    ctx.reply(
      `Hello! I'm SimpleCuisineBot. I know a ton ` +
        `of recipes, you just need to select a ` +
        `cuisine to get started: \n\n` +
        `/${FoodBotCommands.showCuisines}`
    )
  );

  // default error handler
  bot.catch(async (error: unknown, ctx) =&gt; {
    console.log('Bot error', error);

    await ctx.reply('Sorry, something went wrong while handling your message.');
  });
};

export default botCommandHandlers;
</code></pre><p>There are different handlers for different types of updates, the <code>bot.on("text")</code> is the default handler for any text message. Handlers are matched in the order they are defined so we'll add more specific handlers above it and leave messages that don't match any other handlers to it. In this case, we're sending a default message in the default handler.</p><p><code>bot.catch</code> is the default handler for any errors that occur in the application. We'll add more error handling logic later.</p><h4>Create session middleware</h4><p>If you recall in <code>src/types</code> we defined <code>IFoodContext</code>, in which we added additional properties to the default <code>Context</code> created by Telegraf. Among them was a <code>session</code> property, to make sure that this property is available in all instances of <code>Context</code> in the event handlers we need to create a custom middleware to create in on every update. Create a <code>src/telegram/middlewares/createSession.ts</code> file. Add the following code:</p><pre><code>import { MiddlewareFn } from  'telegraf';
import { IFoodContext } from  '../../types';

/**
* Adds session property to the user context
* whenever a request is received
* */
const  createSession: MiddlewareFn&lt;IFoodContext&gt; = async (ctx, next) =&gt; {
    Object.assign(ctx, { ...ctx, session: {} });

    return  next();
};

export  default  createSession;
</code></pre><h4>Initialize the bot</h4><p>Create a <code>src/telegram/index.ts</code> file. Here's where the telegram bot will be initialized. We're going to add middlewares, setup handlers for commands sent to the bot, send telegram a list of all the commands that the bot handles and set up the mechanism for receiving updates from telegram:</p><pre><code>import { Telegraf } from 'telegraf';
import { BotCommand } from 'telegraf/typings/core/types/typegram';
import config from '../config';
import { IFoodContext, FoodBotCommands } from '../types';
import botCommandHandlers from './handlers';
import createSession from './middlewares/createSession';

// list of commands the bot will handle
export const botCommands: readonly BotCommand[] = [
  {
    command: FoodBotCommands.showCuisines,
    description: 'show available cuisines',
  },
];

export const bot = new Telegraf&lt;IFoodContext&gt;(config.telegramBot.token);

export const startTelegramBot = async (): Promise&lt;void&gt; =&gt; {
  // register middlewares
  const middlewares = [createSession];
  middlewares.forEach((middleware) =&gt; bot.use(middleware));

  // setup command handlers
  botCommandHandlers(bot);

  // register available bot commands on telegram server
  await bot.telegram.setMyCommands(botCommands);

  if (!config.isProduction) {
    // use polling mode in development
    bot.launch();
    console.log("Bot polling for updates..")
  } else {
    // use telegram webhookurl in prod
    bot.telegram.setWebhook(config.telegramBot.webhookUrl);
  }
};
</code></pre><p>The structure of each <code>botCommand</code> in the <code>botCommands</code> list is dictated by the <code>BotCommand</code> type. <code>botCommands</code> list is the argument to <code>bot.telegram.setMyCommands</code> which sends telegram the list of commands.</p><p>In order to receive updates from telegram such as new messages, we can either use <code>polling</code> mode or provide a <code>webhookUrl</code> for telegram to call whenever there's an update. We're going to use <code>polling</code> mode while building the bot locally and set a <code>webhookUrl</code> when in production (in case you want to deploy the bot).</p><h4>Add webhook handler</h4><p>Create a <code>src/api/index.ts</code> file:</p><pre><code>import express from 'express';
import config from '../config';
import { bot } from '../telegram';

const app = express();

// webhook to handle updates from telegram in prod
app.use(bot.webhookCallback(`/${config.telegramBot.token}`));

export default app;
</code></pre><p>The path to the webhook handler is the same as <code>config.telegramBot.webhookUrl</code> since we're just adding a <code>/config.telegramBot.token</code> route to the base app URL.</p><p><code>bot.webhookCallback</code> passes messages received from Telegram by calls to this route to the bot.</p><h4>Start the project</h4><p>In <code>src/index.ts</code> we'll add code to start the bot.</p><pre><code>import  app  from  './api';
import  config  from  './config';
import { startTelegramBot } from  './telegram';

const  startProject = async () =&gt; {
    // initialize telegram bot
    try {
        await startTelegramBot();
        console.log('Bot initialized successfully');
    } catch (error) {
        console.log('Something went wrong while initializing bot');
        console.log(error);
        process.exit(1);
    }

    // start API server
  app
    .listen(config.port, () =&gt; {
      console.info(`Server listening on port: ${config.port}`);
    })
    .on('error', (error) =&gt; {
      console.error(error);
      process.exit(1);
    });
};

startProject();
</code></pre><p>If we encounter any errors while initializing the bot or starting the server, we terminate the process.</p><p>Since we're using polling mode for development, the API server will only be started in a production environment.</p><p>At this point the structure of your project should look like this:</p><pre><code>&#9500;&#9472;&#9472; .env
&#9500;&#9472;&#9472; package.json
&#9500;&#9472;&#9472; src
&#9474;   &#9500;&#9472;&#9472; api
&#9474;   &#9474;   &#9492;&#9472;&#9472; index,ts
&#9474;   &#9500;&#9472;&#9472; config.ts
&#9474;   &#9500;&#9472;&#9472; index.ts
&#9474;   &#9500;&#9472;&#9472; telegram
&#9474;   &#9474;   &#9500;&#9472;&#9472; handlers.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; index.ts
&#9474;   &#9474;   &#9492;&#9472;&#9472; middlewares
&#9474;   &#9474;       &#9492;&#9472;&#9472; createSession.ts
&#9474;   &#9492;&#9472;&#9472; types.ts
&#9500;&#9472;&#9472; tsconfig.json
&#9492;&#9472;&#9472; yarn.lock
</code></pre><h3>Building the features</h3><h4>Show a list of available cuisines</h4><p>From the Spoonacular API docs, I've gathered a list of the available cuisines so we'll be using that. Create a <code>src/cuisineList.ts</code> file for the cuisines:</p><pre><code>const cuisineList: string[] = [
  'african',
  'american',
  'british',
  'cajun',
  'caribbean',
  'chinese',
  'eastern european',
  'european',
  'french',
  'german',
  'greek',
  'indian',
  'irish',
  'italian',
  'japanese',
  'jewish',
  'korean',
  'latin american',
  'mediterranean',
  'mexican',
  'middle eastern',
  'nordic',
  'southern',
  'spanish',
  'thai',
  'vietnamese',
];

export default cuisineList;
</code></pre><p>We'll use a telegram inline menu to display the list of available cuisines and to create the menus we are going to make use of <code>telegraf-inline-menu</code> to which we installed earlier.</p><p>First, we need to update our types in <code>src/types.ts</code>. We need an additional <code>cuisines</code> field in <code>ISessionData</code>, which will contain a list of recipes</p><pre><code>interface ISessionData {
  cuisines?: string[];
  page?: number;
  itemCount?: number;
}
</code></pre><p>Also, we need to create a <code>src/constants.ts</code> file for a few constants that we'll be needing such as the API URL and the number of rows to display on each menu page. I set it to five here but you can make it any number you want:</p><pre><code>export  const  SPOONACULAR_API_URL = 'https://api.spoonacular.com';

// maximum number of rows in any menu page
export  const  MAXIMUM_MENU_ROWS = 5;
</code></pre><p>Next, for the cuisine list menu itself, create a <code>src/telegram/menus/cuisineListMenu.ts</code> file:</p><pre><code>import { Body, MenuTemplate } from 'telegraf-inline-menu/dist/source';
import { ConstOrContextPathFunc } from 'telegraf-inline-menu/dist/source/generic-types';
import { IFoodContext } from '../../types';
import cuisineList from '../../cuisineList';
import { MAXIMUM_MENU_ROWS } from '../../constants';

const cuisineListMenuLogic: ConstOrContextPathFunc&lt;IFoodContext, Body&gt; = (ctx) =&gt; {
  const { page } = ctx.session;

  // no of cuisines to skip in current menu page
  let offset = 0;

  // skip cuisines in previous pages
  if (page) {
    offset = (page - 1) * MAXIMUM_MENU_ROWS;
  }

  // list of cuisines to display for current menu
  // page
  ctx.session.cuisines = cuisineList.slice(
    offset,
    offset + MAXIMUM_MENU_ROWS + 1
  );

  // store total cuisine count to allow pagination method
  // calculate no of pages in current menu
  ctx.session.itemCount = cuisineList.length;

  const text = 'Select a cuisine to get recipes for';

  return text;
};

const cuisineListMenu = new MenuTemplate&lt;IFoodContext&gt;(cuisineListMenuLogic);

// add cuisines in list to menu
cuisineListMenu.choose('selectedCuisine', (ctx) =&gt; ctx.session.cuisines || [], {
  do: async (ctx, key) =&gt; {
    await ctx.reply(`You selected ${key} cuisine!`);
    await ctx.answerCbQuery();

    return false;
  },
  buttonText: (ctx, key) =&gt; key,
  disableChoiceExistsCheck: true,
  maxRows: MAXIMUM_MENU_ROWS,
  columns: 1,
});

// add buttons to paginate cuisine list over several
// menu pages
cuisineListMenu.pagination('cuisineListPagination', {
  setPage: (ctx, page) =&gt; {
    ctx.session.page = page;
  },
  getCurrentPage: (ctx) =&gt; ctx.session.page,
  getTotalPages: (ctx) =&gt; (ctx.session.itemCount as number) / MAXIMUM_MENU_ROWS,
});

export default cuisineListMenu;
</code></pre><p>Alright, let's talk about what the code above is doing:</p><p>We start by defining <code>cuisineListMenuLogic</code> which is a function to initialize the <code>cuisineListMenu</code>. This function is run whenever the menu is loaded or reloaded.</p><p>To keep the menu interface neat we're going to split the static list of cuisines over several pages and <code>cuisineListMenuLogic</code> is where we put the logic to determine the cuisines to be displayed on the current page of the menu.</p><p>In <code>cuisineListMenuLogic</code> we try to determine the current page we're on and using the current page number in addition to the number of rows to display per menu page we select the cuisines to be displayed on the current page. We then store the current list of cuisines to display and the total number of cuisines altogether in <code>ctx.session</code> to make it available to other functions in the menu.</p><pre><code>const cuisineListMenu = new MenuTemplate&lt;IFoodContext&gt;(cuisineListMenuLogic);
</code></pre><p>Afterwards we instantiate <code>cuisineListMenu</code> .</p><p>The <code>.choose</code> method on a menu creates a group of buttons on the menu and attaches an action to be performed whenever any of the choices is selected. It takes an action prefix, a list of choices and options to configure the behaviour of this group of buttons respectively.</p><ul><li><p>action prefix: Every sub-menu, menu or group of buttons attached to a menu in <code>telegraf-inline-menu</code> requires an identifier, it can be anything but identifiers for all paths under a specific menu have to be unique. Here <code>selectedCuisine</code> is the action prefix.</p></li><li><p>choices: Choices the user can pick from. The choices here are the list of cuisines on the current page.</p></li><li><p>options:</p><ul><li><p><code>do</code>: is the action to be performed when an option is selected. Currently, we send a message containing the selected cuisine's name whenever a cuisine is selected by using <code>ctx.reply</code>. <code>ctx.reply</code> is a shorthand method for sending messages back to the chat in the current context. <code>ctx.answerCbQuery</code> is a shorthand method to let telegram know that the action for that choice has been completed so it stops displaying a progress bar for it. The return value of this function determines the behaviour of the menu after the action is completed, returning false makes the menu do nothing.</p></li><li><p><code>buttonText</code>: is used to determine the text to be displayed on the buttons added to the menu. We use the key which is the name of each cuisine for its respective button text.</p></li><li><p><code>disableChoiceExistsCheck</code>: <code>telegraf-inline-menu</code> tries to prevent choices not present from being selected. Since we build the choices dynamically by selecting a range of cuisines when the menu is initially loaded or reloaded then by the time a user selects a cuisine, the check for the choice would always fail since the cuisine list would be empty so we need to disable it.</p></li><li><p>maxRows: Number of rows to use in displaying cuisines on each menu page.</p></li><li><p>columns: Number of columns to use in displaying cuisines.</p></li></ul></li></ul><p>Next, we set up the pagination buttons for the menu by calling the <code>.pagination</code> method. <code>.pagination</code> takes an action prefix and pagination options. Let's go over the pagination options:</p><ul><li><p>setPage: Function to run whenever a page is selected</p></li><li><p>getCurrentPage: Returns the current page the menu is on.</p></li><li><p>getTotalPages: Used in creating the navigation buttons for a menu. We calculate the number of pages here by using the total number of cuisines and the number of rows to be used in displaying cuisines on each page.</p></li></ul><p>To use this menu in the bot we need to create a menu middleware. Create a <code>src/telegram/middlewares/menuMiddleware.ts</code> file:</p><pre><code>import { MenuMiddleware } from 'telegraf-inline-menu/dist/source';
import { IFoodContext } from '../../types';
import cuisineListMenu from '../menus/cuisineListMenu';

/**
 * Middleware needed to track and respond to inline
 * menu button clicks
 */
const menuMiddleware = new MenuMiddleware&lt;IFoodContext&gt;('/', cuisineListMenu);

export default menuMiddleware;
</code></pre><p><code>MenuMiddleware</code> takes a root menu to render by default and a root path for that menu.</p><p>Register this middleware in <code>src/telegram/index.ts</code>:</p><pre><code>import  menuMiddleware  from  './middlewares/menuMiddleware';

// register middlewares
const  middlewares = [createSession, menuMiddleware];
</code></pre><p>Finally, add a command handler to render <code>cuisineListMenu</code>. Let's add a new handler in <code>src/telegram/handlers.ts</code>.</p><pre><code>import  menuMiddleware  from  './middlewares/menuMiddleware';

// show list of all cuisines
bot.command(FoodBotCommands.showCuisines, async (ctx) =&gt;
  menuMiddleware.replyToContext(ctx)
);
</code></pre><p><code>.replyToContext</code> sends a menu in response to a message. Since we didn't specify a path argument, menu middleware sends the default <code>cuisineListMenu</code>.</p><p>Start the bot locally by running:</p><pre><code>yarn run dev
</code></pre><p>You'll see an output like this on the console:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Tc2Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 424w, https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 848w, https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 1272w, https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 424w, https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 848w, https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 1272w, https://substackcdn.com/image/fetch/$s_!Tc2Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6c8765f-aabc-454b-ac2d-1d9cb03775f2_633x159.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Finding the bot on telegram</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LKEz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LKEz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 424w, https://substackcdn.com/image/fetch/$s_!LKEz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 848w, https://substackcdn.com/image/fetch/$s_!LKEz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 1272w, https://substackcdn.com/image/fetch/$s_!LKEz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LKEz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;botSearch.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="botSearch.png" title="botSearch.png" srcset="https://substackcdn.com/image/fetch/$s_!LKEz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 424w, https://substackcdn.com/image/fetch/$s_!LKEz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 848w, https://substackcdn.com/image/fetch/$s_!LKEz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 1272w, https://substackcdn.com/image/fetch/$s_!LKEz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35aab567-39c6-41d9-beee-93dd3ae7ad08_374x240.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Messaging the bot we get:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!b3-n!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!b3-n!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 424w, https://substackcdn.com/image/fetch/$s_!b3-n!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 848w, https://substackcdn.com/image/fetch/$s_!b3-n!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 1272w, https://substackcdn.com/image/fetch/$s_!b3-n!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!b3-n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!b3-n!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 424w, https://substackcdn.com/image/fetch/$s_!b3-n!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 848w, https://substackcdn.com/image/fetch/$s_!b3-n!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 1272w, https://substackcdn.com/image/fetch/$s_!b3-n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00a0ffa2-43e2-4041-abdf-5f8e1847ab34_700x612.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h4>Fetch recipes for a specific cuisine</h4><p>Users should get a list of recipes when they select a cuisine. The list of recipes for a particular cuisine will also be displayed on a menu.</p><p>First, we need to update our types in <code>src/types.ts</code>. We need an additional <code>recipes</code> field in <code>ISessionData</code>, which will contain a list of recipes</p><pre><code>interface ISessionData {
  recipes?: IRecipe[];
  cuisines?: string[];
  page?: number;
  itemCount?: number;
}
</code></pre><p>Add a few additional types:</p><pre><code>/**
 * Recipe item returned by spoonacular
 */
export interface IRecipe {
  id: number;
  title: string;
  image: string;
  imageType: string;
}

/**
 * Spoonacular recipes search response
 */
export interface IRecipesResponse {
  results: IRecipe[];
  offset: number;
  number: number;
  totalResults: number;
}
</code></pre><p><code>IRecipe</code> represents a recipe item returned by the spoonacular API.</p><p><code>IRecipesResponse</code> represents the result of requesting a list of recipes under a particular cuisine.</p><p>Create a new <code>src/telegram/menus/recipeListMenu.ts</code> file. We'll define the <code>recipeListMenu</code> to display the recipes under a cuisine.</p><pre><code>import axios from 'axios';
import { createBackMainMenuButtons, MenuTemplate } from 'telegraf-inline-menu';
import { ConstOrContextPathFunc } from 'telegraf-inline-menu/dist/source/generic-types';
import { SPOONACULAR_API_URL, MAXIMUM_MENU_ROWS } from '../../constants';
import config from '../../config';
import { IFoodContext, IRecipesResponse } from '../../types';

/**
 *  Fetch recipes from spoonacular
 */
const fetchRecipes = async (cuisine: string, page: number) =&gt; {
  const { data } = await axios.get&lt;IRecipesResponse&gt;(
    `${SPOONACULAR_API_URL}/recipes/complexSearch`,
    {
      params: {
        apiKey: config.spoonacular.apiKey,
        cuisine,
        number: MAXIMUM_MENU_ROWS, // number of recipes to return at a time
        // skip recipes in previous pages
        offset: page * MAXIMUM_MENU_ROWS,
      },
    }
  );

  return data;
};

/**
 * Logic for controlling recipe list menu
 */
const recipeListMenuLogic: ConstOrContextPathFunc&lt;IFoodContext, string&gt; =
  async (ctx) =&gt; {
    const { page } = ctx.session;
    const cuisine = ctx.match ? ctx.match[ctx.match.length - 1] : '';

    // check that selected cuisine is not empty
    if (!cuisine) {
      throw new Error('Cuisine not provided');
    }

    // fetch recipes list from spoonacular
    const data = await fetchRecipes(cuisine, page ? page - 1 : 0);

    let text = '';

    if (data.results.length === 0) {
      text = 'There are no recipes available for the selected cuisine:';
    } else {
      text = `Here are the recipes available for the selected cuisine '${cuisine}' :`;
      ctx.session.recipes = data.results;
      ctx.session.itemCount = data.totalResults;
    }

    return text;
  };

/**
 * Menu to list all recipes
 * */
const recipesListMenu = new MenuTemplate&lt;IFoodContext&gt;(recipeListMenuLogic);

// Add each available recipe to menu
recipesListMenu.choose(
  'showRecipe',
  (ctx) =&gt; {
    const { recipes } = ctx.session;

    // constructs choices list out of recipe ids
    return recipes ? recipes.map((recipe) =&gt; recipe.id) : [];
  },
  {
    do: async (ctx, key) =&gt; {
      await ctx.reply(`Recipe ID: ${key}`);
      await ctx.answerCbQuery();

      return false;
    },
    buttonText: (ctx, key) =&gt; {
      const { recipes } = ctx.session;

      const currentRecipe = recipes?.find(
        (recipe) =&gt; recipe.id === Number(key)
      );

      return currentRecipe ? currentRecipe.title : '';
    },
    columns: 1,
    disableChoiceExistsCheck: true,
    maxRows: MAXIMUM_MENU_ROWS,
  }
);

// Paginates recipes results
recipesListMenu.pagination('recipeListItem', {
  setPage: (ctx, page) =&gt; {
    ctx.session.page = page;
  },
  getCurrentPage: (ctx) =&gt; ctx.session.page,
  getTotalPages: (ctx) =&gt; (ctx.session.itemCount as number) / MAXIMUM_MENU_ROWS,
});

// enable navigating to previous and main menu
recipesListMenu.manualRow(
  createBackMainMenuButtons('previous', 'Back to cuisines list')
);

export default recipesListMenu;
</code></pre><p>That's a lot, let's go through what each section is doing:</p><p>To start with, <code>fetchRecipes</code> is a utility function that fetches a list of recipes for a particular cuisine from the spoonacular API. It uses Axios to make an HTTP request and we define the response data type as the <code>IRecipesResponse</code> type we just added to <code>src/types.ts</code>.</p><p>The <code>/recipes/complexSearch</code> endpoint takes a number of parameters which you can check on the <a href="recipes/complexSearch">offical docs</a> but we're only using four of them for the purpose of this tutorial:</p><ul><li><p>apiKey: This is your API key from Spoonacular and is required for every API call.</p></li><li><p>cuisine: Cuisine name to get recipes for</p></li><li><p>number: Number of recipes to return at a time. We're making use of the <code>MAXIMUM_MENU_ROWS</code> constant we created earlier since the number of recipes we show at a time is limited.</p></li><li><p>offset: Number of recipes to skip. We use this in addition to the <code>number</code> param to paginate the returned recipes list.</p></li></ul><p><code>recipeListMenuLogic</code> is the function that <code>recipeListMenu</code> runs when loaded. Remember that menus have identifier's? The full path to a menu is included in <code>ctx.match</code> and the selected cuisine is included in the path so we can get it from there. <code>recipeListMenuLogic</code> then fetches recipes for the selected cuisine from spoonacular and sets a message depending on the API response.</p><p>The arguments for <code>recipeListMenu.choose</code> are similar to <code>cuisineListMenu</code> with a few differences:</p><ul><li><p>For the list of choices, we return the recipe IDs instead of the recipe names because the full path to each choice is included in the <a href="https://core.telegram.org/bots/api#callbackquery">callback data</a> for each button representing a choice on the menu and Telegram limits the size of callback data to 1 - 64 bytes. Since some recipes might have really long names, using their IDs is more reliable.</p></li><li><p><code>do</code>: sends the recipe ID when a choice is selected</p></li><li><p><code>buttonText</code>: tries to get the matching recipe name for each recipe ID passed to it as a key.</p></li></ul><p>We then add pagination to this menu and then manually create a new row of buttons on the menu using <code>recipesListMenu.manualRow</code>.</p><p><code>createBackMainMenuButtons</code> create buttons to navigate to the previous menu if the previous menu is not the root menu and to the root menu directly. The arguments to it are the names to be shown on each button which are the previous menu button text and the root menu button text respectively. Since <code>recipeListMenu</code> is only one path down from the <code>cuisineListMenu</code>, only the root menu navigation button will be displayed.</p><p>Next, we have to connect <code>cuisineListMenu</code> and <code>recipeListMenu</code> such that when a cuisine button is selected, the recipes for that cuisine are displayed.</p><p>Update <code>src/telegram/menus/cuisineListMenu.ts</code>. Replace <code>cuisineListMenu.choose</code> with a new <code>cuisineListMenu.chooseIntoSubmenu</code>:</p><pre><code>import recipesListMenu from './recipeListMenu';

// Show cuisine recipes when cuisine is selected
cuisineListMenu.chooseIntoSubmenu(
  'recipeList',
  (ctx) =&gt; ctx.session.cuisines || [],
  recipesListMenu,
  {
    buttonText: (ctx, key) =&gt; key,
    disableChoiceExistsCheck: true,
    maxRows: MAXIMUM_MENU_ROWS,
    columns: 1,
  }
);
</code></pre><p>Now, let's test the bot so far:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GDS6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GDS6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 424w, https://substackcdn.com/image/fetch/$s_!GDS6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 848w, https://substackcdn.com/image/fetch/$s_!GDS6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 1272w, https://substackcdn.com/image/fetch/$s_!GDS6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GDS6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!GDS6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 424w, https://substackcdn.com/image/fetch/$s_!GDS6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 848w, https://substackcdn.com/image/fetch/$s_!GDS6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 1272w, https://substackcdn.com/image/fetch/$s_!GDS6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63a4ffb7-3493-416d-885e-5df6d0584f97_845x333.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PiWY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PiWY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 424w, https://substackcdn.com/image/fetch/$s_!PiWY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 848w, https://substackcdn.com/image/fetch/$s_!PiWY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 1272w, https://substackcdn.com/image/fetch/$s_!PiWY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PiWY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!PiWY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 424w, https://substackcdn.com/image/fetch/$s_!PiWY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 848w, https://substackcdn.com/image/fetch/$s_!PiWY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 1272w, https://substackcdn.com/image/fetch/$s_!PiWY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d93253c-a71e-47d1-9107-9266ca408b26_838x377.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h4>Get preparation instructions for a specific recipe</h4><p>To display preparation instructions on selecting a specific recipe, we start by adding even more types to <code>src/types.ts</code>:</p><pre><code>/**
 * Individual recipe instruction step
 */
interface IRecipeStep {
  number: number;
  step: string;
  ingredients: {
    name: string;
  }[];
}

/** Recipe instructions from spoonacular */
interface IRecipeInstructions {
  name: string;
  steps: IRecipeStep[];
}

/**
 * Full recipe information from Spoonacular
 */
export interface IRecipeInformation {
  title: string;
  image: string;
  extendedIngredients: {
    original: string;
  }[];
  analyzedInstructions: IRecipeInstructions[];
}
</code></pre><p><code>IRecipeInformation</code> represents a part of the full details of a recipe that we're concerned with as returned by spoonacular. We've broken this information into several types simply to make it easier to reason about</p><p><code>IRecipeInstructions</code> represents the preparation instructions for a recipe and <code>IRecipeStep</code> is each of the individual steps to be followed in order.</p><p>Next, we have to update <code>src/telegram/menus/recipeListMenu.ts</code>:</p><ul><li><p>Update the imports:</p><pre><code>import { IFoodContext, IRecipeInformation, IRecipesResponse } from  '../../types';
</code></pre></li><li><p>Create a new function to send the recipe instructions rather than a simple message containing the recipe ID:</p><pre><code>/**
* Show recipe information on telegram
*/
const showRecipeInformation = async (ctx: IFoodContext, recipeId: string) =&gt; {
  const { data } = await axios.get&lt;IRecipeInformation&gt;(
    `${SPOONACULAR_API_URL}/recipes/${recipeId}/information`,
    {
      params: {
        apiKey: config.spoonacular.apiKey,
      },
    }
  );

  // format list of ingredients
  const ingredients = `Ingredients needed for this recipe are:\n\n${data.extendedIngredients
    .map((ingredient) =&gt; `- ${ingredient.original}`)
    .join('\n')}`;

  // format recipe instruction steps
  const instructions =
    `Steps to prepare:\n\n` +
    `${
      data.analyzedInstructions.length
        ? data.analyzedInstructions[0].steps
            .map((step) =&gt; `${step.number}. ${step.step}`)
            .join('\n\n')
        : 'No steps to display &#128517;'
    }`;

  // full message
  const message = `${data.title}\n\n${ingredients}\n\n\n${instructions}`;

  await ctx.replyWithPhoto(data.image);
  await ctx.reply(message);
  await ctx.answerCbQuery();

  return false;
};
</code></pre><p>In <code>showRecipeInformation</code>, we fetch the information for a recipe then concatenate the recipe name with its list of ingredients and instruction steps into a single message.</p></li></ul><p>Then we send a picture of the finished recipe along with the concatenated message to the user.</p><ul><li><p>Pass the new function to <code>do</code> in <code>recipeListMenu.choose</code>:</p><pre><code>do:  async (ctx, key) =&gt;  showRecipeInformation(ctx, key),
</code></pre><p>Let's start the bot locally to test again:</p></li></ul><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LyNe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LyNe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 424w, https://substackcdn.com/image/fetch/$s_!LyNe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 848w, https://substackcdn.com/image/fetch/$s_!LyNe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 1272w, https://substackcdn.com/image/fetch/$s_!LyNe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LyNe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d7229d85-3891-4395-9dc5-0ea074894b83_846x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!LyNe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 424w, https://substackcdn.com/image/fetch/$s_!LyNe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 848w, https://substackcdn.com/image/fetch/$s_!LyNe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 1272w, https://substackcdn.com/image/fetch/$s_!LyNe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7229d85-3891-4395-9dc5-0ea074894b83_846x675.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LleA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LleA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 424w, https://substackcdn.com/image/fetch/$s_!LleA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 848w, https://substackcdn.com/image/fetch/$s_!LleA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 1272w, https://substackcdn.com/image/fetch/$s_!LleA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LleA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!LleA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 424w, https://substackcdn.com/image/fetch/$s_!LleA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 848w, https://substackcdn.com/image/fetch/$s_!LleA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 1272w, https://substackcdn.com/image/fetch/$s_!LleA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F873e54dd-4455-4305-8bd7-aadfab3fe97a_837x674.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gwN9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gwN9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 424w, https://substackcdn.com/image/fetch/$s_!gwN9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 848w, https://substackcdn.com/image/fetch/$s_!gwN9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 1272w, https://substackcdn.com/image/fetch/$s_!gwN9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gwN9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;image.png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="image.png" title="image.png" srcset="https://substackcdn.com/image/fetch/$s_!gwN9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 424w, https://substackcdn.com/image/fetch/$s_!gwN9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 848w, https://substackcdn.com/image/fetch/$s_!gwN9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 1272w, https://substackcdn.com/image/fetch/$s_!gwN9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4aa69e8f-4c77-4858-ac6c-8176bbd6d47e_854x144.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p> And everything works fine!</p><p>We're almost done, we just need to update our error handling logic. Update <code>bot.catch</code> in <code>src/telegram/handlers.ts</code>:</p><ul><li><p>Update the imports</p></li></ul><pre><code>import { AxiosError } from  'axios';
import { CallbackQuery } from  'telegraf/typings/core/types/typegram';
</code></pre><ul><li><p>Update the function in <code>bot.catch</code>:</p><pre><code>// default error handler
bot.catch(async (error: unknown, ctx) =&gt; {
 console.log('Bot error', error);

 // remove progress bar from menu button on error
 if ((ctx.callbackQuery as CallbackQuery.DataCallbackQuery)?.data)
   await ctx.answerCbQuery();

 // handle axios errors
 if ((error as AxiosError).isAxiosError) {
   const status = (error as AxiosError).response?.status;

   // handle daily quota limit error from Spoonacular
   // https://spoonacular.com/food-api/docs#Quotas
   if (status === 402) {
     await ctx.reply(
       '&#128517; Sorry, we seem to have reached our daily quota limit for the ' +
         'spoonacular API and cannot handle any more requests today'
     );

     return;
   }
 }

 await ctx.reply('Sorry, something went wrong while handling your message.');
});
</code></pre><p>We're dealing with two things here:</p><ul><li><p>Remember that telegram uses callback data to handle menu logic? So we call <code>ctx.answerCbQuery</code> if an error occurs while handling a message that has callback data to notify Telegram to stop showing a progress bar for that menu item.</p></li><li><p>Spoonacular has daily quota limits for requests to its API. Once this limit is exceeded, all API calls are rejected with an HTTP status code<code>402</code>. So we need to handle that error when it occurs in an Axios request.</p></li></ul></li></ul><p>And that's all!</p>]]></content:encoded></item></channel></rss>