<?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"><channel><title><![CDATA[Karol Horosin]]></title><description><![CDATA[Articles about applied AI, software engineering & product from staff engineer and consultant, ex Head of Engineering, founder.]]></description><link>https://horosin.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 17 May 2026 12:00:21 GMT</lastBuildDate><atom:link href="https://horosin.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building for myself: simple tools, real use]]></title><description><![CDATA[I don’t usually trust online tools- but they’re just too convenient.
Whether it’s editing PDFs, converting files, or tweaking images, you name it.
So, to keep things private, I built a few simple ones myself.
Why toolou?
I needed a place to host thes...]]></description><link>https://horosin.com/building-for-myself-simple-tools-real-use</link><guid isPermaLink="true">https://horosin.com/building-for-myself-simple-tools-real-use</guid><category><![CDATA[AI]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Wed, 04 Jun 2025 09:00:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747312790250/b209e539-bfb2-42c2-8d0b-1a94a9bef0c8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I don’t usually trust online tools- but they’re just too convenient.</p>
<p>Whether it’s editing PDFs, converting files, or tweaking images, you name it.</p>
<p>So, to keep things private, I built a few simple ones myself.</p>
<h2 id="heading-why-toolou">Why toolou?</h2>
<p>I needed a place to host these tools, so I've created <a target="_blank" href="https://toolou.com/">toolou.com</a></p>
<p>Silly name, basic layout, pretty much vibe-coded with v0 and Copilot Edits.</p>
<p>My favourites so far: Word to markdown converter, JSON formatter, and LinkedIn date finder. Let’s take a look!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747311039924/fce3ae9c-d51e-430c-a1d5-1a418b075f58.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-word-to-markdown-converter">Word to markdown converter</h2>
<p>I wanted to paste documents into LLMs. Some of them, including OpenAI thinking models, do not allow document upload.</p>
<p>I wanted a quick way to go from .docx to markdown, without uploading them into shady web tools.</p>
<p>So I just created a tool on my own!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747311090543/becde1d0-bf89-4d36-887d-ebcb680e3328.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-json-formatter">JSON formatter</h2>
<p>It’s as simple as they come, but easily my most-used tool.</p>
<p>Especially helpful when I’m already in the browser and for some reason don’t feel like opening a code editor.</p>
<p>It was also the first one I built, and the one that got this whole thing started.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747311319681/25172e7c-22a0-4b45-8d72-564bdae046d0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-linkedin-date-finder">LinkedIn date finder</h2>
<p>Definitely the weirdest one.</p>
<p>I built it because I wanted to see when posts were actually published. Sometimes a post felt oddly outdated, but LinkedIn only shows “1y”- which can mean anything from 12 to 24 months.</p>
<p>I also wanted to get a sense of the best times to post.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747311489049/f46d4c8d-592c-4433-a08a-43ce967a0a0b.png" alt class="image--center mx-auto" /></p>
<p>I even made a chrome extension based on it!</p>
<p>This was a fun little project on it's own.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747311534649/9a73cc7c-77e4-4141-95dc-4941abc420a6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-summing-up">Summing up</h2>
<p>Are any of these revolutionary? Not at all.</p>
<p>Did I have fun? Absolutely.</p>
<p>Building something just for myself was surprisingly satisfying. You should give it a try too!</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a><strong>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[AI made my game while I waited for coffee]]></title><description><![CDATA[So, I took part in #vibejam—a challenge to build a game in under two weeks, with the twist that over 80% of the code had to be written using AI.
Here’s the result: galacticsiege.com. It’s very unfinished, but it exists. I’m now rehauling it to be mor...]]></description><link>https://horosin.com/ai-made-my-game-while-i-waited-for-coffee</link><guid isPermaLink="true">https://horosin.com/ai-made-my-game-while-i-waited-for-coffee</guid><category><![CDATA[AI]]></category><category><![CDATA[vibe coding]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[v0.dev]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Wed, 28 May 2025 10:31:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744834773132/a8612b91-f10c-4909-ad8c-7065b6d42f5b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So, I took part in #vibejam—a challenge to build a game in under two weeks, with the twist that over 80% of the code had to be written using AI.</p>
<p>Here’s the result: <a target="_blank" href="https://galacticsiege.com">galacticsiege.com</a>. It’s very unfinished, but it exists. I’m now rehauling it to be more human dev friendly.</p>
<h2 id="heading-whats-it-about-and-what-did-i-learn">What’s it about, and what did I learn?</h2>
<p>The game is a turn-based strategy inspired by <em>Polytopia</em>. Your goal is to take over every star system on the map by competing with other spacefaring civilisations. You gather energy, build infrastructure, and train units. You've got 30 turns to do it!</p>
<p>I actually used AI for nearly 100% of the code. I only edited about five lines myself, added analytics, threw in the required jam button, and went straight to deployment.</p>
<p>It took around 100 “turns” with @v0 (related to the final code), plus about 30 more in separate chats and random prompts to ChatGPT and Gemini.</p>
<h2 id="heading-when-did-i-find-the-time">When did I find the time?</h2>
<p>I was prompting almost exclusively in between other things—waiting in line at the supermarket, brewing coffee, or killing time during some endless build process. The rough edges of the final game probably reflect that.</p>
<p>I really tried to polish it more, but the LLMs just weren’t computing tokens in my favour. At one point, I couldn’t figure out why a game with zero assets had a loading screen. Turns out v0 had added simulated loading... because that’s what real games do.</p>
<h2 id="heading-why-i-didnt-just-fix-it-myself">Why I didn’t just fix it myself?</h2>
<p>That was the whole point—it was an experiment to see what LLMs can actually do when left to their own devices.</p>
<p>I do want to finish the game eventually. If you check it out, let me know what you think! The gameplay is definitely broken, but the foundation is there.</p>
<h2 id="heading-whats-next">What’s next?</h2>
<p>I’ll be sharing more thoughts on the whole process soon—like how different LLMs perform on various creative tasks (generating 3D models, writing code, debugging, ideation). Maybe I’ll even write a guide on going from AI slop to something actually deployable.</p>
<p>Are you building side projects with AI? Maybe even games? I’d love to hear about it.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a><strong>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The AI coding tool I use the most]]></title><description><![CDATA[There are a lot of coding tools out there. I’ve tried a bunch, and I still use several - like augment code and GitHub Copilot. But if I had to pick the one I reach for most often, or the one I’d recommend first right now? It’s v0. Why?
Speed
For me, ...]]></description><link>https://horosin.com/the-ai-coding-tool-i-use-the-most</link><guid isPermaLink="true">https://horosin.com/the-ai-coding-tool-i-use-the-most</guid><category><![CDATA[AI]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[v0.dev]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Fri, 02 May 2025 11:26:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746185137438/9f9bb5be-6cac-4f14-8a48-6f17ab59d83a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are a lot of coding tools out there. I’ve tried a bunch, and I still use several - like augment code and GitHub Copilot. But if I had to pick the one I reach for most often, or the one I’d recommend first right now? It’s v0. Why?</p>
<h2 id="heading-speed">Speed</h2>
<p>For me, it’s all about how quickly I can go from idea to prototype. I’ve built around 30 different things using v0 so far. It’s still a code-first tool, but the flow is just faster. I can be out on a walk, think of something I want to try, kick off a chat, and check results later. It’s super low-friction, and that matters a lot when you’re juggling ideas.</p>
<p>I’ve even recommended it to friends - and seen them love it too.</p>
<h2 id="heading-real-world-use-case">Real-world use case</h2>
<p>One of my friends, a UX designer, uses it to prepare for workshops with clients. She says it’s a game-changer - she can show something functional that looks close to the final product without sinking hours into building it. That makes feedback way easier and faster.</p>
<h2 id="heading-how-i-use-it">How I use it?</h2>
<p>Here are just a few ways I use v0 regularly:</p>
<ul>
<li><p>Prototyping UIs before starting real dev work</p>
</li>
<li><p>Building little apps for fun and sharing with friends</p>
</li>
<li><p>Exploring side-project and business ideas</p>
</li>
<li><p>Even working on a game (more on that soon)</p>
</li>
</ul>
<p>For all these “scratch” or temporary projects, v0 is amazing. It helps me think through ideas visually and interactively, without getting bogged down in setup. That said - it’s not quite there yet for production-level code.</p>
<h2 id="heading-what-could-be-improved">What could be improved?</h2>
<p>V0 still has some rough edges. Things I’d love to see improved:</p>
<ul>
<li><p>Better Git integration</p>
</li>
<li><p>A more capable code editor</p>
</li>
<li><p>Automatic handling of runtime errors</p>
</li>
<li><p>Smarter visual intelligence (like taking screenshots of its output, evaluating, and improving - right now I do that myself)</p>
</li>
<li><p>Better backend generation when working with integrations (databases, auth, etc. - e.g. Supabase)</p>
</li>
<li><p>Mobile app support</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I’m currently making the most of my paid subscription, and I keep V0 close by for whenever a new idea pops into my head. Some of the projects I’ve shared here actually started there. Only started - it’s what v0 is good for at the moment - growing beyond the prototype is just too frustrating.</p>
<p>I’m also testing out lovable.dev at the moment (BTW there’s a discount code for $25 atm: 25-DISCOUNT-5281).</p>
<p>Have you tried it yet? What’s your take? Want me to drop a few tips or maybe a tutorial?</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a><strong>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Navigating Large Codebases in VS Code]]></title><description><![CDATA[Working with large codebases is different. Let me show you some ways I navigate through massive piles of code.
In small projects you can feel like a magician. With AI wind in your sails, you storm through backlog like crazy. This changes when feature...]]></description><link>https://horosin.com/navigating-large-codebases-in-vs-code</link><guid isPermaLink="true">https://horosin.com/navigating-large-codebases-in-vs-code</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[enterprise]]></category><category><![CDATA[tips]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Wed, 09 Apr 2025 19:21:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744224931897/faf07b64-ee2c-4125-a390-a24d9e6de111.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Working with large codebases is different. Let me show you some ways I navigate through massive piles of code.</p>
<p>In small projects you can feel like a magician. With AI wind in your sails, you storm through backlog like crazy. This changes when features pile up and cost of failure increases.</p>
<p>Here’s what I do:</p>
<p>(For windows in most of the cases replace CMD with CTRL!)</p>
<hr />
<p>🔹 <strong>CMD+P - an absolute killer</strong></p>
<p>Start typing - fuzzy file name search across the project</p>
<p>@ … - go to symbol (variable or function name) in the file you’re in</p>
<p>: … - jump to line number</p>
<hr />
<p>🔹 <strong>CTRL+G - jump to line number shortcut</strong></p>
<p>🔹 <strong>CMD+Shift+o - jump to symbol</strong></p>
<p>🔹 <strong>CMD+Shift+\ - jump to matching bracket</strong> ⭐️</p>
<p>🔹 <strong>CMD+click - go to definition</strong></p>
<hr />
<p>🔹 <strong>CMD+Shift+P and then „Find all references”</strong></p>
<p>It actually finds places where given function, class or whatever is used. Better than just find all.</p>
<p>In general the command opens a command palette, which I use all the time, but it’s the one everyone (likely) knows.</p>
<hr />
<p>🔹 <strong>VS Code Bookmarks! Check out the extension by Alessandro Fragnani</strong></p>
<p>It lets you bookmark lines in files across the project and jump between them.</p>
<p>Huge time saver when working across many files, often cleaner than looking up got diffs.</p>
<hr />
<p>🔹 <strong>Copy Python Path by kawamataryo</strong></p>
<p>In VS code you can easily copy a relative path. But there’s no option to copy an import statement by right clicking on a symbol or a file.</p>
<p>For months I copied the path and replaced slashes with dots. Then I discovered this extension!</p>
<hr />
<p>These are life saving in my daily work.</p>
<p>Which ones do you use?</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a><strong>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to set up free, local coding AI assistant for VS Code]]></title><description><![CDATA[Introduction
I like coding with AI assistants. They let me focus on what matters - shipping quickly while only tuning the most important details like performance, security or design.
You can’t use AI code completion and chat at all times though. You ...]]></description><link>https://horosin.com/how-to-set-up-free-local-coding-ai-assistant-for-vs-code</link><guid isPermaLink="true">https://horosin.com/how-to-set-up-free-local-coding-ai-assistant-for-vs-code</guid><category><![CDATA[llm]]></category><category><![CDATA[AI]]></category><category><![CDATA[General Programming]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Fri, 14 Mar 2025 16:13:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741969092878/654eb89c-6cbd-4177-ba85-f0f8f278d97d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>I like coding with AI assistants. They let me focus on what matters - shipping quickly while only tuning the most important details like performance, security or design.</p>
<p>You can’t use AI code completion and chat at all times though. You may be on a plane or in any other offline situation. There are also times where companies don’t want their code to be sent to third parties. Because of that, they impose strict policies on AI tool usage. That’s where local, open LLMs come in.</p>
<p>How good can local LLMs be for coding? Good enough to be useful but definitely not on par with popular offerings like OpenAI or Anthropic models through GitHub Copilot or Cursor. (Even after DeepSeek release, most people swear by Claude Sonnet).</p>
<p>I’d compare completions quality to the early versions of copilot — sometimes making mistakes but overall useful enough to keep them on. The chat quality is OK but agentic capabilities are functional only on more performant machines.</p>
<p>In this short tutorial, we will:</p>
<ol>
<li><p>Install LM studio and set up our models</p>
</li>
<li><p>Set up Continue - a VS Code and JetBrains IDE extension</p>
<ol>
<li><p>Code completions (inline suggestions)</p>
</li>
<li><p>Chat</p>
</li>
</ol>
</li>
<li><p>Bonus - set up agentic coding with Cline</p>
</li>
</ol>
<p>I’ve tested performance on two machines: M1 MacBook Air (16GB RAM) and M1 Max MacBook Pro (64GB RAM).</p>
<p>The tutorial is compatible with MacOS, Windows and Linux but your model performance may vary. You can also try to replicate this setup in JetBrains IDEs.</p>
<h2 id="heading-setting-up-lm-studio-local-llms">Setting up LM Studio - local LLMs</h2>
<p>In my opinion, LM Studio is the best and easiest to use local LLM UI. It doesn’t have many options other than model inference (like loading documents), but it serves its basic purpose well.</p>
<p>Go ahead, download and install LM Studio - <a target="_blank" href="https://lmstudio.ai/">https://lmstudio.ai</a>.</p>
<p>Open the app. You will be greeted with a quick-start tutorial. Do it if you want or skip it by clicking a button in the upper right corner.</p>
<p>In the sidebar on the left, click 🔍 magnifying glass icon. This will open a model discovery feature which allows you to find and download new LLMs. Make sure to come back here in the future to test some models on your own.</p>
<p>Let’s find and download two models for different purposes.</p>
<ul>
<li><p>Qwen2.5 Coder 3B, Q4_K variant - a small model (~2GB) that will easily fit into your memory. It works great for code completion and will not negatively affect your workflow even if you have only 8GB of RAM. Stick to this one only if you don’t have at least 32GB of memory and a faster GPU (like non-pro Apple M chips)</p>
</li>
<li><p>Qwen/Qwen2.5-Coder-14B-Instruct-GGUF, Q4_K_M variant - a bigger model, better for chat sessions and agentic use. Use 7B versions on machines with 16GB (V)RAM or less - but only for chat, agentic capabilities at these sizes are quite poor.</p>
</li>
</ul>
<p>If you have more RAM and more compute, play around with some thinking models. I didn’t choose a thinking one for this tutorial, because they are usually not fast enough for use while coding. They are useful for tasks that are not time-sensitive though!</p>
<p>Generally, recommended models shown at the top of the list in LM Studio are good. Frustrated with the number of options? Experiment! Things are changing every week so there’s no one, easy recommendation.</p>
<p>Close the “Discover” window by clicking on the X.</p>
<p>Now let’s enable our local LLM server, so that our coding assistant can use our models. Go to “Developer” section (terminal icon). And flip the “Status: stopped” switch so that it changes to “running”.</p>
<p>Then, click “Select model to load” at the top of the window. Load our 3B model with default settings. For 7B/14B I recommend selecting 20,000 context window length if you want to use coding agents. This costs RAM though! Adjust to your needs.</p>
<h2 id="heading-setting-up-continue-open-source-copilot-alternative">Setting up Continue - open source Copilot alternative</h2>
<p>Start by installing the extension - search for “Continue” in VS Code extensions or go to <a target="_blank" href="https://www.continue.dev/">continue.dev</a>. In VS Code, click install. You should see a new icon in your left sidebar. Click on it.</p>
<p>You will see a text box and a quickstart prompt - ignore it for now.</p>
<p>Before using it, let’s turn off GitHub Copilot completions to avoid conflicts. At the top of your screen, near the search bar, find Copilot icon, click on it and choose “Configure Completions” and then “Disable Completions” from the menu. You can turn them on in the same way afterwards.</p>
<h3 id="heading-configuring-chat">Configuring chat</h3>
<p>Let’s go back to Continue sidebar. Click on the model selector (by default showing Claude 3.5 Sonnet at the time of writing). Click “+ Add Chat model”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741968636977/e4b54a7c-f5bb-453e-8c28-d8383446a70b.png" alt class="image--center mx-auto" /></p>
<p>In the popup window select:</p>
<ul>
<li><p>Provider: LM Studio</p>
</li>
<li><p>Model: you can leave autodetect and select it later</p>
</li>
<li><p>Click “Connect”</p>
</li>
</ul>
<p>Now your model selector will show our models. Let’s select one of the installed models and say hi!</p>
<p>You can do all the usual stuff you can do with other coding chats.</p>
<h3 id="heading-configuring-completions">Configuring completions</h3>
<p>To configure code completion click “Continue” on the right of the status bar. Select “Configure autocomplete options” from the menu. You will see a JSON configuration file. Add this as a property of the main object:</p>
<pre><code class="lang-json">  <span class="hljs-string">"tabAutocompleteModel"</span>: {
    <span class="hljs-attr">"apiBase"</span>: <span class="hljs-string">"&lt;http://localhost:1234/v1/&gt;"</span>,
    <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Qwen2.5 Coder 3B"</span>,
    <span class="hljs-attr">"provider"</span>: <span class="hljs-string">"lmstudio"</span>,
    <span class="hljs-attr">"model"</span>: <span class="hljs-string">"qwen2.5-coder-3b-instruct"</span>
  },
</code></pre>
<p>If you want to use another model, just copy it’s name from the “Developer” section of ML Studio.</p>
<p>Hit save, go to some of your code files, start typing and enjoy completions!</p>
<h2 id="heading-optional-agentic-coding-with-cline">(Optional) Agentic coding with Cline</h2>
<p>If you’re into agentic coding (and you should be) and have a strong enough machine, let’s explore how to get these capabilities using local models. I’d recommend doing this only if you can run at least a 14B model. Cursor IDE and Copilot Edits using bigger models perform much better, but hey, we’re running local models here! (Which I hope won’t be a trade off in the near future.)</p>
<ol>
<li><p>Install <a target="_blank" href="https://cline.bot/">Cline</a>.</p>
</li>
<li><p>After installing, find its icon in the sidebar.</p>
</li>
<li><p>You’ll be greeted with a choice: “Get Started for Free” or “Use your own API key”. Select the API key.</p>
</li>
<li><p>Select API Provider - LM Studio.</p>
</li>
<li><p>Pick desired model from the list (here <code>qwen2.5-coder-14b-instruct</code> ).</p>
</li>
<li><p>Click “Let’s go!”</p>
</li>
<li><p>Give the agent a task!</p>
</li>
</ol>
<p>In my experience, the generations are a bit slow (one reason is the <a target="_blank" href="https://github.com/cline/cline/blob/main/src/core/prompts/system.ts">massive</a> Cline system prompt). That said, it is capable of providing useful code, addressing given tasks well.</p>
<h2 id="heading-summary">Summary</h2>
<p>As you can see, local coding assistants have come a long way, making it possible to work with AI even when offline or restricted by company policies. While they may not match the capabilities of cloud-based solutions like GitHub Copilot or Cursor, they offer a practical alternative for those of us who need strict privacy or offline functionality.</p>
<p>The setup works well even on computers with limited resources, though having more RAM and a beefy GPU gives you the flexibility to use larger, more capable models. It might not replace your primary AI coding assistant, but it's definitely worth having as a backup or alternative when privacy and offline access are priorities.</p>
<p>Let me know if you run into any issues or if you have some cool experiences to share!</p>
<p>Check out my other <a target="_blank" href="https://horosin.com/series/large-language-models-ai">posts related to LLMs.</a></p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a><strong>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How I built a browser extension to make LinkedIn less annoying]]></title><description><![CDATA[Genesis
LinkedIn has its issues–not just the hollow content, fake stories, and unoriginal ideas. You can’t even check when someone’s post was published, because the platform hides the exact date behind relative timestamps like "2d" or "1w ago".
I’ve ...]]></description><link>https://horosin.com/how-i-built-a-browser-extension-to-make-linkedin-less-annoying</link><guid isPermaLink="true">https://horosin.com/how-i-built-a-browser-extension-to-make-linkedin-less-annoying</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[chrome extension]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[LinkedIn]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Fri, 24 Jan 2025 13:50:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742294034526/6417454f-306e-4ce1-a38b-e11a71ae1dd2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-genesis">Genesis</h2>
<p>LinkedIn has its issues–not just the hollow content, fake stories, and unoriginal ideas. You can’t even check when someone’s post was published, because the platform hides the exact date behind relative timestamps like "2d" or "1w ago".</p>
<p>I’ve always wanted to create and publish a browser extension. I started a few but never got to releasing them. I guess they were not useful enough. I also found working on the extensions somehow annoying due to limitations of what you can do with them imposed by browsers.</p>
<p>A few days ago I posted about <a target="_blank" href="https://toolou.com/tools/linkedin-post-date">a tool I’ve created allowing to extract LinkedIn post’s exact date</a> from its URL. Someone encouraged me to turn it into a browser extension. Well, two days and several chats with AI later, here I come with the plugin.</p>
<p>I don’t want it to only be about dates, because there are other LinkedIn quirks that I want to change. I’m also looking for something that would give me better analytics about posted content - mine and others’. Hence, I gave it a more general name.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737726283611/94149287-06eb-42be-8e22-c633d93d430d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-a-look-at-the-extension">A look at the extension</h2>
<p>You can give it a try <a target="_blank" href="https://chromewebstore.google.com/detail/linkedin-tools-show-real/bfdkndbadgnlcbaljmghnbgmcaiiiabn">here.</a></p>
<p>It’s simple, the plugin adds a real date to each post it sees on your timeline.</p>
<p><img src="https://toolou.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fscreenshots.b8d2b9f4.jpg&amp;w=3840&amp;q=75" alt="LinkedIn Tools Extension Screenshot" /></p>
<p>It doesn’t work with short post excerpts in the profile view but once you click to see all posts, timestamps are back.</p>
<p>It also works with comments, displaying the date on hover.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737726361712/e491d041-b6da-4bfc-8273-ece0751f1de0.png" alt class="image--center mx-auto" /></p>
<p>Also added a popup for future functionalities.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737726369965/1f648565-771d-405a-9a8d-6e8df3d7459e.png" alt class="image--center mx-auto" /></p>
<p>Uncomplicated, useful, fun to make.</p>
<h2 id="heading-how-to-develop-a-chrome-extension">How to develop a Chrome extension?</h2>
<p>This method worked well for getting something done quickly. I’d say I started from nothing since last time I tried to develop an extension was about 5 years ago and focused on Firefox. The whole process took me around four hours.</p>
<ul>
<li><p>Start with Google’s <a target="_blank" href="https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world">hello world tutorial</a></p>
</li>
<li><p>Also learn <a target="_blank" href="https://developer.chrome.com/docs/extensions/get-started/tutorial/scripts-on-every-tab">how to modify content of web pages</a></p>
</li>
<li><p>Once you have the above running and open in VS Code or Cursor, use Copilot Edits (I used Claude) or Composer mode to quickly make changes. I used Copilot.</p>
</li>
<li><p>Test and iterate. At some point AI won’t help you anymore and you’ll have to make some tweaks yourself.</p>
</li>
<li><p>Once you are happy with the extension, <a target="_blank" href="https://developer.chrome.com/docs/webstore/publish">read how to publish your extension.</a></p>
</li>
<li><p>Create icons and graphics</p>
<ul>
<li><p>I used <a target="_blank" href="https://lucide.dev/icons/">lucide icons</a> and merged them to create a logo.</p>
</li>
<li><p>You can use an editor like <a target="_blank" href="https://www.photopea.com">Photopea</a> - an excellent, free, in-browser Photoshop alternative</p>
</li>
<li><p>Create banners and screenshots per instructions, I’ve also used Photopea</p>
</li>
</ul>
</li>
<li><p>Create a privacy policy and host it somewhere. If you have a blog or a website, put it there. Perhaps a google drive link will work as well? <a target="_blank" href="https://toolou.com/tools/linkedin-post-date/extension/privacy-policy">I've created this short document</a> - also with the help of ChatGPT and Copilot edits.</p>
</li>
<li><p>Publish and wait patiently for review. (24h in my case)</p>
</li>
</ul>
<p>LLMs were a huge help here, I only made some tweaks to the code. Preparing everything for the publication was quite some work.</p>
<h2 id="heading-promotion">Promotion</h2>
<p>I decided to promote the extension on my tools website - <strong>toolou.com.</strong> I just asked Copilot in Edits mode to create a banner and a page based on a functionality description. I’ve asked Copilot Chat to compile it beforehand, based on the extension codebase. I only changed the placeholder URL for extension download page and placed a screenshot in the proper directory.</p>
<p><a target="_blank" href="https://toolou.com/tools/linkedin-post-date">Here it is live.</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737726398475/9962876b-5a2c-401f-9fca-3e66fa9ab338.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737726404581/10b9d9b8-6ad7-4ca0-b3c4-d5c2ff2fd727.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-future">Future</h2>
<p>There’s an elephant in the room – I don’t use Chrome that much. Yeah, I have Edge open all the time (just to have a chromium browser on hand) and it supports Chrome extensions. That said, my daily driver is Safari. I really like how performant and power efficient it is. Since a lot of what I do happens in a browser - it matters. So I’d like to give creating a Safari extension a go. My first impression is that it’s much more complex, because you need to ship it via AppStore, but it makes it even a bit more interesting.</p>
<p>As I mentioned before, I have some other issues with LinkedIn. One of them is that they do not really provide a public API. I cannot programmatically fetch even my own posts or stats. That’s worse than <a target="_blank" href="https://horosin.com/create-a-dashboard-to-track-your-twitterx-follower-stats-with-apis-and-github-actions">Twitter/X, which API at least lets me fetch a count of followers and posts.</a></p>
<p>Since I’ll ‘ll be sending data to my server for analysis, I have more ideas in mind. I’d love to do analytics on posts you see on the timeline as well as provide AI-powered suggestions for better content. Maybe also highlight posts worth engaging with?</p>
<p>For now, <a target="_blank" href="https://chromewebstore.google.com/detail/linkedin-tools-show-real/bfdkndbadgnlcbaljmghnbgmcaiiiabn">try the extension</a>, and let me know what you think.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>.</p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I’ve quietly launched two apps]]></title><description><![CDATA[In November, I decided to publish two of the side projects I’ve been working on. Somewhere between my regular work, personal life, conferences, and writing, I’ve been slowly chugging along with coding. And it was so fun! Rarely in everyday work do yo...]]></description><link>https://horosin.com/ive-quietly-launched-two-apps</link><guid isPermaLink="true">https://horosin.com/ive-quietly-launched-two-apps</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Startups]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Mon, 13 Jan 2025 07:00:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742294533917/66b0c523-5050-4246-b654-33e12e643c1c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In November, I decided to publish two of the side projects I’ve been working on. Somewhere between my regular work, personal life, conferences, and writing, I’ve been slowly chugging along with coding. And it was so fun! Rarely in everyday work do you start something entirely new and bring a fresh idea to life. I had a chance to begin with a concept and quickly iterate towards “passable” for a release. Because these sure aren’t perfect.</p>
<p><a target="_blank" href="https://airportregistry.com/">The first app</a> was supposed to help air travelers with information not available anywhere else. Some parts of the world were getting ready to lift the 100ml/3.4oz liquid limit in carry-on bags thanks to new scanner technology. I wanted to be the first to collect information about airports that have these new scanners. The app would allow you to find out whether you need to worry about the size of your liquids when traveling to your destination.</p>
<p>Sadly, the EU put the change on hold due to problems with the new scanner software. I decided to release the app anyway because I already had all the other information about airports collected in the database. So there’s no liquid limit information, but everything else is there: official websites, locations, airlines involved, etc. I’ve already seen it become useful for people looking for reliable information about smaller airports.</p>
<p>What was cool about working on it? Well, figuring out the deployment and coding sure was exciting, but getting all the data for the website was the biggest challenge. I needed to use multiple data sources, <a target="_blank" href="https://horosin.com/scraping-data-off-wikipedia-three-ways-no-code-and-code-python-pandas-sheets">resort to some light scraping</a>, and have an LLM process all of this. I actually did the whole process with little coding by using a self-deployed instance of <a target="_blank" href="https://n8n.io/">n8n</a>. I also figured out <a target="_blank" href="https://horosin.com/how-to-create-a-sendfox-newsletter-signup-form-in-nextjs">getting newsletter sign ups</a>, <a target="_blank" href="https://horosin.com/how-to-quickly-get-feedback-and-data-for-your-app-using-pre-filled-google-forms">gathering feedback</a> and <a target="_blank" href="https://horosin.com/programatically-get-photos-matching-content-in-your-apps-with-python-and-unsplash-api">programatically getting photos for content</a> - as described in the linked articles.</p>
<p><a target="_blank" href="https://toolou.com/">The second app</a> is just a simple collection of browser-based tools. I often need to format some code or perform other small data transformations on the go. I don’t feel comfortable pasting any data into available offerings, so I decided to create some of these tools for my own use and release them to the public. It’s more of a training project; I’ve used AI heavily to code it. It didn’t take that long, and I borrowed many solutions from Airport Registry when building it. The most useful and novel tool is the <a target="_blank" href="https://toolou.com/tools/linkedin-post-date">one that allows you to find the exact date of a LinkedIn post</a>, since LinkedIn hides it. Especially useful when you’re working in social media.</p>
<p>In general, working on these two apps reminded me of how many problems you need to solve when building products. Deployment, UI design, backend, database setup, backups, security, marketing, content creation, and so on. And these two didn’t even need user management, payments, or support! I noticed that the second time was much easier because I had a lot already figured out.</p>
<p>My takeaway, quite obvious really, is that once you solve each of these problems, in your next project you can focus on what matters—creating something valuable for your users. So I plan to keep building and, while doing that, create a set of solutions that I can reuse in other projects. I feel that having this kind of know-how is key to building small products on my own and is also a huge advantage when building with others within companies.</p>
<p>How did the projects perform in December? Nothing extraordinary—and that's perfectly fine. The Airport Registry site received 173 unique visitors, while the tools collection had 44. I kept marketing minimal, only launching Airport Registry on Product Hunt. I’ve been thriving in my day job, spent a lot of time with family, got sick, quickly prototyped two new apps (using AI: v0 and Copilot with Claude are great), fine-tuned some small language models, recorded a podcast, and more. I felt a bit bad about not shipping more after two fun releases, but I also know that I needed this time to take a step back and come back with new ideas.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <strong><a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></li>
<li><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I tried to replace myself with AI and automate my code reviews]]></title><description><![CDATA[I’ve always struggled to find a use case where a language model could completely replace me, even for singular tasks. It’s an aid, a pair programmer, a proofreader, a code generator. But it’s never reliable or smart enough for me to trust it to act o...]]></description><link>https://horosin.com/i-tried-to-replace-myself-with-ai-and-automate-my-code-reviews</link><guid isPermaLink="true">https://horosin.com/i-tried-to-replace-myself-with-ai-and-automate-my-code-reviews</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[n8n]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Tue, 07 Jan 2025 08:47:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742294675931/133105a3-5128-40ba-8d13-eb1376723319.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve always struggled to find a use case where a language model could completely replace me, even for singular tasks. It’s an aid, a pair programmer, a proofreader, a code generator. But it’s never reliable or smart enough for me to trust it to act or write in my name. And honestly, I wouldn’t want that anyway. Even mundane tasks require context that’s hard to convey to a model.</p>
<p>But that’s no reason not to experiment!</p>
<h2 id="heading-the-role-of-code-reviews">The Role of Code Reviews</h2>
<p>In my engineering work, I spend a significant amount of time reviewing others' code. In fact, in most companies, developers spend 2-5 hours per week on code reviews. Some find it boring or see it as an extra burden. However, there are three key goals:</p>
<ol>
<li>Ensure quality and catch mistakes</li>
<li>Stay up to date with changes</li>
<li>Help others improve their programming skills</li>
</ol>
<p>All of these are critical, but there’s also an indirect consequence of code reviews in modern development practices. Usually, a code review is a step taken before deployment. Performing a swift code review becomes the single most important thing you can do to help the team ship the next feature. I prioritize reviews ahead of my own work.</p>
<p>At most of the places I’ve worked, there’s been discussion about codifying standards for great code reviews to help everyone improve. My current workplace was no exception.</p>
<h2 id="heading-experimenting-with-ai">Experimenting with AI</h2>
<p>I drafted a set of guidelines for effective code reviews. As I looked at them through the lens of my AI engineering experience, I realized they could make a perfect prompt for a language model. This inspired me to try making AI assist with reviews. My goal was to spot more issues and perhaps even provide automated reviews for the team, even if they weren’t perfect.</p>
<h3 id="heading-the-plan">The Plan</h3>
<p>I devised a plan in minutes. There’s a tool I often use for low-code automations, n8n. I discovered that I could easily connect to GitHub via its API to read code and post review comments.</p>
<h3 id="heading-prototype-phase">Prototype Phase</h3>
<p>I built a prototype using GPT-4o and tested it with my personal projects. The outputs were, frankly, terrible. The comments were overly polite, and their usefulness got buried under layers of fluff. I shortened the prompt, made it more specific, and explicitly asked the model to be critical in all comments except the summary. I also provided examples of effective feedback.</p>
<p>Version 2 was far more useful but still included some unnecessary and impractical remarks. I encountered several limitations:</p>
<ol>
<li><strong>Tool constraints:</strong> n8n, the tool I used to prototype the workflow, didn’t support the latest OpenAI API functions. This resulted in subpar outputs compared to what was possible.</li>
<li><strong>Contextual limitations:</strong> The diff provided limited context about the project and its tasks. Providing a great review requires understanding the business context.</li>
</ol>
<p>That said, the AI managed to spot mistakes in the code that I had missed. Some of its outputs were genuinely helpful. The process involved reviewing all generated comments, removing 75% of them, adding my own insights, and then submitting the review. Despite the overhead, this workflow was actually useful enough.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>I haven’t gone further yet. To improve the system, I’d need to implement new API functionalities (like JSON outputs), pull extra code and project context from the codebase, and possibly integrate task management software to fetch relevant business information. The approach shows  promise, and I’m aware of several companies exploring similar solutions. I want to refine it further and potentially turn it into a small product.</p>
<p>I’m sharing this post as a checkpoint and a possible discussion starter. If you think this is interesting and would like to test it, let me know. I’ve been considering building a version tailored for companies to automate code reviews, as well as one for individuals to help them become better reviewers and save time.</p>
<p>What do you think is the best route?</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <strong><a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></li>
<li><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to quickly get feedback and data for your app using pre-filled Google Forms]]></title><description><![CDATA[Another small nugget of practical knowledge from me.
When you’re building a project as a solo developer, you need a quick way to gather feedback or collect data without reinventing the wheel. While you can always write more code, you often don’t have...]]></description><link>https://horosin.com/how-to-quickly-get-feedback-and-data-for-your-app-using-pre-filled-google-forms</link><guid isPermaLink="true">https://horosin.com/how-to-quickly-get-feedback-and-data-for-your-app-using-pre-filled-google-forms</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Startups]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[Low Code]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Fri, 11 Oct 2024 07:23:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742294813631/dabfd30e-b485-4398-b484-8b6828e8b866.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Another small nugget of practical knowledge from me.</p>
<p>When you’re building a project as a solo developer, you need a quick way to gather feedback or collect data without reinventing the wheel. While you can always write more code, you often don’t have to. Sometimes you can even ship something with just a frontend! Pre-filled Google Forms can offer a solution in these situations. Whether you need to create a basic contact form, allow order customization, gather feature requests, bug reports, or collect structured data from users, this method is simple, effective, and easy to implement.</p>
<p>The strength of this approach lies in passing the data from your app into the form URL, auto-filling certain fields to save users time and encourage them to complete the form. Also, you may pass identifiers allowing you to integrate this data quickly without manual parsing.</p>
<h2 id="heading-real-world-example-gathering-airport-data">Real-world example: gathering airport data</h2>
<p>Let’s say you’re running a project like mine - <a target="_blank" href="https://airportregistry.com/">airportregistry.com</a>, where users can look up and manage information about airports worldwide. You want to let users quickly request corrections or updates to airport data directly from the site.</p>
<p>Instead of asking users to manually enter details about the airport they’re viewing, you can pre-fill these fields in a Google Form with data from your site, making it much easier for them to submit feedback. Here's an example:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> requestInfoLink =
  <span class="hljs-string">`https://docs.google.com/forms/`</span> +
  <span class="hljs-string">`d/e/1FAIpQLSfAVYhpjNOZR6_gMPJPtlFYPRqGaem_rFb8nQ4CV27VTgmGzA/viewform?usp=pp_url`</span> +
  <span class="hljs-string">`&amp;entry.1513837494=<span class="hljs-subst">${airport.airport}</span>`</span> +
  <span class="hljs-string">`&amp;entry.20379740=<span class="hljs-subst">${airport.iataCode}</span>`</span> +
  <span class="hljs-string">`&amp;entry.475896124=<span class="hljs-subst">${airport.location}</span>`</span> +
  <span class="hljs-string">`&amp;entry.166674403=<span class="hljs-subst">${airport.country}</span>`</span> +
  <span class="hljs-string">`&amp;entry.60635846=<span class="hljs-subst">${airport.continent}</span>`</span>;
</code></pre>
<p>With this setup, when a user clicks the feedback link, they’ll be taken to a Google Form with details like the airport name, IATA code, location, country, and continent already filled in. This removes friction and increases the chances that users will provide meaningful feedback.</p>
<h2 id="heading-how-to-create-a-pre-filled-google-form">How to create a pre-filled Google Form</h2>
<ol>
<li>Create a new form: Start by creating a new Google Form with the fields you need. For example, you might include fields like “Airport Name,” “IATA Code,” “Location,” etc.</li>
<li>Set up pre-fillable fields:<ul>
<li>Click the three-dot menu (ellipsis) in the upper right corner and choose "Get pre-filled link."</li>
<li>Enter some placeholder values in the fields you want to pre-fill. Use easily recognizable placeholders like “AirportNamePlaceholder,” “IATACodePlaceholder,” etc. This makes it easier to identify and replace them in the generated link.</li>
<li>Click "Get link" and copy the URL provided by Google.</li>
</ul>
</li>
<li>Customize the link:<ul>
<li>The URL you copied will contain placeholders that look something like <code>entry.123456789=AirportNamePlaceholder</code>.</li>
<li>Replace these placeholder values with variables or data from your app, as shown in the example above.</li>
</ul>
</li>
</ol>
<p>Tada! You saved yourself some time you would waste writing frontend and backend code.</p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>Pre-filled Google Forms offer a straightforward way to integrate feedback and data entry mechanisms into your app without writing code.</p>
<p>Use the time saved to really focus on providing value with other features in your app.</p>
<p>Do you think this is bad? How many users does your app have? Hm?</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>. Also check out the website where I applied this technique - <a target="_blank" href="http://airportregistry.com">airportregistry.com</a>!</p>
<p>You can also find me here:</p>
<ul>
<li>x: <a target="_blank" href="https://twitter.com/horosin_">horosin_</a></li>
<li><a target="_blank" href="https://www.linkedin.com/in/horosin/">linkedin</a></li>
</ul>
<h2 id="heading-other-small-tips-for-your-saas">Other small tips for your SaaS</h2>
<ul>
<li><a target="_blank" href="https://horosin.com/programatically-get-photos-matching-content-in-your-apps-with-python-and-unsplash-api">Get stock photos for your content automatically</a></li>
<li><a target="_blank" href="https://horosin.com/how-to-create-a-sendfox-newsletter-signup-form-in-nextjs">Newsletter signup form in Next.js for free or lifetime deal</a></li>
<li><a target="_blank" href="https://horosin.com/scraping-data-off-wikipedia-three-ways-no-code-and-code-python-pandas-sheets">Scraping Wikipedia for high quality data</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Programatically get photos matching content in your apps with Python and Unsplash API]]></title><description><![CDATA[Introduction
I often want to add visually interesting elements to my projects and struggle with content. Let’s say you have a subpage that shows all the posts/any other information related to a city. It would be great to have a photo of said city in ...]]></description><link>https://horosin.com/programatically-get-photos-matching-content-in-your-apps-with-python-and-unsplash-api</link><guid isPermaLink="true">https://horosin.com/programatically-get-photos-matching-content-in-your-apps-with-python-and-unsplash-api</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Python]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Wed, 24 Jul 2024 09:28:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742295032646/80cd9b11-39bf-4e37-aa8c-6c9da6c52254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>I often want to add visually interesting elements to my projects and struggle with content. Let’s say you have a subpage that shows all the posts/any other information related to a city. It would be great to have a photo of said city in the header, wouldn’t it?</p>
<p>You may think that the way to go is to generate something with AI. It may work, but there are many issues with this approach.</p>
<p>There are great free stock photo sites like Pexels or Unsplash. But how to find the right picture?</p>
<p>I stumbled upon this problem while working on one of my side projects. It turns out Unsplash has a great API. In Python, we need to call it manually but there are client packages for JS, Ruby, PHP, mobile, etc.</p>
<h2 id="heading-unsplash-api">Unsplash API</h2>
<p>The API basically allows you to find photos matching your search query. A few caveats:</p>
<ol>
<li><p>Make sure you use direct links from Unsplash. Downloading and re-hosting images is not allowed.</p>
</li>
<li><p>You must attribute Unsplash and the author.</p>
</li>
<li><p>The API is rate limited to 50 requests per hour for unverified apps, 5k after approval and more if you need it.</p>
</li>
</ol>
<p>More on usage guidelines: <a target="_blank" href="https://help.unsplash.com/en/articles/2511245-unsplash-api-guidelines">Unsplash</a>.</p>
<p>Getting started (<a target="_blank" href="https://unsplash.com/documentation#creating-a-developer-account">in-depth guide</a>):</p>
<ol>
<li><p>Sign up for an account: <a target="_blank" href="https://unsplash.com/oauth/applications">https://unsplash.com/oauth/applications</a></p>
</li>
<li><p>Go to your apps: <a target="_blank" href="https://unsplash.com/oauth/applications">https://unsplash.com/oauth/applications</a></p>
</li>
<li><p>Create a “New application”</p>
</li>
<li><p>Go to your app and save Access key, we’ll need it in the following steps</p>
</li>
</ol>
<h2 id="heading-fetching-an-image">Fetching an image</h2>
<p>Have a look at this sample function, you can adapt it to your needs.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> os

<span class="hljs-comment"># Load Unsplash API key from environment variables</span>
UNSPLASH_ACCESS_KEY = os.getenv(<span class="hljs-string">'UNSPLASH_ACCESS_KEY'</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_image_info</span>(<span class="hljs-params">query</span>):</span>
    url = <span class="hljs-string">"&lt;https://api.unsplash.com/search/photos&gt;"</span>
    params = {
        <span class="hljs-string">"query"</span>: query,
        <span class="hljs-string">"client_id"</span>: UNSPLASH_ACCESS_KEY,
        <span class="hljs-string">"per_page"</span>: <span class="hljs-number">1</span>,
        <span class="hljs-string">"orientation"</span>: <span class="hljs-string">"landscape"</span>,
    }
    response = requests.get(url, params=params)

    <span class="hljs-keyword">if</span> response.status_code == <span class="hljs-number">200</span>:
        data = response.json()
        <span class="hljs-keyword">if</span> data[<span class="hljs-string">'results'</span>]:
            <span class="hljs-keyword">return</span> data[<span class="hljs-string">'results'</span>][<span class="hljs-number">0</span>]
    <span class="hljs-keyword">elif</span> response.status_code == <span class="hljs-number">403</span>:
            <span class="hljs-keyword">raise</span> ConnectionError(<span class="hljs-string">"Rate limited or not authorized"</span>)

    <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>
</code></pre>
<p>When you call this function for a “New York” query, you should receive an object like that:</p>
<pre><code class="lang-json">{
   <span class="hljs-attr">"id"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"slug"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"created_at"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"width"</span>: <span class="hljs-string">"integer"</span>,
   <span class="hljs-attr">"height"</span>: <span class="hljs-string">"integer"</span>,
   <span class="hljs-attr">"color"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"blur_hash"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"alt_description"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"breadcrumbs"</span>: <span class="hljs-string">"array"</span>,
   <span class="hljs-attr">"urls"</span>: <span class="hljs-string">"object"</span>,
   <span class="hljs-attr">"links"</span>: <span class="hljs-string">"object"</span>,
   <span class="hljs-attr">"likes"</span>: <span class="hljs-string">"integer"</span>,
   <span class="hljs-attr">"user"</span>: <span class="hljs-string">"object"</span>,
   <span class="hljs-attr">"tags"</span>: <span class="hljs-string">"array"</span>,
   <span class="hljs-attr">"..."</span>: <span class="hljs-string">"..."</span>
}
</code></pre>
<p>You should pay attention to three of them really.</p>
<p>First, <code>urls</code> - this is what you will use in your app.</p>
<pre><code class="lang-json"> <span class="hljs-string">"urls"</span>: {
    <span class="hljs-attr">"raw"</span>: <span class="hljs-string">"&lt;https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixid=M3w2MjA1MzR8MHwxfHNlYXJjaHwxfHxOZXclMjBZb3JrfGVufDB8MHx8fDE3MjA5MDAxMTd8MA&amp;ixlib=rb-4.0.3&gt;"</span>,
    <span class="hljs-attr">"full"</span>: <span class="hljs-string">"&lt;https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?crop=entropy&amp;cs=srgb&amp;fm=jpg&amp;ixid=M3w2MjA1MzR8MHwxfHNlYXJjaHwxfHxOZXclMjBZb3JrfGVufDB8MHx8fDE3MjA5MDAxMTd8MA&amp;ixlib=rb-4.0.3&amp;q=85&gt;"</span>,
    <span class="hljs-attr">"regular"</span>: <span class="hljs-string">"&lt;https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3w2MjA1MzR8MHwxfHNlYXJjaHwxfHxOZXclMjBZb3JrfGVufDB8MHx8fDE3MjA5MDAxMTd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080&gt;"</span>,
    <span class="hljs-attr">"small"</span>: <span class="hljs-string">"&lt;https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3w2MjA1MzR8MHwxfHNlYXJjaHwxfHxOZXclMjBZb3JrfGVufDB8MHx8fDE3MjA5MDAxMTd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=400&gt;"</span>,
    <span class="hljs-attr">"thumb"</span>: <span class="hljs-string">"&lt;https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3w2MjA1MzR8MHwxfHNlYXJjaHwxfHxOZXclMjBZb3JrfGVufDB8MHx8fDE3MjA5MDAxMTd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=200&gt;"</span>,
    <span class="hljs-attr">"small_s3"</span>: <span class="hljs-string">"&lt;https://s3.us-west-2.amazonaws.com/images.unsplash.com/small/photo-1496442226666-8d4d0e62e6e9&gt;"</span>,
 },
</code></pre>
<p>Second, <code>user</code> - you will use this info for proper attribution, namely:</p>
<ul>
<li><p><code>user.name</code> : Author name</p>
</li>
<li><p><code>user.links.html</code> : Link to their profile, you must use. Make sure to append <code>utm_source=your_app_name&amp;utm_medium=referral</code> to it, per guidelines.</p>
</li>
</ul>
<p>Third, <code>alt_description</code> , make sure to add it to your final IMG tags for accessibility and SEO.</p>
<p>This piece of code is really everything you need to get going. With one caveat.</p>
<h2 id="heading-watching-for-rate-limit">Watching for rate limit</h2>
<p>When you’re in initial demo mode, you will only have 50 requests per hour. When trying to fetch more images, hitting rate limit is a given. What to do?</p>
<p>Simply wait for a bit if the function above throws ConnectionError. I wait for around 70 seconds.</p>
<p>Also mind that if no results are found, you will receive None. In this case I am using a simplified query (country instead of the city).</p>
<p>Once the website is online, you can request higher quota in the dashboard.</p>
<h2 id="heading-summary">Summary</h2>
<p>We've covered how to use Python and the Unsplash API to automatically fetch photos for your apps and websites.</p>
<p>I hope you’ll build something cool with it. Send your projects my way in the comments or on Twitter.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>. I will soon be releasing a crazy interesting project that uses this solution!</p>
<p>You can also find me here:</p>
<ul>
<li><p>x: <a target="_blank" href="https://twitter.com/horosin_">horosin_</a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/">linkedin</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to create a SendFox newsletter signup form in Next.js]]></title><description><![CDATA[Introduction
SendFox is a popular newsletter service built by AppSumo. The company is famous for its store with lifetime deals for software. The product has limitations and is not a perfect solution but if you need a solid tool with predictable cost,...]]></description><link>https://horosin.com/how-to-create-a-sendfox-newsletter-signup-form-in-nextjs</link><guid isPermaLink="true">https://horosin.com/how-to-create-a-sendfox-newsletter-signup-form-in-nextjs</guid><category><![CDATA[Next.js]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[newsletter]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Tue, 09 Jul 2024 13:58:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742295230718/845c93aa-dbd0-4808-9f10-f5c531505f28.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>SendFox is a popular newsletter service built by AppSumo. The company is famous for its store with lifetime deals for software. The product has limitations and is not a perfect solution but if you need a solid tool with predictable cost, it may be just the fit for you. There's a generous free tier as well.</p>
<p>There aren't many examples online for how to create a sign up form outside of embedding one. They don't look the best and don't play nice with JS frameworks.</p>
<p>Let's create a form component and an API route in Next.js that will give us full control.</p>
<p>I am using <code>shadcn/ui</code> for ui components and <code>lucide-react</code> for icons. You can easily swap these components for something else.</p>
<h2 id="heading-set-up">Set up</h2>
<p>Skip if you're adding this to an existing app.</p>
<p>If you want to follow this tutorial from scratch, perform the following steps.</p>
<ol>
<li><p>Set up Next.js app with shadcn/ui - <a target="_blank" href="https://ui.shadcn.com/docs/installation/next">link</a></p>
</li>
<li><p>Add components: <code>npx shadcn-ui@latest add button input</code></p>
</li>
<li><p>Install icons pack: <code>npm i lucide-react</code></p>
</li>
</ol>
<p>You're good to go.</p>
<h2 id="heading-senffox-api">SenfFox API</h2>
<p>SendFox has a very basic API documentation, which you can find here: <a target="_blank" href="https://help.sendfox.com/article/278-endpoints">https://help.sendfox.com/article/278-endpoints</a>.</p>
<p>You will need to get an API key from your SendFox account. Go to the <a target="_blank" href="https://sendfox.com/account/oauth">settings, API section</a> and click "Create new token".</p>
<p>Copy and store safely.</p>
<h2 id="heading-api-route">API route</h2>
<p>Let's start with an API route that will handle our sign up logic.</p>
<p>You can add more verification to this code to pre-filter spammy sign-ups.</p>
<p>You may want to also save emails somewhere other than SendFox for redundancy.</p>
<p>I've created a file <code>app/api/newsletter/route.ts</code> with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">request: Request</span>) </span>{
  <span class="hljs-keyword">const</span> { email } = <span class="hljs-keyword">await</span> request.json();

  <span class="hljs-keyword">if</span> (!email) {
    <span class="hljs-keyword">return</span> Response.json({ error: <span class="hljs-string">"Email is required"</span> }, { status: <span class="hljs-number">400</span> });
  }

  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"&lt;https://api.sendfox.com/contacts&gt;"</span>, {
    method: <span class="hljs-string">"POST"</span>,
    headers: {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${process.env.SENDFOX_TOKEN}</span>`</span>,
    },
    body: <span class="hljs-built_in">JSON</span>.stringify({
      email,
    }),
  });

  <span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">return</span> Response.json({ error: <span class="hljs-string">"Failed to subscribe"</span> }, { status: <span class="hljs-number">500</span> });
  }

  <span class="hljs-keyword">return</span> Response.json({ message: <span class="hljs-string">"Subscribed successfully"</span> });
}
</code></pre>
<p>You can test it in Postman or via curl or just jump in to create a form.</p>
<h2 id="heading-form-component">Form component</h2>
<p>Create a file <code>app/components/send-fox-form.tsx</code> with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/ui/button"</span>;
<span class="hljs-keyword">import</span> { Input } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/ui/input"</span>;
<span class="hljs-keyword">import</span> { Loader2 } <span class="hljs-keyword">from</span> <span class="hljs-string">"lucide-react"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">type</span> FormStatus = <span class="hljs-string">"idle"</span> | <span class="hljs-string">"loading"</span> | <span class="hljs-string">"success"</span> | <span class="hljs-string">"error"</span>;

<span class="hljs-keyword">const</span> ButtonLoading = <span class="hljs-function">() =&gt;</span> (
  &lt;Button disabled&gt;
    &lt;Loader2 className=<span class="hljs-string">"mr-2 h-4 w-4 animate-spin"</span> /&gt;
    Please wait
  &lt;/Button&gt;
);

<span class="hljs-keyword">const</span> SendFoxForm = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [email, setEmail] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [status, setStatus] = useState&lt;FormStatus&gt;(<span class="hljs-string">"idle"</span>);
  <span class="hljs-keyword">const</span> [errorMessage, setErrorMessage] = useState(<span class="hljs-string">""</span>);

  <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (event: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    event.preventDefault();
    setStatus(<span class="hljs-string">"loading"</span>);
    setErrorMessage(<span class="hljs-string">""</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/newsletter"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        headers: {
          <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        },
        body: <span class="hljs-built_in">JSON</span>.stringify({ email: email.trim().toLowerCase() }),
      });

      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">if</span> (response.ok) {
        setStatus(<span class="hljs-string">"success"</span>);
      } <span class="hljs-keyword">else</span> {
        setStatus(<span class="hljs-string">"error"</span>);
        setErrorMessage(data.message || <span class="hljs-string">"Failed to subscribe"</span>);
      }
    } <span class="hljs-keyword">catch</span> (error) {
      setStatus(<span class="hljs-string">"error"</span>);
      setErrorMessage(<span class="hljs-string">"An error occurred while trying to subscribe."</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"w-full"</span>&gt;
      &lt;form
        onSubmit={handleSubmit}
        className=<span class="hljs-string">"flex w-full max-w-md items-center space-x-2 mx-auto"</span>
      &gt;
        &lt;Input
          className=<span class="hljs-string">"w-full"</span>
          <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span>
          placeholder=<span class="hljs-string">"Email"</span>
          name=<span class="hljs-string">"email"</span>
          value={email}
          onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setEmail(e.target.value)}
          required
          disabled={status === <span class="hljs-string">"loading"</span> || status === <span class="hljs-string">"success"</span>}
        /&gt;
        {status === <span class="hljs-string">"loading"</span> ? (
          &lt;ButtonLoading /&gt;
        ) : (
          &lt;Button <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> disabled={status === <span class="hljs-string">"success"</span>}&gt;
            {status === <span class="hljs-string">"success"</span> ? <span class="hljs-string">"Subscribed!"</span> : <span class="hljs-string">"Subscribe"</span>}
          &lt;/Button&gt;
        )}
      &lt;/form&gt;
      &lt;div className=<span class="hljs-string">"pt-2 min-h-[1em]"</span>&gt;
        {(status === <span class="hljs-string">"idle"</span> || status === <span class="hljs-string">"loading"</span>) &amp;&amp; &lt;p&gt;&amp;nbsp;&lt;/p&gt;}
        {status === <span class="hljs-string">"error"</span> &amp;&amp; (
          &lt;p className=<span class="hljs-string">"text-sm text-red-500 text-center"</span>&gt;{errorMessage}&lt;/p&gt;
        )}
        {status === <span class="hljs-string">"success"</span> &amp;&amp; (
          &lt;p className=<span class="hljs-string">"text-sm text-muted text-center"</span>&gt;
            Subscription successful! Thank you <span class="hljs-keyword">for</span> joining.
          &lt;/p&gt;
        )}
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SendFoxForm;
</code></pre>
<p>The <code>SendFoxForm</code> component handles the subscription logic and user interaction. It utilizes <code>useState</code> to manage the form's status and user input. The form includes three states: <code>idle</code>, <code>loading</code>, and <code>success</code>, each guiding the user through the subscription process with appropriate feedback.</p>
<p>Business Logic Overview:</p>
<ol>
<li><p><strong>Form Submission Handling</strong>:</p>
<ul>
<li><p>When the form is submitted, it prevents the default form behavior and sets the status to <code>loading</code>.</p>
</li>
<li><p>The email input is trimmed and converted to lowercase before being sent to the server.</p>
</li>
</ul>
</li>
<li><p><strong>API Interaction</strong>:</p>
<ul>
<li><p>The form makes a POST request to the <code>/api/newsletter</code> route with the user's email.</p>
</li>
<li><p>If the response is successful (<code>response.ok</code>), the status changes to <code>success</code>.</p>
</li>
<li><p>If there's an error, the status changes to <code>error</code>, and an appropriate error message is displayed.</p>
</li>
</ul>
</li>
<li><p><strong>User Feedback</strong>:</p>
<ul>
<li><p>While the form is submitting, a loading button is displayed to inform the user to wait.</p>
</li>
<li><p>If the subscription is successful, a "Subscribed!" message is shown, and further input is disabled.</p>
</li>
<li><p>If there is an error, an error message is displayed, guiding the user to rectify the issue.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a working SendFox newsletter sign up form in your Next.js app.</p>
<p>While SendFox is in no way perfect, it may be the right choice for your first newsletter or a side project.</p>
<p>Personally, I used it for one of my upcoming projects and it's been a good experience so far. It lacks features related to managing multiple lists of contacts so may not be the best choice if you're running a few projects with separate domains.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>. I will soon be releasing a crazy interesting project that uses this solution!</p>
<p>You can also find me here:</p>
<ul>
<li><p>x: <a target="_blank" href="https://twitter.com/horosin_">horosin_</a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/">linkedin</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Scraping data off Wikipedia: three ways (no code and code)]]></title><description><![CDATA[I needed some data for my side project. Google helped me find only outdated CSVs, expensive closed sources and APIs hidden behind “contact sales” button.
Wikipedia was ranking high in my searches but I quickly found that the tables are as unstructure...]]></description><link>https://horosin.com/scraping-data-off-wikipedia-three-ways-no-code-and-code-python-pandas-sheets</link><guid isPermaLink="true">https://horosin.com/scraping-data-off-wikipedia-three-ways-no-code-and-code-python-pandas-sheets</guid><category><![CDATA[Python]]></category><category><![CDATA[google sheets]]></category><category><![CDATA[data analysis]]></category><category><![CDATA[pandas]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Thu, 27 Jun 2024 11:17:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719486792496/f4bdbe3d-062b-40fb-8d49-bf259ad652ac.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I needed some data for my side project. Google helped me find only outdated CSVs, expensive closed sources and APIs hidden behind “contact sales” button.</p>
<p>Wikipedia was ranking high in my searches but I quickly found that the tables are as unstructured as the ones you typically create in Word or Google Docs. There was no consistency in columns and naming conventions. This at first discouraged me but after failing to find another datasource, I gave it a go. And I ended up using it for my project.</p>
<p>Let me show you how to quickly load Wikipedia data for your data analysis. I was astonished how easy it is.</p>
<p>Big thanks to all Wikipedia contributors doing the hard work.</p>
<h2 id="heading-method-1-loading-tables-in-google-sheets">Method 1: Loading tables in Google Sheets</h2>
<p>This one feels like magic.</p>
<p>Use this formula in one cell, and it will expand the entire table scraped from Wikipedia.</p>
<pre><code class="lang-plaintext">=importHTML(WIKIPEDIA_URL, "table", NUMBER_OF_TABLE_ON_THE_PAGE);
</code></pre>
<p>Let’s say you want to fetch a list of all EU countries with their population count, GDP, language and other essential data.</p>
<p>There’s a Wikipedia page containing all of this information and citing sources of the information(!): <a target="_blank" href="https://en.wikipedia.org/wiki/Member_state_of_the_European_Union">https://en.wikipedia.org/wiki/Member_state_of_the_European_Union</a>.</p>
<p>The table containing this info is the <em>second table on the page</em> (the first one is the little summary table). The formula will look like:</p>
<pre><code class="lang-plaintext">=importHTML("https://en.wikipedia.org/wiki/Member_state_of_the_European_Union", "table", 2);
</code></pre>
<p>After running it, you’ll get all of this data in your Google Sheets, ready to analyze or export.</p>
<p>Google Sheets can also read lists! Full docs <a target="_blank" href="https://support.google.com/docs/answer/3093339?hl=en">here</a>.</p>
<h2 id="heading-method-2-pandas-and-python">Method 2: Pandas and Python</h2>
<p>I was surprised to find out that the most popular library for data analysis for Python has a feature equivalent to Google Docs.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd

url = <span class="hljs-string">"https://en.wikipedia.org/wiki/Member_state_of_the_European_Union"</span>

dfs = pd.read_html(url)

df = dfs[<span class="hljs-number">1</span>]
</code></pre>
<p><code>read_html</code> function from pandas takes site URL as an input and fetches all tables to a list of dataframes. Again, the second one is what we are looking for. And it’s ready for further analysis!</p>
<p>Since Google Docs was not something I intended to stick with, I used this method in addition to method 3 to gather data from several pages.</p>
<p>Full documentation <a target="_blank" href="https://pandas.pydata.org/docs/reference/api/pandas.read_html.html">here</a>.</p>
<h2 id="heading-method-3-python-and-beautiful-soup">Method 3: Python and Beautiful Soup</h2>
<p>Well, Beautiful Soup is a go to library for parsing HTML in Python ecosystem. I looked for easy solutions but I needed to fall back on it in the end. The reason was I needed to get not only data from tables but also from headings that preceded them.</p>
<p>Instead of writing the code from scratch, I recommend using ChatGPT and refining its output. I basically pasted a url, said what I needed to scrape and got the script without giving any guidance.</p>
<p>If you wanted to do something similar to me - getting all tables from a page and data from multiple levels of headings preceding them - you would do something like this.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> bs4 <span class="hljs-keyword">import</span> BeautifulSoup

response = requests.get(url)
soup = BeautifulSoup(response.content, <span class="hljs-string">'html.parser'</span>)
tables = soup.find_all(<span class="hljs-string">'table'</span>, {<span class="hljs-string">'class'</span>: <span class="hljs-string">'wikitable'</span>})

all_airports = []

<span class="hljs-keyword">for</span> table <span class="hljs-keyword">in</span> tables:
  continent = table.find_previous(<span class="hljs-string">'h2'</span>).find(<span class="hljs-string">'span'</span>, class_=<span class="hljs-string">'mw-headline'</span>).text
  region = table.find_previous(<span class="hljs-string">'h3'</span>).find(<span class="hljs-string">'span'</span>, class_=<span class="hljs-string">'mw-headline'</span>).text
  country = table.find_previous(<span class="hljs-string">'h4'</span>).find(<span class="hljs-string">'span'</span>, class_=<span class="hljs-string">'mw-headline'</span>).text

  rows = table.find_all(<span class="hljs-string">'tr'</span>)

  <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> rows[<span class="hljs-number">1</span>:]:
      cols = row.find_all(<span class="hljs-string">'td'</span>)

      <span class="hljs-comment"># here push the data wherever you need</span>
</code></pre>
<p>It looks pretty neat, doesn’t it? I was worried that scraping “by hand” would be a pain but actually Wikipedia usually has a pretty clear, predictable HTML structure. It varies page to page but adheres to the same patterns.</p>
<p>This method in addition to using pandas helped me get all the data I needed.</p>
<h2 id="heading-summary">Summary</h2>
<p>As you can see, getting data for your analyses or side projects can be nice and easy.</p>
<p>There is a catch though - there are no data consistency mechanisms in tables inside Wikipedia pages. This means that after using any of the above methods, you’ll likely need to do some cleaning and check for mismatches in naming conventions. Some regular expressions will come in handy as well.</p>
<p>At times I am cynical about technological progress but discovering that information access can be so straightforward makes me feel that I can really stand on the shoulders of giants when building on my own.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a><strong>.</strong> I will soon be releasing a crazy interesting project I needed data from Wikipedia for!</p>
<p>You can also find me here:</p>
<ul>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to debug and solve a big production problem with SaaS]]></title><description><![CDATA[Software development is mostly not actually writing software. Sometimes it’s debugging a critical issue that cannot wait and is beyond abilities of first-line support. Keep reading to learn what tools you can and should employ when dealing with a lon...]]></description><link>https://horosin.com/how-to-debug-and-solve-a-big-production-problem-with-saas</link><guid isPermaLink="true">https://horosin.com/how-to-debug-and-solve-a-big-production-problem-with-saas</guid><category><![CDATA[software development]]></category><category><![CDATA[incident response]]></category><category><![CDATA[Career]]></category><category><![CDATA[agile]]></category><category><![CDATA[SaaS]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Tue, 05 Mar 2024 14:00:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742295352857/17780c05-56c8-4cfc-8d9e-4223f189ef37.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Software development is mostly not actually writing software. Sometimes it’s debugging a critical issue that cannot wait and is beyond abilities of first-line support. Keep reading to learn what tools you can and should employ when dealing with a long-running hard-to-fix problems.</p>
<p>You may be guarded by countless level of corporate hierarchy but eventually you will either be asked to debug a production issue or you will be in a job that will require it.</p>
<p>This article is aimed primarily at software engineers but also QAs, managers, leads, solution engineers.</p>
<h1 id="heading-how-does-an-issue-reach-you">How does an issue reach you?</h1>
<p>You get an invite to a call, you’re tagged in jira ticket, on slack, or DM’ed. Ok, cool.</p>
<p>Someone is asking you to drop a feature you’ve been working on, switch context and help. Annoying, I know. Give yourself 90 seconds for grief and move on.</p>
<p>If someone is asking you for urgent help with production issue, they mean it.</p>
<p>Before you start, make sure your direct superior/manager/leas knows you are switching priorities.</p>
<p>And if you start investigating, make sure everyone knows it, so other people are not wasting time doing the same.</p>
<p>As you’ll see, comms are the key.</p>
<h1 id="heading-communication">Communication</h1>
<p>Before we get to any technical tips, let’s start with communication. I’ll mention some tools, some should be a part of the system already in place, automatic procedures. If they’re not, you should introduce them.</p>
<h2 id="heading-incident-slack-channel">Incident slack channel</h2>
<p>If you’re reading this in early XXI century, you have a company chat app.</p>
<p>Make sure there is a channel where all the people that can help with the problem or need information about it have a place to talk. Invite people and ask them to add anyone interested.</p>
<p>Spam this channel with progress updates.</p>
<p>Example name: <code>inc-2023-01-04-sign-up-down</code> . It can also include a ticket number instead of a date.</p>
<h2 id="heading-cadency">Cadency</h2>
<p>Depending on the criticality of the issue, post summaries of what’s going on.</p>
<ul>
<li>If it’s an urgent issue that is classified as P1 (priority 1, critical system down, significant financial impact), you’re likely to post summaries each hour or two.</li>
<li>If it’s a long running issue with lower priority, begin the day with the plan for what’s next and close the day with progress summary.</li>
</ul>
<h2 id="heading-message-template">Message template</h2>
<p>Here’s what I use:</p>
<blockquote>
<p><strong>Summary of [name/ticket] incident investigation as of 4PM, Jan 18th</strong></p>
<p>Resolved: yes/no/partially</p>
<p><strong>∑ Brief summary</strong></p>
<ul>
<li>we know that the cause of the issue is: …</li>
<li>fix by … didn’t work, we’re trying …., estimated test at …</li>
<li>replication is hard, streamlining it</li>
</ul>
<p>⏭️ <strong>Next steps</strong></p>
<ul>
<li>[…]</li>
</ul>
<p>🧠 <strong>Other notes</strong></p>
<ul>
<li>[ideas and resources]</li>
</ul>
</blockquote>
<p>These updates may not get reactions but many people will read them and quietly thank you for your thoroughness. If you are a recipient of those, like them, react with 👀 emoji, or whatever. Feedback is always good.</p>
<p>Forward daily updates on the main channel for issue discussions or on the team channel, so people less involved but still interested or able to help can see them.</p>
<h2 id="heading-escalation">Escalation</h2>
<p>Whenever you are stuck - flag it and ask for help. You have an important task, it’s not worth being quiet. If your direct manager isn’t listening - try their manager.</p>
<p>When asking people for help, state how urgent is the question and when you need it done.</p>
<p>Don’t sit alone on an unresolved issue. You work with other specialists, present your idea to someone, get feedback.</p>
<h1 id="heading-getting-essential-information">Getting essential information</h1>
<p>There are few types of information you are going to need. Apart from what comes with your programming experience.</p>
<ol>
<li>How to reproduce the issue? Steps taken, environment details.</li>
<li>Input and output data - input that is causing the issue, the erroneous output, the right output.</li>
<li>Logs with real-world examples of the issue in the wild.</li>
<li>System knowledge - think knowing the part of the product in question, documentation.</li>
<li>Initial estimate of impact on customers.</li>
</ol>
<p>If you lack any of these at the start, work hard to get them first. I’ve been asked to work on a bug that no one could reproduce a few times. Customers that encountered it, didn’t have time to jump on a call. Imagine how hard it was to work on such an elusive problem.</p>
<p>You are OK to push back in these situations. Make sure to be collaborative with your customer-facing colleagues that will help gather this information. If you can, tell them exactly what you need. You can use the list above.</p>
<p>Regarding the point 5, estimating impact, it will need to be done properly either by you or someone else in the end. It will be needed for proper customer communication.</p>
<h1 id="heading-working-on-the-issue">Working on the issue</h1>
<p>Finding and fixing a bug is a little bit like doing science. You are likely going to follow a process like this:</p>
<ol>
<li>Construct a hypothesis</li>
<li>Test it by doing experiments</li>
<li>Analyze your data and draw a conclusion</li>
<li>Come up with a fix</li>
<li>Test the fix</li>
<li>If it doesn’t work → repeat</li>
</ol>
<p>Make sure to write down your current hypothesis and log any additional ideas. Keep track of what you checked and what you didn’t. If the work will end up taking long time or you’ll be forced to hand it over to someone else, these notes will be extremely useful.</p>
<p>In general, maintain a log of what you experimented with. This will help you avoid running in circles and providing good updates for the team.</p>
<h1 id="heading-create-tools">Create tools!</h1>
<p>Whenever an experiment requires manual labour and you have to do it multiple times, create small tools helping you do it. As an example, if experimenting requires decrypting some values stored in your system, create a piece of code that will do that en masse instead of using online tools and console programs all the time.</p>
<p>If you need to parse some files and you are opening tens of them and looking for clues - create a small JS website that will help you extract the right information.</p>
<p>Mini-tools will help you move faster and you won’t need to repeat the same work. Of course make them only when it is necessary.</p>
<p>At many projects of mine, these tools later became a part of regular testing and development processes.</p>
<h1 id="heading-after-the-incident">After the incident</h1>
<p>Your company likely will have a process, usually it is called RCA - Root Cause Analysis. Someone should schedule a meeting with all of the people involved in the work and prepare a document, later distributed to everyone interested.</p>
<p>On top of this note, relevant work preventing such incidents in the future should be scheduled.</p>
<p>The point of the whole exercise is to learn from mistakes.</p>
<p>This meeting should not be geared towards blaming individuals. It should point out where the processes failed.</p>
<p>When everything is fixed, make sure everyone knows it is the case.</p>
<p>If you spent some extra hours working on it, ask to be compensated or to be able to work less in the coming days. Make sure to note down what you did and how it helped the company and use it on your next evaluation meeting.</p>
<h1 id="heading-summary">Summary</h1>
<p>If you have dealt with production issues at your job, this guide probably sounds familiar. If you are yet to be asked to fix something, this article gives you the proper frame of mind.</p>
<p>Remember:</p>
<ul>
<li>communication is the key, don’t neglect it</li>
<li>fixing a bug is like doing scientific research, form and test hypotheses, note everything down</li>
<li>create tools that will help you with manual work</li>
<li>take and give credit, forget about the blame</li>
</ul>
<p>What are your experiences with troubleshooting live issues?</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <strong><a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li>x/twitter: <strong><a target="_blank" href="https://twitter.com/horosin_">horosin_</a></strong></li>
<li><strong><a target="_blank" href="https://www.linkedin.com/in/horosin/">linkedin</a></strong></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Create a dashboard to track your twitter/X follower stats with APIs and GitHub Actions]]></title><description><![CDATA[I like to have tangible metrics assigned to my goals. Ideally, these would be automatically tracked and there would be a mechanism to hold me accountable.
One of my aims this year is to publish more, be more open about how I work and what I find inte...]]></description><link>https://horosin.com/create-a-dashboard-to-track-your-twitterx-follower-stats-with-apis-and-github-actions</link><guid isPermaLink="true">https://horosin.com/create-a-dashboard-to-track-your-twitterx-follower-stats-with-apis-and-github-actions</guid><category><![CDATA[Twitter]]></category><category><![CDATA[APIs]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Wed, 07 Feb 2024 16:19:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742295869326/19626361-422b-45ad-93e2-5e8207c5579f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I like to have tangible metrics assigned to my goals. Ideally, these would be automatically tracked and there would be a mechanism to hold me accountable.</p>
<p>One of my aims this year is to publish more, be more open about how I work and what I find interesting in my areas of expertise. What metrics can I attach to it? One is certainly a number of posts, the other could be how many people find them interesting enough to follow me.</p>
<p>To see how these metrics change over time, I decided to create a small dashboard that would track their historical values. I decided to start with X/Twitter.</p>
<p>Check out the dashboard created in this tutorial here: <a target="_blank" href="https://horosin.github.io/metrics-dashboard/">https://horosin.github.io/metrics-dashboard/</a></p>
<p>Full code: https://github.com/horosin/metrics-dashboard</p>
<p>You may have heard that X restricted access to their API last year. They did, but they still allow us to access our own basic metrics (contrary to platforms like LinkedIn - shame on you Microsoft, I have to scrape in order to access my data).</p>
<h1 id="heading-what-were-going-to-build">What we’re going to build</h1>
<p>There will be a few pieces of software to write/configure:</p>
<ul>
<li>The code for fetching the data from X</li>
<li>A script saving the data somewhere (in this case in the JSON file in GitHub repository)</li>
<li>Schedule to run the code periodically - every day at a given time</li>
<li>Dashboard to present the data (simple single HTML file using chart.js)</li>
<li>GitHub Pages to host the dashboard</li>
</ul>
<p>The best part is that we can do all of that for free (including compute).</p>
<h1 id="heading-set-up">Set up</h1>
<h2 id="heading-twitter-application">Twitter application</h2>
<p>Setting up a Twitter application in the developer section is a prerequisite for accessing Twitter's API, which is essential for fetching data like follower counts, posting tweets, or accessing other Twitter resources programmatically. Here's a step-by-step guide to get you started.</p>
<ol>
<li><p><strong>Navigate to the Twitter Developer Site</strong>: Go to <a target="_blank" href="https://developer.twitter.com/">Twitter Developer</a> and sign in with your Twitter account. If you don't have a Twitter account, you'll need to create one. Complete the application/sign up.</p>
</li>
<li><p><strong>Go to the Developer Dashboard</strong>: Access your <a target="_blank" href="https://developer.twitter.com/en/portal/dashboard">Twitter Developer Dashboard</a>.</p>
</li>
<li><p><strong>Create a Project</strong>: Click on "Create Project". You will be asked to provide a project name, description, and use case. Fill these out according to your project's needs.</p>
</li>
<li><p><strong>Create an App within Your Project</strong>: After creating your project, you'll have the option to create an app within this project. Click on "Create App" and fill in the necessary details like the App name.</p>
</li>
<li><p><strong>Get Your API Keys and Tokens</strong>: Once your app is created, you will be directed to a page with your app's details, including the API Key, API Secret Key, Access Token, and Access Token Secret. Save these credentials securely; you'll need them to authenticate your requests to the Twitter API.</p>
</li>
</ol>
<h2 id="heading-project">Project</h2>
<p>Now let’s get to coding. Create a new directory on your system and open a console there.</p>
<pre><code class="lang-bash">mkdir metrics-dashboard
<span class="hljs-built_in">cd</span> metrics-dashboard
</code></pre>
<p>Make sure to initialize a Git repository, and later connect it to a GitHub project.</p>
<p>Initialise node.js project and install some packages that we’re going to need to authenticate with the API. </p>
<pre><code class="lang-bash">npm init
npm i dotenv oauth-1.0a crypto
</code></pre>
<p>Create a <code>.env</code>  file with all the keys acquired from X before. DO NOT commit this to the repository. This is only to test the script locally.</p>
<pre><code class="lang-bash">TWITTER_API_KEY=<span class="hljs-string">''</span>
TWITTER_API_SECRET=<span class="hljs-string">''</span>
TWITTER_ACCESS_TOKEN=<span class="hljs-string">''</span>
TWITTER_ACCESS_SECRET=<span class="hljs-string">''</span>
</code></pre>
<p>Create the <code>.gitignore</code> file to avoid this. Sample below contains other paths we’d want to ignore.</p>
<pre><code class="lang-yaml"><span class="hljs-string">node_modules/</span>
<span class="hljs-string">.env</span>
<span class="hljs-string">.DS_Store</span>
<span class="hljs-string">dashboard/data/</span>
</code></pre>
<h1 id="heading-fetching-the-data">Fetching the data</h1>
<p>First, we’ll write a Node.js script called to fetch follower statistics from your platform's API. We'll use standard fetch library to make the API calls and <strong><code>oauth-1.0a</code></strong> to authenticate with X. After fetching the data, we will write results to a JSON file that will serve as our database. This will be handled by a separate script. To make the output accessible to it, we will write it to an environment file available in GitHub Actions.</p>
<p>I am using node 20.</p>
<p>Create a file called <code>x_fetch_data.js</code>  in the root of our project.</p>
<pre><code class="lang-jsx"><span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config();
<span class="hljs-keyword">const</span> OAuth = <span class="hljs-built_in">require</span>(<span class="hljs-string">'oauth-1.0a'</span>);
<span class="hljs-keyword">const</span> crypto = <span class="hljs-built_in">require</span>(<span class="hljs-string">'crypto'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-comment">// Initialize OAuth 1.0a</span>
<span class="hljs-keyword">const</span> oauth = OAuth({
  <span class="hljs-attr">consumer</span>: {
    <span class="hljs-attr">key</span>: process.env.TWITTER_API_KEY, <span class="hljs-comment">// Read from environment variable</span>
    <span class="hljs-attr">secret</span>: process.env.TWITTER_API_SECRET <span class="hljs-comment">// Read from environment variable</span>
  },
  <span class="hljs-attr">signature_method</span>: <span class="hljs-string">'HMAC-SHA1'</span>,
  hash_function(base_string, key) {
    <span class="hljs-keyword">return</span> crypto.createHmac(<span class="hljs-string">'sha1'</span>, key).update(base_string).digest(<span class="hljs-string">'base64'</span>);
  }
});

<span class="hljs-keyword">const</span> token = {
  <span class="hljs-attr">key</span>: process.env.TWITTER_ACCESS_TOKEN, <span class="hljs-comment">// Read from environment variable</span>
  <span class="hljs-attr">secret</span>: process.env.TWITTER_ACCESS_SECRET <span class="hljs-comment">// Read from environment variable</span>
};

<span class="hljs-keyword">const</span> url = <span class="hljs-string">'https://api.twitter.com/2/users/me?user.fields=public_metrics'</span>;

<span class="hljs-keyword">const</span> fetchTwitterFollowerCount = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> requestData = {
    url,
    <span class="hljs-attr">method</span>: <span class="hljs-string">'GET'</span>,
  };

  <span class="hljs-comment">// OAuth header</span>
  <span class="hljs-keyword">const</span> headers = oauth.toHeader(oauth.authorize(requestData, token));
  headers[<span class="hljs-string">'User-Agent'</span>] = <span class="hljs-string">'v2UserLookupJS'</span>;

  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(url, {
    <span class="hljs-attr">method</span>: <span class="hljs-string">'GET'</span>,
    headers
  });

  <span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`HTTP error! status: <span class="hljs-subst">${response.status}</span>`</span>);
  }

  <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
  <span class="hljs-built_in">console</span>.log(data);

  <span class="hljs-comment">// Extract the metrics</span>
  <span class="hljs-keyword">const</span> metrics = data?.data?.public_metrics;

  <span class="hljs-comment">// Write the metrics to the environment file</span>
  fs.appendFileSync(process.env.GITHUB_OUTPUT, <span class="hljs-string">`METRICS=<span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(metrics)}</span>\n`</span>);
};

fetchTwitterFollowerCount().catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> <span class="hljs-built_in">console</span>.error(err));
</code></pre>
<p>To test the script, you can run:</p>
<pre><code class="lang-jsx">GITHUB_OUTPUT=testoutput node x_fetch_data.js
</code></pre>
<p>You should see your X metrics in the output, as well as in the <code>testoutput</code> file:</p>
<pre><code class="lang-jsx">metrics={<span class="hljs-string">"followers_count"</span>:<span class="hljs-number">288</span>,<span class="hljs-string">"following_count"</span>:<span class="hljs-number">302</span>,<span class="hljs-string">"tweet_count"</span>:<span class="hljs-number">1381</span>,<span class="hljs-string">"listed_count"</span>:<span class="hljs-number">0</span>,<span class="hljs-string">"like_count"</span>:<span class="hljs-number">591</span>}
</code></pre>
<h1 id="heading-saving-the-data">Saving the data</h1>
<p>To save the data, create another script in a file <code>x_save_data.js</code> . It will take the output from the environment and append it to the <code>./data/x.json</code> .</p>
<p>Make sure to create this file first and commit it to the git repository. It should have an empty array as its content.</p>
<pre><code class="lang-jsx">[]
</code></pre>
<p>The script also doesn’t add a duplicate record if the data was already fetched that day. It overwrites the old one instead.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-comment">// Parse the JSON string from the environment variable</span>
<span class="hljs-keyword">const</span> metrics = <span class="hljs-built_in">JSON</span>.parse(process.env.METRICS);
<span class="hljs-keyword">const</span> path = <span class="hljs-string">'./data/x.json'</span>;
<span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
<span class="hljs-keyword">const</span> today = <span class="hljs-string">`<span class="hljs-subst">${now.getFullYear()}</span>-<span class="hljs-subst">${<span class="hljs-built_in">String</span>(now.getMonth() + <span class="hljs-number">1</span>).padStart(<span class="hljs-number">2</span>, <span class="hljs-string">'0'</span>)}</span>-<span class="hljs-subst">${<span class="hljs-built_in">String</span>(now.getDate()).padStart(<span class="hljs-number">2</span>, <span class="hljs-string">'0'</span>)}</span>`</span>;

<span class="hljs-keyword">let</span> data = [];
<span class="hljs-keyword">if</span> (fs.existsSync(path)) {
    data = <span class="hljs-built_in">JSON</span>.parse(fs.readFileSync(path));
}

<span class="hljs-keyword">const</span> todayIndex = data.findIndex(<span class="hljs-function"><span class="hljs-params">entry</span> =&gt;</span> entry.date === today);
<span class="hljs-keyword">if</span> (todayIndex &gt; <span class="hljs-number">-1</span>) {
    data[todayIndex] = { <span class="hljs-attr">date</span>: today, ...metrics };
} <span class="hljs-keyword">else</span> {
    data.push({ <span class="hljs-attr">date</span>: today, ...metrics });
}

fs.writeFileSync(path, <span class="hljs-built_in">JSON</span>.stringify(data, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));
</code></pre>
<p>You can test the script by editing testouput file by adding single quotes around the JSON and then running the following. File edit I necessary, as GitHub Actions environment behaves differently and doesn't need the quotes.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># load output from the previous script</span>
<span class="hljs-built_in">set</span> -a; <span class="hljs-built_in">source</span> testoutput; <span class="hljs-built_in">set</span> +a;
node x_save_data.js
</code></pre>
<h1 id="heading-scheduled-github-action">Scheduled GitHub Action</h1>
<p>Now, let’s create a file with GitHub action code. It will run every day at a specified time and fetch our metrics. It will then save them and commit to the repository.</p>
<p>Save the following code under <code>.github/workflows/fetch_x_data.yml</code> .</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Fetch</span> <span class="hljs-string">X</span> <span class="hljs-string">Data</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">schedule:</span>
    <span class="hljs-comment"># Runs at 4 AM UTC</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">'0 4 * * *'</span>
  <span class="hljs-attr">workflow_dispatch:</span> <span class="hljs-comment"># This line enables manual triggering of the action</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">fetch_data:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">permissions:</span>
      <span class="hljs-attr">contents:</span> <span class="hljs-string">write</span>
      <span class="hljs-attr">pull-requests:</span> <span class="hljs-string">write</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Check</span> <span class="hljs-string">out</span> <span class="hljs-string">the</span> <span class="hljs-string">repository</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Node.js</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">node-version:</span> <span class="hljs-string">"20"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Fetch</span> <span class="hljs-string">Data</span> <span class="hljs-string">from</span> <span class="hljs-string">Platform</span> <span class="hljs-string">X</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">fetch_data</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">node</span> <span class="hljs-string">x_fetch_data.js</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">TWITTER_API_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.TWITTER_API_KEY</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">TWITTER_API_SECRET:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.TWITTER_API_SECRET</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">TWITTER_ACCESS_TOKEN:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.TWITTER_ACCESS_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">TWITTER_ACCESS_SECRET:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.TWITTER_ACCESS_SECRET</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Save</span> <span class="hljs-string">data</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">node</span> <span class="hljs-string">x_save_data.js</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">METRICS:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.fetch_data.outputs.METRICS</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Commit</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">if</span> <span class="hljs-string">there's</span> <span class="hljs-string">changes</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          git config --global user.email "action@github.com"
          git config --global user.name "GitHub Action"
          git add data/x.json
          git commit -m "Update data for Platform X" || exit 0  # exit 0 if no changes
          git push</span>
</code></pre>
<p>Run the action manually by committing the code and then going to “Actions” section of your project on GitHub and triggering it from there.</p>
<h1 id="heading-dashboard">Dashboard</h1>
<p>Okay, how about presenting the data? I didn’t want to mess around with simple HTML, so I asked ChatGPT to generate it for me.</p>
<p>Create an <code>index.html</code>  file in the <code>dashboard</code> folder. We’re not using the main directory of our project in order to avoid hosting the data fetching code alongside the HTML.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Twitter Dashboard<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/chart.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">font-family</span>: Arial, sans-serif;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">display</span>: flex;
      <span class="hljs-attribute">flex-direction</span>: column;
      <span class="hljs-attribute">align-items</span>: center;
    }

    <span class="hljs-selector-class">.chart-container</span> {
      <span class="hljs-attribute">width</span>: <span class="hljs-number">80%</span>;
      <span class="hljs-attribute">max-width</span>: <span class="hljs-number">1000px</span>;
    }

    <span class="hljs-selector-tag">canvas</span> {
      <span class="hljs-attribute">max-width</span>: <span class="hljs-number">100%</span>;
    }

    <span class="hljs-selector-tag">h1</span> {
      <span class="hljs-attribute">text-align</span>: center;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">20px</span>;
    }

    <span class="hljs-selector-tag">h2</span> {
      <span class="hljs-attribute">text-align</span>: center;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">20px</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Twitter Dashboard<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Number of Followers<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"chart-container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"followersChart"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">canvas</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Number of Tweets<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"chart-container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"tweetsChart"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">canvas</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    fetch(<span class="hljs-string">'data/x.json'</span>)
      .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> response.json())
      .then(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> {
        <span class="hljs-keyword">const</span> dates = data.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.date);
        <span class="hljs-keyword">const</span> followers = data.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.followers_count);
        <span class="hljs-keyword">const</span> tweets = data.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.tweet_count);

        <span class="hljs-keyword">const</span> minFollowers = <span class="hljs-built_in">Math</span>.min(...followers) - <span class="hljs-number">100</span>;
        <span class="hljs-keyword">const</span> minTweets = <span class="hljs-built_in">Math</span>.min(...tweets) - <span class="hljs-number">100</span>;

        <span class="hljs-keyword">const</span> followersCtx = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'followersChart'</span>).getContext(<span class="hljs-string">'2d'</span>);
        <span class="hljs-keyword">const</span> tweetsCtx = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'tweetsChart'</span>).getContext(<span class="hljs-string">'2d'</span>);

        <span class="hljs-keyword">new</span> Chart(followersCtx, {
          <span class="hljs-attr">type</span>: <span class="hljs-string">'line'</span>,
          <span class="hljs-attr">data</span>: {
            <span class="hljs-attr">labels</span>: dates,
            <span class="hljs-attr">datasets</span>: [{
              <span class="hljs-attr">label</span>: <span class="hljs-string">'Followers'</span>,
              <span class="hljs-attr">data</span>: followers,
              <span class="hljs-attr">backgroundColor</span>: <span class="hljs-string">'rgba(54, 162, 235, 0.2)'</span>,
              <span class="hljs-attr">borderColor</span>: <span class="hljs-string">'rgba(54, 162, 235, 1)'</span>,
              <span class="hljs-attr">borderWidth</span>: <span class="hljs-number">1</span>
            }]
          },
          <span class="hljs-attr">options</span>: {
            <span class="hljs-attr">scales</span>: {
              <span class="hljs-attr">y</span>: {
                <span class="hljs-attr">beginAtZero</span>: <span class="hljs-literal">false</span>,
                <span class="hljs-attr">min</span>: minFollowers
              }
            }
          }
        });

        <span class="hljs-keyword">new</span> Chart(tweetsCtx, {
          <span class="hljs-attr">type</span>: <span class="hljs-string">'line'</span>,
          <span class="hljs-attr">data</span>: {
            <span class="hljs-attr">labels</span>: dates,
            <span class="hljs-attr">datasets</span>: [{
              <span class="hljs-attr">label</span>: <span class="hljs-string">'Tweets'</span>,
              <span class="hljs-attr">data</span>: tweets,
              <span class="hljs-attr">backgroundColor</span>: <span class="hljs-string">'rgba(255, 99, 132, 0.2)'</span>,
              <span class="hljs-attr">borderColor</span>: <span class="hljs-string">'rgba(255, 99, 132, 1)'</span>,
              <span class="hljs-attr">borderWidth</span>: <span class="hljs-number">1</span>
            }]
          },
          <span class="hljs-attr">options</span>: {
            <span class="hljs-attr">scales</span>: {
              <span class="hljs-attr">y</span>: {
                <span class="hljs-attr">beginAtZero</span>: <span class="hljs-literal">false</span>,
                <span class="hljs-attr">min</span>: minTweets
              }
            }
          }
        });
      });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Commit it to the repository.</p>
<p>(optional) If you want to test it locally, do so by copying the data folder to the dashboard folder and launching a simple server inside it.</p>
<pre><code class="lang-bash">cp -r data dashboard/
<span class="hljs-built_in">cd</span> dashboard
<span class="hljs-comment"># start server with Python if you have it installed (version 3)</span>
<span class="hljs-comment"># otherwise, use other way e. g. https://gist.github.com/willurd/5720255</span>
python -m http.server 8000
</code></pre>
<h1 id="heading-dashboard-deployment-to-github-pages">Dashboard deployment to GitHub Pages</h1>
<p>Now that we have our dashboard, it’s time to present it to the web!</p>
<p>If you are using a free account on GitHub, your page needs to be public, as well as the whole repository.</p>
<p>Create a <code>.github/workflows/deploy_dashboard.yml</code> file.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">GitHub</span> <span class="hljs-string">Pages</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">schedule:</span>
    <span class="hljs-comment"># redeploy after data update</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">'0 5 * * *'</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
  <span class="hljs-attr">workflow_dispatch:</span> <span class="hljs-comment"># This line enables manual triggering of the action</span>

<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>
  <span class="hljs-attr">pages:</span> <span class="hljs-string">write</span>
  <span class="hljs-attr">id-token:</span> <span class="hljs-string">write</span>

<span class="hljs-comment"># Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.</span>
<span class="hljs-comment"># However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.</span>
<span class="hljs-attr">concurrency:</span>
  <span class="hljs-attr">group:</span> <span class="hljs-string">"pages"</span>
  <span class="hljs-attr">cancel-in-progress:</span> <span class="hljs-literal">false</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">deploy:</span>
    <span class="hljs-attr">permissions:</span>
      <span class="hljs-attr">pages:</span> <span class="hljs-string">write</span>      <span class="hljs-comment"># to deploy to Pages</span>
      <span class="hljs-attr">id-token:</span> <span class="hljs-string">write</span>   <span class="hljs-comment"># to verify the deployment originates from an appropriate source</span>

    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">github-pages</span>
      <span class="hljs-attr">url:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.deployment.outputs.page_url</span> <span class="hljs-string">}}</span>

    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Pages</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/configure-pages@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Copy</span> <span class="hljs-string">data</span> <span class="hljs-string">to</span> <span class="hljs-string">dashboard</span> <span class="hljs-string">folder</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">cp</span> <span class="hljs-string">-r</span> <span class="hljs-string">data</span> <span class="hljs-string">dashboard/</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">pages</span> <span class="hljs-string">artifact</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-pages-artifact@v3</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">dashboard/</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">GitHub</span> <span class="hljs-string">Pages</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">deployment</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/deploy-pages@v4</span> <span class="hljs-comment"># or specific "vX.X.X" version tag for this action</span>
</code></pre>
<p>The action should deploy the page. You can find the URL in your Github project settings or in the Actions section in the workflow output.</p>
<p>Again, you can find mine here: <a target="_blank" href="https://horosin.github.io/metrics-dashboard/">https://horosin.github.io/metrics-dashboard/</a>.</p>
<h1 id="heading-summary">Summary</h1>
<p>And there you have it! A complete, automated system to track your social media (X) metrics, automate data fetching, save historical data, and visualize the trends. With this setup, you can extend the functionality to other platforms and metrics, creating a comprehensive dashboard for all your analytical needs. Let me know if it’s something you’d like to read about.</p>
<p>If you liked this content, please support me by sharing this post and subscribing to my <strong><a target="_blank" href="https://horosin.com/newsletter">Newsletter</a>.</strong></p>
<p>You can also find me here:</p>
<ul>
<li>x/twitter: <strong><a target="_blank" href="https://twitter.com/horosin_">horosin_</a></strong></li>
<li><strong><a target="_blank" href="https://www.linkedin.com/in/horosin/">linkedin</a></strong></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Deploy a language model (LLM) on AWS Lambda]]></title><description><![CDATA[If you just want to see the code - see github.
What are we going to build any why?
LLMs are a new hot piece of technology that everyone is experimenting with. Managed services like OpenAI are the cheapest and most convenient way to use them. You can ...]]></description><link>https://horosin.com/deploy-a-language-model-llm-on-aws-lambda</link><guid isPermaLink="true">https://horosin.com/deploy-a-language-model-llm-on-aws-lambda</guid><category><![CDATA[llm]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Docker]]></category><category><![CDATA[AI]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Sat, 06 Jan 2024 20:01:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704570589207/c0e981e9-b546-479a-8383-ba9d266b688b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you just want to see the code - see <a target="_blank" href="https://github.com/horosin/llm-on-aws-lambda">github</a>.</p>
<h1 id="heading-what-are-we-going-to-build-any-why">What are we going to build any why?</h1>
<p>LLMs are a new hot piece of technology that everyone is experimenting with. Managed services like OpenAI are the cheapest and most convenient way to use them. You can use their LLMs only for a small fee and for an approval to train new versions on your data (in some cases).</p>
<p>In certain applications, it is required to run an LLM on our own. You may want to process sensitive data (medical records or legal documents) or get great quality output in a language different than English. Sometimes you have a specialised task that doesn’t require expensive big models from OpenAI.</p>
<p>Open source LLMs are matching quality of big players like OpenAI but still require a lot of expensive compute resources. There are smaller models available that can run on weaker hardware and perform well enough.</p>
<p>As an experiment let’s deploy a smaller open source LLM on AWS lambda. Our goal is to learn more about LLMs and docker-based lambdas. We will also evaluate performance and cost to determine if any real-world applications are feasible.</p>
<p>For this project, we are going to use <a target="_blank" href="https://huggingface.co/microsoft/phi-2">Microsoft Phi-2</a> model, 2.7 billion parameter LLM that matches quality of outputs from 13B parameter or more open-source models. It was trained on a large dataset and is a viable model for many applications. From my experience it hallucinates a lot but otherwise provides useful outputs. Its size is perfect for AWS Lambda environment.</p>
<p>We will download the model from Huggingface and run it via <code>llama-cpp-python</code> package (bindings to the popular llama.cpp, heavily optimised model CPU runtime). We will use smaller, quantised version but even the full one should fit in Lambda memory.</p>
<p>We will create an HTTP REST endpoint via lambda URL mechanism, that for a given call:</p>
<pre><code class="lang-jsx">PROMPT=<span class="hljs-string">"Create five questions for a job interview for a senior python software engineer position."</span>

curl $LAMBDA_URL -d <span class="hljs-string">"{ \"prompt\": \"$PROMPT\" }"</span> \
    | jq -r <span class="hljs-string">'.choices[0].text, .usage'</span>
</code></pre>
<p>(jq command used to decode returned JSON)</p>
<p>Provides LLM output alongside execution details:</p>
<pre><code class="lang-plaintext">Instruct: Create five questions for a job interview for a senior python software engineer position.
Output: Questions: 

1. What experience do you have in developing web applications?
2. What is your familiarity with different Python programming languages?
3. How would you approach debugging a complex Python program?
4. Can you explain how object-oriented programming principles can be applied to software development? 
5. In a recent project, you were responsible for managing the codebase of a team of developers. Can you discuss your experience with this process?

{
  "prompt_tokens": 21,
  "completion_tokens": 95,
  "total_tokens": 116
}
</code></pre>
<p>For this tutorial you need to know basics of: programming, docker, AWS, Python.</p>
<p>I am using an M-series Mac, so as a default, we’re going to aim at deployment to ARM AWS Lambda, as it simply costs less for more performance. Instructions how to deploy x86 version are included.</p>
<h1 id="heading-environment-setup-aws-docker-and-python">Environment setup (AWS, Docker and Python)</h1>
<p>I won’t dive into the basics here, if you don’t have the tools installed, please follow these resources.</p>
<ol>
<li><p>You need AWS account and AWS CLI installed and configured.</p>
<ol>
<li><p>Good tutorial from google search: <a target="_blank" href="https://cloud-gc.readthedocs.io/en/latest/chapter02_beginner-tutorial/awscli-config.html">tutorial</a>.</p>
</li>
<li><p>Official docs, including if you’re using SSO: <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html">AWS official tutorial.</a></p>
</li>
</ol>
</li>
<li><p>You also need docker: <a target="_blank" href="https://docs.docker.com/engine/install/">docker docs.</a></p>
</li>
<li><p>An IDE, I am using Visual Studio Code.</p>
</li>
</ol>
<h1 id="heading-set-up-lambda-function-with-docker-locally">Set up lambda function with docker locally</h1>
<p>Let’s start with getting local environment running. If you need more information than I included here, check out <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/python-image.html">official AWS documentation.</a></p>
<p>We will store every file in single project directory, without subfolders.</p>
<p>First we will need a basic Python lambda function handler. In your project folder, create a file called <code>lambda_function.py</code>.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sys

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handler</span>(<span class="hljs-params">event, context</span>):</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Hello from AWS Lambda using Python"</span> + sys.version + <span class="hljs-string">"!"</span>
</code></pre>
<p>We will also create a <code>requirements.txt</code> file, in which we specify our dependencies. Let’s start with AWS library for interacting with their services, just as an example.</p>
<pre><code class="lang-plaintext">boto3
</code></pre>
<p>Then, we need to specify our docker image composition in <code>Dockerfile</code> file. Comments document what each line does.</p>
<pre><code class="lang-docker"><span class="hljs-keyword">FROM</span> public.ecr.aws/lambda/python:<span class="hljs-number">3.12</span>

<span class="hljs-comment"># Copy requirements.txt</span>
<span class="hljs-keyword">COPY</span><span class="bash"> requirements.txt <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span></span>

<span class="hljs-comment"># Install the specified packages</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install -r requirements.txt</span>

<span class="hljs-comment"># Copy function code</span>
<span class="hljs-keyword">COPY</span><span class="bash"> lambda_function.py <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span></span>

<span class="hljs-comment"># Set the CMD to your handler</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"lambda_function.handler"</span> ]</span>
</code></pre>
<p>Finally, we will create a <code>docker-compose.yml</code> file to make our life easier when running and building the container.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">llm-lambda:</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">llm-lambda</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-number">9000</span><span class="hljs-string">:8080</span>
</code></pre>
<p>OK, done! Make sure your docker engine is running and type in the terminal:</p>
<pre><code class="lang-bash">docker-compose up
</code></pre>
<p>The container should build and start. Our lambda is available under a long URL below (quirks of official Amazon image).</p>
<p>To test if the lambda is running, open another terminal window / tab and type in:</p>
<pre><code class="lang-bash">curl <span class="hljs-string">"http://localhost:9000/2015-03-31/functions/function/invocations"</span> -d <span class="hljs-string">'{}'</span>
</code></pre>
<p>Now you can come back to the tab you ran docker-compose in and type ctrl-c to stop the container.</p>
<p>You can put this test code in a bash script, as seen in the project repo. Also, it will be useful to create a .gitignore file, if you’re going to version control this project.</p>
<h1 id="heading-run-an-llm-inside-a-container">Run an LLM inside a container</h1>
<p>Now that we have a working lambda, let’s add some AI magic.</p>
<p>To run an LLM, we need to add <code>llama-cpp-python</code> to our <code>requirements.txt</code>.</p>
<pre><code class="lang-plaintext">boto3
llama-cpp-python
</code></pre>
<p>To build it, we need to introduce a <a target="_blank" href="https://docs.docker.com/build/building/multi-stage/">docker build stage</a>. This is because default amazon docker image doesn’t include build tools required for llama-cpp. We are doing a pip install with <code>CMAKE_ARGS="-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"</code> flags, in order to use multi-threaded optimisations.</p>
<p>The code below also includes downloading the model. Community hero, the Bloke, is sharing compressed (quantised, less computationally intensive but lower quality) versions of models, ready to download <a target="_blank" href="https://huggingface.co/TheBloke/phi-2-GGUF">here</a>. To make use of them, we can install the huggingface CLI and run appropriate command. You can switch the repository (<code>TheBloke/phi-2-GGUF</code>) and the model (<code>phi-2.Q4_K_M.gguf</code>) to whatever you like, if you want to deploy a different model.</p>
<pre><code class="lang-python">RUN pip install huggingface-hub &amp;&amp; \
    mkdir model &amp;&amp; \
    huggingface-cli download TheBloke/phi<span class="hljs-number">-2</span>-GGUF phi<span class="hljs-number">-2.</span>Q4_K_M.gguf --local-dir ./model --local-dir-use-symlinks <span class="hljs-literal">False</span>
</code></pre>
<pre><code class="lang-docker"><span class="hljs-comment"># Stage 1: Build environment using a Python base image</span>
<span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.12</span> as builder

<span class="hljs-comment"># Install build tools</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; apt-get install -y gcc g++ cmake zip</span>

<span class="hljs-comment"># Copy requirements.txt and install packages with appropriate CMAKE_ARGS</span>
<span class="hljs-keyword">COPY</span><span class="bash"> requirements.txt .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> CMAKE_ARGS=<span class="hljs-string">"-DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=OpenBLAS"</span> pip install --upgrade pip &amp;&amp; pip install -r requirements.txt</span>

<span class="hljs-comment"># Stage 2: Final image using AWS Lambda Python image</span>
<span class="hljs-keyword">FROM</span> public.ecr.aws/lambda/python:<span class="hljs-number">3.12</span>

<span class="hljs-comment"># Install huggingface-cli and download the model</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install huggingface-hub &amp;&amp; \
    mkdir model &amp;&amp; \
    huggingface-cli download TheBloke/phi-2-GGUF phi-2.Q4_K_M.gguf --local-dir ./model --local-dir-use-symlinks False</span>

<span class="hljs-comment"># Copy installed packages from builder stage</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /usr/<span class="hljs-built_in">local</span>/lib/python3.12/site-packages/ /var/lang/lib/python3.12/site-packages/</span>

<span class="hljs-comment"># Copy lambda function code</span>
<span class="hljs-keyword">COPY</span><span class="bash"> lambda_function.py <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span></span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"lambda_function.handler"</span> ]</span>
</code></pre>
<p>Let’s modify our lambda code to run LLM inference. Some more work is required to get the prompt out of the request body in the production environment.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> base64
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">from</span> llama_cpp <span class="hljs-keyword">import</span> Llama

<span class="hljs-comment"># Load the LLM, outside the handler so it persists between runs</span>
llm = Llama(
    model_path=<span class="hljs-string">"./model/phi-2.Q4_K_M.gguf"</span>, <span class="hljs-comment"># change if different model</span>
    n_ctx=<span class="hljs-number">2048</span>, <span class="hljs-comment"># context length</span>
    n_threads=<span class="hljs-number">6</span>,  <span class="hljs-comment"># maximum in AWS Lambda</span>
)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handler</span>(<span class="hljs-params">event, context</span>):</span>
    print(<span class="hljs-string">"Event is:"</span>, event)
    print(<span class="hljs-string">"Context is:"</span>, context)

        <span class="hljs-comment"># Locally the body is not encoded, via lambda URL it is</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">if</span> event.get(<span class="hljs-string">'isBase64Encoded'</span>, <span class="hljs-literal">False</span>):
            body = base64.b64decode(event[<span class="hljs-string">'body'</span>]).decode(<span class="hljs-string">'utf-8'</span>)
        <span class="hljs-keyword">else</span>:
            body = event[<span class="hljs-string">'body'</span>]

        body_json = json.loads(body)
        prompt = body_json[<span class="hljs-string">"prompt"</span>]
    <span class="hljs-keyword">except</span> (KeyError, json.JSONDecodeError) <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> {<span class="hljs-string">"statusCode"</span>: <span class="hljs-number">400</span>, <span class="hljs-string">"body"</span>: <span class="hljs-string">f"Error processing request: <span class="hljs-subst">{str(e)}</span>"</span>}

    output = llm(
        <span class="hljs-string">f"Instruct: <span class="hljs-subst">{prompt}</span>\nOutput:"</span>,
        max_tokens=<span class="hljs-number">512</span>, 
        echo=<span class="hljs-literal">True</span>,
    )

    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"statusCode"</span>: <span class="hljs-number">200</span>,
        <span class="hljs-string">"body"</span>: json.dumps(output)
    }
</code></pre>
<p>OK, time to test! My docker setup has 8GB of RAM assigned, but it should run well under 4GB.</p>
<p>Let’s rebuild the container and start it again. It will take more time, as the model needs to download (~1.8GB).</p>
<pre><code class="lang-bash">docker-compose up --build
</code></pre>
<p>This time, let’s test with a real prompt. First run will take more time, as the model needs to load.</p>
<pre><code class="lang-bash">curl <span class="hljs-string">"http://localhost:9000/2015-03-31/functions/function/invocations"</span> \
    -d <span class="hljs-string">'{ "body": "{ \"prompt\": \"Generate a good name for a bakery.\" }" }'</span>
</code></pre>
<p>Example output:</p>
<pre><code class="lang-bash">Instruct: Generate a good name <span class="hljs-keyword">for</span> a bakery.
Output: Sugar Rush Bakery.

{
  <span class="hljs-string">"prompt_tokens"</span>: 15,
  <span class="hljs-string">"completion_tokens"</span>: 6,
  <span class="hljs-string">"total_tokens"</span>: 21
}
</code></pre>
<h1 id="heading-deploy-to-aws-lambda">Deploy to AWS Lambda</h1>
<p>With everything working locally, let’s finally deploy our LLM to AWS!</p>
<p>To execute the deployment successfully, the following steps are required:</p>
<ol>
<li><p><strong>Initial Setup</strong>: Determine necessary information such as AWS region, ECR repository name, Docker platform, IAM policy file, and disable AWS CLI pager.</p>
</li>
<li><p><strong>Verify AWS Configuration</strong>: Optionally confirm that the AWS CLI is correctly configured.</p>
</li>
<li><p><strong>ECR (Elastic Container Registry) Repository Management</strong>:</p>
<ul>
<li><p>Determine if the specified ECR repository already exists.</p>
</li>
<li><p>If absent, create a new ECR repository.</p>
</li>
</ul>
</li>
<li><p><strong>IAM Role Handling</strong>:</p>
<ul>
<li><p>Check for the existence of a specific IAM role for Lambda.</p>
</li>
<li><p>If not found, establish the IAM role and apply the <code>AWSLambdaBasicExecutionRole</code> policy.</p>
</li>
</ul>
</li>
<li><p><strong>Docker-ECR Authentication</strong>: Securely log Docker into the ECR registry using the retrieved login credentials.</p>
</li>
<li><p><strong>Docker Image Construction</strong>: Utilize Docker Compose to build the Docker image, specifying the desired platform.</p>
</li>
<li><p><strong>ECR Image Tagging</strong>: Label the Docker image appropriately for ECR upload.</p>
</li>
<li><p><strong>ECR Image Upload</strong>: Transfer the tagged Docker image to the ECR.</p>
</li>
<li><p><strong>Acquire IAM Role ARN</strong>: Fetch the ARN linked to the specified IAM role.</p>
</li>
<li><p><strong>Lambda Function Verification</strong>: Assess whether the Lambda function exists.</p>
</li>
<li><p><strong>Lambda Function Configuration</strong>: Set parameters like timeout, memory allocation, and image URI for Lambda.</p>
</li>
<li><p><strong>Lambda Function Deployment/Update</strong>:</p>
<ul>
<li><p>For a new Lambda function, create it with defined settings and establish a public Function URL.</p>
</li>
<li><p>For an existing Lambda function, update its code using the new Docker image URI.</p>
</li>
</ul>
</li>
<li><p><strong>Function URL Retrieval</strong>: Obtain and display the Function URL of the Lambda function.</p>
</li>
</ol>
<p>It is a simple but lengthy process to do manually, so I’ve created a script using AWS CLI. Feel free to modify the variables and run it yourself. The script is quite complex, you can do all the steps manually in the GUI if you want, apart from pushing the container.</p>
<p>If you are not doing this on M-series Mac, be sure to change linux/arm64 to linux/amd64.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># Variables</span>
AWS_REGION=<span class="hljs-string">"eu-central-1"</span>
ECR_REPO_NAME=<span class="hljs-string">"llm-lambda"</span>
IMAGE_TAG=<span class="hljs-string">"latest"</span>
LAMBDA_FUNCTION_NAME=<span class="hljs-string">"llm-lambda"</span>
LAMBDA_ROLE_NAME=<span class="hljs-string">"llm-lambda-role"</span> <span class="hljs-comment"># Role name to create, not ARN</span>
DOCKER_PLATFORM=<span class="hljs-string">"linux/arm64"</span> <span class="hljs-comment"># Change as needed, e.g., linux/amd64</span>
IAM_POLICY_FILE=<span class="hljs-string">"trust-policy.json"</span>
PAGER= <span class="hljs-comment"># Disable pager for AWS CLI</span>

<span class="hljs-comment"># Authenticate with AWS</span>
aws configure list <span class="hljs-comment"># Optional, just to verify AWS CLI is configured</span>

<span class="hljs-comment"># Check if the ECR repository exists</span>
REPO_EXISTS=$(aws ecr describe-repositories --repository-names <span class="hljs-variable">$ECR_REPO_NAME</span> --region <span class="hljs-variable">$AWS_REGION</span> 2&gt;&amp;1)

<span class="hljs-keyword">if</span> [ $? -ne 0 ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Repository does not exist. Creating repository: <span class="hljs-variable">$ECR_REPO_NAME</span>"</span>
    <span class="hljs-comment"># Create ECR repository</span>
    aws ecr create-repository --repository-name <span class="hljs-variable">$ECR_REPO_NAME</span> --region <span class="hljs-variable">$AWS_REGION</span>
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Repository <span class="hljs-variable">$ECR_REPO_NAME</span> already exists. Skipping creation."</span>
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Check if the Lambda IAM role exists</span>
ROLE_EXISTS=$(aws iam get-role --role-name <span class="hljs-variable">$LAMBDA_ROLE_NAME</span> 2&gt;&amp;1)

<span class="hljs-keyword">if</span> [ $? -ne 0 ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"IAM role does not exist. Creating role: <span class="hljs-variable">$LAMBDA_ROLE_NAME</span>"</span>
    <span class="hljs-comment"># Create IAM role for Lambda</span>
    aws iam create-role --role-name <span class="hljs-variable">$LAMBDA_ROLE_NAME</span> --assume-role-policy-document file://<span class="hljs-variable">$IAM_POLICY_FILE</span>
    aws iam attach-role-policy --role-name <span class="hljs-variable">$LAMBDA_ROLE_NAME</span> --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"IAM role <span class="hljs-variable">$LAMBDA_ROLE_NAME</span> already exists. Skipping creation."</span>
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Get login command from ECR and execute it to authenticate Docker to the registry</span>
aws ecr get-login-password --region <span class="hljs-variable">$AWS_REGION</span> | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com

<span class="hljs-comment"># Build the Docker image using Docker Compose with specific platform</span>
DOCKER_BUILDKIT=1 docker-compose build --build-arg BUILDPLATFORM=<span class="hljs-variable">$DOCKER_PLATFORM</span>

<span class="hljs-comment"># Tag the Docker image for ECR</span>
docker tag llm-lambda:latest $(aws sts get-caller-identity --query Account --output text).dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPO_NAME</span>:<span class="hljs-variable">$IMAGE_TAG</span>

<span class="hljs-comment"># Push the Docker image to ECR</span>
docker push $(aws sts get-caller-identity --query Account --output text).dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPO_NAME</span>:<span class="hljs-variable">$IMAGE_TAG</span>

<span class="hljs-comment"># Get the IAM role ARN</span>
LAMBDA_ROLE_ARN=$(aws iam get-role --role-name <span class="hljs-variable">$LAMBDA_ROLE_NAME</span> --query <span class="hljs-string">'Role.Arn'</span> --output text)

<span class="hljs-comment"># Check if Lambda function exists</span>
FUNCTION_EXISTS=$(aws lambda get-function --function-name <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span> --region <span class="hljs-variable">$AWS_REGION</span> 2&gt;&amp;1)

<span class="hljs-comment"># Parameters for Lambda function</span>
LAMBDA_TIMEOUT=300 <span class="hljs-comment"># 5 minutes in seconds</span>
LAMBDA_MEMORY_SIZE=10240 <span class="hljs-comment"># Maximum memory size in MB</span>
LAMBDA_IMAGE_URI=$(aws sts get-caller-identity --query Account --output text).dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$ECR_REPO_NAME</span>:<span class="hljs-variable">$IMAGE_TAG</span>

<span class="hljs-comment"># Deploy or update the Lambda function</span>
<span class="hljs-keyword">if</span> <span class="hljs-built_in">echo</span> <span class="hljs-variable">$FUNCTION_EXISTS</span> | grep -q <span class="hljs-string">"ResourceNotFoundException"</span>; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating new Lambda function: <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span>"</span>
    aws lambda create-function --function-name <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span> \
        --region <span class="hljs-variable">$AWS_REGION</span> \
        --role <span class="hljs-variable">$LAMBDA_ROLE_ARN</span> \
        --timeout <span class="hljs-variable">$LAMBDA_TIMEOUT</span> \
        --memory-size <span class="hljs-variable">$LAMBDA_MEMORY_SIZE</span> \
        --package-type Image \
        --architectures arm64 \
        --code ImageUri=<span class="hljs-variable">$LAMBDA_IMAGE_URI</span>

    aws lambda create-function-url-config --function-name <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span> \
        --auth-type <span class="hljs-string">"NONE"</span> --region <span class="hljs-variable">$AWS_REGION</span>

    <span class="hljs-comment"># Add permission to allow public access to the Function URL</span>
    aws lambda add-permission --function-name <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span> \
        --region <span class="hljs-variable">$AWS_REGION</span> \
        --statement-id <span class="hljs-string">"FunctionURLAllowPublicAccess"</span> \
        --action <span class="hljs-string">"lambda:InvokeFunctionUrl"</span> \
        --principal <span class="hljs-string">"*"</span> \
        --function-url-auth-type <span class="hljs-string">"NONE"</span>
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Updating existing Lambda function: <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span>"</span>
    aws lambda update-function-code --function-name <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span> \
        --region <span class="hljs-variable">$AWS_REGION</span> \
        --image-uri <span class="hljs-variable">$LAMBDA_IMAGE_URI</span>
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Retrieve and print the Function URL</span>
FUNCTION_URL=$(aws lambda get-function-url-config --region <span class="hljs-variable">$AWS_REGION</span> --function-name <span class="hljs-variable">$LAMBDA_FUNCTION_NAME</span> --query <span class="hljs-string">'FunctionUrl'</span> --output text)
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Lambda Function URL: <span class="hljs-variable">$FUNCTION_URL</span>"</span>
</code></pre>
<p>Now, let’s add proper permissions and run the script.</p>
<pre><code class="lang-bash">chmod +x deploy.sh
./deploy.sh
<span class="hljs-comment"># AWS_PROFILE=your_profile ./deploy.sh </span>
<span class="hljs-comment"># if you want to use a different profile than default</span>
</code></pre>
<p>As a result of the script, you should se the lambda function URL in your terminal. Let’s use it for a test.</p>
<pre><code class="lang-bash">PROMPT=<span class="hljs-string">"Create five questions for a job interview for a senior python software engineer position."</span>

curl <span class="hljs-variable">$LAMBDA_URL</span> -d <span class="hljs-string">"{ \"prompt\": \"<span class="hljs-variable">$PROMPT</span>\" }"</span> \
    | jq -r <span class="hljs-string">'.choices[0].text, .usage'</span>
</code></pre>
<p>Like before, the first run will take longer.</p>
<pre><code class="lang-bash">chmod +x test_remote.sh
LAMBDA_URL=your_url ./test_remote.sh
</code></pre>
<p>You should receive back an output similar to the one at the top of the article.</p>
<p>If you want to see the full project code, see <a target="_blank" href="https://github.com/horosin/llm-on-aws-lambda">github</a>.</p>
<h1 id="heading-performance">Performance</h1>
<h2 id="heading-cost-and-speed">Cost and speed</h2>
<p>You may have noticed that I chose to run the model under the configuration:</p>
<ul>
<li><p>6 core multi-threading</p>
</li>
<li><p>10GB of RAM</p>
</li>
<li><p>5 minute timeout</p>
</li>
</ul>
<p>How was the performance and how much RAM was used?</p>
<p>Lambda never reserves compute for you and sometimes loads your program “cold”. When you make a request, anything you need to load (like a model) needs to happen at this moment, we call this a cold start.</p>
<p>For the prompt about five interview questions, the execution time after cold start was 17 seconds which is not bad at all.</p>
<p>After that, it consistently took 9 seconds to generate an output, tested in 4 subsequent runs. The average output length was 83 tokens. So, <strong>we achieved ~9.2 tokens/second</strong> when running hot.</p>
<p>As a comparison, community reported speeds for OpenAI models are: GPT-4 10t/s, GPT-4-turbo 48t/s, GPT-3-turbo 50-100t/s. So we’re matching a decent but not industry leading speed with little effort.</p>
<p>You could run this prompt 4444 times in a free tier. Above that, <strong>it would cost $1.2 per 1000 runs</strong>. The same 1000 runs with GPT-3.5 turbo would cost $0.2. This makes this endeavour not really cost effective, so low cost shouldn’t be your aim if you’re going to implement this in production.</p>
<h2 id="heading-optimisation">Optimisation</h2>
<p>Could we run the model cheaper? The model uses under 2600MB of memory consistently. We could lower the allocated resources, with a caveat that 6 core CPU is assigned only to Lambdas above ~8846MB.</p>
<p>I ran first test at 3500MB (3 CPU cores / threads) to see how it compares. I didn’t change the code to run only 3 threads which had a downgrading effect. In this case, the lambda always timed out (ran over 5 minutes, which meant the request was cancelled).</p>
<p>After changing the number of threads to the proper 3, the execution almost timed out at cold start, returning results after 4 minutes and 33 seconds. Subsequent 3 runs took 26-40 seconds averaging 3.15 tokens/second. 1000 paid runs would cost <strong>$1.45.</strong> So we got slower and paid more, not a good optimisation! With this in mind, it makes sense to pay for more memory, because AWS assigns CPU resources proportionally to the reserved RAM amount.</p>
<h1 id="heading-next-steps">Next steps</h1>
<p>As you can see, running an open-source language model is possible even in a regular environment like AWS Lambda. Cost and quality of output is not matching industry-leading services but can serve as a use-case specific solution. If anything, this was a great opportunity to learn about basic LLM ecosystem libraries and tools as well as strengthen you AWS knowledge.</p>
<p>Some possible next steps, if you want to keep exploring and bringing this project closer to production-ready:</p>
<ol>
<li><p>Try to run a different (bigger) model or a less compressed version of Microsoft Phi-2.</p>
</li>
<li><p>Create a CDK config to automatically deploy the model and create resources.</p>
</li>
<li><p>Create a GitHub Action to validate the project and run ./deploy.sh.</p>
</li>
<li><p>Try a different serverless platform to run the model.</p>
</li>
<li><p>Subscribe to my newsletter and follow me on social media.</p>
</li>
</ol>
<p>I may write about some of these topics, let me know in the comment if you’re interested in any of them! If you bump into any issues let me know as well.</p>
<p>What do you think about this way of deploying LLMs?</p>
<p>More from me:</p>
<ul>
<li><p><a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a></p>
</li>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[2023 review: lessons in engineering, writing, and business]]></title><description><![CDATA[This year has been important in so many ways. I’ve been publishing more content than ever. I had a part time personal assistant. Got access to many wonderful opportunities. I raised my net worth by 60%. I’ve built a great team and moved the product f...]]></description><link>https://horosin.com/2023-review-lessons-in-engineering-writing-and-business</link><guid isPermaLink="true">https://horosin.com/2023-review-lessons-in-engineering-writing-and-business</guid><category><![CDATA[review]]></category><category><![CDATA[goals]]></category><category><![CDATA[2023]]></category><category><![CDATA[blog]]></category><category><![CDATA[writing]]></category><category><![CDATA[money]]></category><category><![CDATA[content]]></category><category><![CDATA[projects]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Thu, 28 Dec 2023 13:13:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1703766906834/4f21f076-74a0-48ab-b101-4b4eada19182.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This year has been important in so many ways. I’ve been publishing more content than ever. I had a part time personal assistant. Got access to many wonderful opportunities. I raised my net worth by 60%. I’ve built a great team and moved the product forward at the cybersec SaaS I work at. I almost finished my master’s in management (thesis postponed to the next year). I designed and furnished my first (and hopefully last) apartment. Hell, I even started using separate products for hair and body. And there was a lot of personal change as well..</p>
<p>Join me in this short recap if you want to learn where a path like mine can lead you. I’ll share mostly blog and online presence related details with some professional (engineering), financial and private information sprinkled on top. Happy to tell more to those interested.</p>
<h3 id="heading-why-am-i-here">Why am I here?</h3>
<p>I like sharing knowledge, writing and helping people. I believe that publishing is a vital part of being a professional. Being here, rushing some content out even though it’s nor perfect, being vulnerable - that’s a great way to get to know more people and open one’s mind to new points of view.</p>
<p>I think one person - Austin Kleon - put this in words much better than me. Check out his short book and you’ll know everything.</p>
<blockquote>
<p><em>Show Your Work!</em> is about why generosity trumps genius. It's about getting <em>findable</em>, about using the network instead of wasting time "networking." It's not self-promotion, it's self-discovery--let others into your process, then let them steal from you.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703767142139/6ab32b5e-d815-4e13-a696-94ed91899dc8.png" alt class="image--center mx-auto" /></p>
<p>I had a blast putting effort into this blog and other projects this year. Some people enjoyed it as well, as seen in stats from various platforms.</p>
<h3 id="heading-stats">Stats</h3>
<p>Here’s the most interesting numbers i dug out!</p>
<p>My blog was visited a total of 43 thousand times:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703767234874/a0460fa9-270f-4809-8b7e-91b6de6d5031.png" alt class="image--center mx-auto" /></p>
<p>I also got additional 23k visits to my articles republished on Hackernoon.</p>
<p>My LinkedIn content generated 65k impressions, similar to views on the blog. I’ve been posting quite irregularly, as seen on the graph below. Near the end of the year, I’ve started listening to the numbers, resulting in more engagement than ever. Posts mentioning others and with my photos are noticeably more popular.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703767260943/b9fe3b1c-91ed-4057-92a4-879d3e3ceee6.png" alt class="image--center mx-auto" /></p>
<p>Other numbers:</p>
<ul>
<li><p>My newsletter got 50 subscribers (up from 24 in July).</p>
</li>
<li><p>On twitter my posts generated ~300k impressions.</p>
</li>
<li><p>I spoke at 8 conferences.</p>
</li>
<li><p>I was a guest in 5 podcast episodes.</p>
</li>
<li><p>I have published 12 blog articles.</p>
</li>
</ul>
<h3 id="heading-best-content">Best content</h3>
<p>If you missed some of my articles, here’s a chance to catch up. It’s clear that my most popular content was not opinions, summaries or anything related to my publishing online. Most viewed articles where software engineering tutorials. A lot of traffic came from google search, where I ranked quite high for some keywords. I also think I figured out a good way to create thumbnails.</p>
<p>11k views: <a target="_blank" href="https://horosin.com/extracting-pdf-and-generating-json-data-with-gpts-langchain-and-nodejs">Extracting and Generating JSON Data with GPTs, LangChain, and Node.js</a></p>
<p>9K views: <a target="_blank" href="https://horosin.com/effortlessly-create-powerpoint-presentations-with-chatgpt-and-marp">How I Effortlessly Create PowerPoint Presentations with ChatGPT and MARP (as a Software Engineer)</a></p>
<p>2K views: <a target="_blank" href="https://horosin.com/fine-tuning-openai-gpt-35-practical-example-with-python">Fine-tuning OpenAI GPT 3.5: Practical example with Python</a></p>
<p>Tip for me for the next year: write more engineering tutorials.</p>
<h3 id="heading-other-projects">Other projects</h3>
<ul>
<li><p><strong>Real estate investing / buying a new apartment:</strong> simple message from me here: don’t build a house or buy a new unfinished apartment if you’re not <strong><em>really</em></strong> into it. I had to learn interior design from the ground up. Get to know materials, design solutions, too many new technical terms. It <em>is</em> interesting, it can be fun, but it is a lot of work. Even though I’m very happy with the outcome, I think I could have spent my time better.</p>
</li>
<li><p><strong>Side projects / startups:</strong> kicked off 2, shipped none. Excuses? Master thesis and moving apartments. Picking them up again in 2024. One was MeetingFusion: <a target="_blank" href="https://meetingfusion.com/">https://meetingfusion.com</a>, the other is still unpublished. MF got several sign ups on the waiting list.</p>
</li>
<li><p><strong>My old AI Startup, Sentimatic</strong>: struggling, will post an update soon.</p>
</li>
<li><p><strong>Consulting</strong>: I’ve been able to provide some small-scale but well-paid software engineering services on the side.</p>
</li>
<li><p><strong>Funding for new AI project</strong>: applied for government funding, spent a lot of time. My assistant spent 175 hours on it! The application was of a master’s thesis size.</p>
</li>
</ul>
<h3 id="heading-opportunities">Opportunities</h3>
<p>Writing here gave me more opportunities than ever before. Apart from everything mentioned above, here are some highlights.</p>
<ul>
<li><p>I’ve been invited few times to run a workshop about topics I write about by several engineering companies. All my offers were rejected due to the price. I am okay with this, as I only considered it if I was able to provide absolute best quality of the workshop.</p>
</li>
<li><p>I’ve got more interesting job offers than ever. I did not proceed with any, as I’m committed to my current work at Netacea.</p>
</li>
<li><p>I’ve been asked to develop a Vue.JS course for a popular course platform. The contract signing is in progress so hopefully next year some people will be able to learn something new using the course.</p>
</li>
<li><p>I’ve been invited to many more conferences than I spoke at. Had to choose where to spend my time.</p>
</li>
</ul>
<h3 id="heading-money">Money</h3>
<p>My work around this blog and anything professional that is not related to Netacea (cybersec startup I’ve mentioned) only <em>cost</em> me money.</p>
<p>Apart from this money pit, I am quite okay at managing my finances, saving it and investing. I’m also fluent in tax codes, I don’t let money go to waste. I am following stock market to a limited extent as well. All of this pays dividends and helps me feel more secure in life. I recommend everyone to have basic financial fluency.</p>
<p>I won’t get into much detail and exact numbers, but I’ll share some stats to give you some insight into how I think about money and investing. I can share some guidelines for engineers in a separate article. Let me know.</p>
<p>My net worth (assets less liabilities) is allocated between cash, real estate and stocks. I am definitely real estate heavy but I have access to really good advice. Going forward, I’d prefer to have my net worth allocated in some business I run or contribute to in form of work or investment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703767822618/f3881aa8-57c0-408c-940f-d4f4dae47d43.png" alt class="image--center mx-auto" /></p>
<p>I am tracking my savings and investments as a whole by doing monthly net worth evaluation (it’s a quick exercise at this point). I’m doing it since April 2019 and have been able to maintain a good pace of growth, albeit with some ups and downs. Don’t get all excited, the starting point was fairly low!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703767854047/61081363-fe71-4777-b13b-b9e3bff22c74.png" alt class="image--center mx-auto" /></p>
<p>My short advice for friends:</p>
<ul>
<li><p>use retirement tax advantaged accounts</p>
</li>
<li><p>buy simplest stock market investments - index funds, S&amp;P500, whole US stock market</p>
</li>
<li><p>get a mortgage, it’s a low interest rate leveraged capital and overall properties keep up with inflation rates</p>
</li>
<li><p>have a safety net by some small cash (as in bank) pile to bail you out in emergencies</p>
</li>
</ul>
<h3 id="heading-plans">Plans</h3>
<p>My plans regarding this blog and my online presence are still in the making. I want to aim for something that will keep giving me satisfaction while being relevant and impactful. Here are the goals I am thinking about:</p>
<ol>
<li><p>Launch and grow a newsletter up to at least a 1000 subscribers.</p>
</li>
<li><p>Shipping one successful SaaS tool on my own. And then grow or sell it.</p>
</li>
<li><p>Grow my global (online) and local (at Netacea) influence as an engineering leader.</p>
</li>
<li><p>Write 1 blog post per week.</p>
</li>
<li><p>Publish video content and get a reasonable following.</p>
</li>
</ol>
<p>Regardless of details of these, you’ll likely see much more from me the next year! Stay tuned.</p>
<h3 id="heading-summary">Summary</h3>
<p>So yeah, a lot happened and I want it to continue to happen. This year has been another great period of my life which I’m truly grateful for. As for this blog, I want to keep writing and being a part of the global community.</p>
<p>After years of learning and working, I finally think like I have something to say. For me writing has always been this elusive thing that I never quite got to do. I stopped hiding behind perfectionism and started sharing. And many people found it useful, which just makes me happy. I keep learning but this time, I make some of my private notes public.</p>
<p>This year I’ve learned a lot, made mistakes, made new friends. The future looks exciting. See you there. Have the most wonderful year of your life.</p>
<p>More from me:</p>
<ul>
<li><p><a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a></p>
</li>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>@horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
<li><p>threads: <a target="_blank" href="https://twitter.com/horosin_"><strong>@horosin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Why AI Regulations Bother Me?]]></title><description><![CDATA[I was a guest speaker at the National Diplomacy Week at Jagiellonian University last month.
Turns out the diplomatic community is really engaged in the topic of AI regulations, development and ethics.
We discussed topics including public diplomacy, c...]]></description><link>https://horosin.com/why-ai-regulations-bother-me</link><guid isPermaLink="true">https://horosin.com/why-ai-regulations-bother-me</guid><category><![CDATA[AI]]></category><category><![CDATA[Regulations]]></category><category><![CDATA[Law]]></category><category><![CDATA[Startups]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[china]]></category><category><![CDATA[usa]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Wed, 27 Dec 2023 12:31:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742295740733/a96f093b-5aa4-4e14-98b4-a72b0bc403fc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was a guest speaker at the National Diplomacy Week at Jagiellonian University last month.</p>
<p>Turns out the diplomatic community is really engaged in the topic of AI regulations, development and ethics.</p>
<p>We discussed topics including public diplomacy, cybersecurity, and AI in business development. It was cool to share my tech thoughts with some international politics enthusiasts.</p>
<p>We also tried to cover both opportunities and challenges AI solutions may pose in the future. I prepared a small summary of the main issues we talked through. I’ve learned a lot about diplomatic world, dive deep into this with me.</p>
<h3 id="heading-what-is-ai-why-the-hype">What is AI? Why the hype?</h3>
<p>Why is there so much talk about AI now? The big moment happened in fall 2022 when ChatGPT came out. People started noticing AI more, but the tech had been around for a few years. What was missing was the right cloud infrastructure and servers, plus a good commercial product to sell.</p>
<p>Artificial Intelligence (AI) is commonly categorized into two types: narrow AI and general AI. Narrow AI, prevalent in today's market, excels in specific tasks such as language translation or image recognition but lacks the ability to operate beyond its programmed scope. On the other hand, general AI (or Artificial General Intelligence, AGI), a theoretical concept yet to be achieved, aspires to mimic human intelligence, demonstrating adaptability and learning across a wide array of domains. The development of AGI holds significant societal, ethical, and regulatory implications, prompting global discussions on how to manage its potential impact, including issues of privacy, safety, and ethical considerations. That said, until someone actually develops an AGI system, the topic is purely academic.</p>
<p>Currently in the AI field, two prominent types of neural networks are transformers and diffusers. Transformers excel in processing and understanding text, significantly enhancing how computers work with written information. Diffusers, meanwhile, creatively generate new content, like images or music, by progressively refining initial concepts. It is important to note that these technologies alone are unlikely to lead to the creation of AGI. Achieving AGI will likely require advancements beyond these current architectures.</p>
<h3 id="heading-what-life-areas-does-ai-revolutionise">What life areas does AI revolutionise?</h3>
<p>I think that AI-related technologies are already making a break through in our industry, similar to how electricity transformed every aspect of our lives. AI has the potential to make significant changes in our personal lives, businesses, healthcare, and even military operations.</p>
<p>However, the scale and pace of AI development raises concerns about its potential harm if not properly regulated. AI is already revolutionizing tasks that are meticulous and repetitive, performing them better than humans. Instead of being a threat, I think it can be seen as useful in improving efficiency and productivity.</p>
<p>A major change in our civilization is coming up, and we can't accurately predict the extent of its impact on our lives. As AI continues to advance, we are entering an era of transformation and I’m curious to see what it will bring.</p>
<p>Some say that current text AI output is mediocre at best. I think that unremarkable skill applied at scale and at a whim can change the world.</p>
<h3 id="heading-regulatory-models-and-ai-superpowers">R<strong>egulatory models and AI superpowers</strong></h3>
<p>Even though there is an already positive impact of AI tools on people's lives, they have been misused for among other misinformation and spam. There have been privacy concerns as well. This promoted certain regulators to take actions and start proposing targeted laws. What is more previous data protection laws still apply and influence AI models.</p>
<p>If we wanted to divide the way AI development is progressing and what attitude towards AI regulations is presented we can mention 3 models: American, Chinese and the EU one.</p>
<p><strong>American</strong></p>
<p>Currently, the USA stands as the leader in the AI market. The American perspective focuses on the prosperity of the tech industry through free-market policies for startups and businesses. There is also a preference for minimal government influence.</p>
<p>However, with the introduction of Chat GPT, there has been a shift in attitude. Now USA is trying to avoid consolidating too much power in a few big AI companies.</p>
<p>Despite those concerns, there is still a techno-optimistic attitude, assuming that companies will self-regulate (which might be a bit far-fetched). Just now new AI Bill of Rights including guidance on usage of AI has been unpacked, seen as the first step toward proper AI regulatory system.</p>
<p><strong>Chinese</strong></p>
<p>China's next-generation plan aims to position the country as a global AI leader, surpassing the USA. The primary goal is development. At first regulations were not present, allowing companies the freedom to pursue their initiatives with funding and access provided.</p>
<p>The concerns started with the rapid development of generative AI. Government got scared that AI may generate content outside the bounds of censorship.</p>
<p>There are now regulatory experiments to still support business development while making sure the advancements will align with the vision of the communist party.</p>
<p><strong>EU</strong></p>
<p>In the EU, we are now finalizing the AI Act - including the anthropocentric vision of AI with a focus on citizen laws and ethics. A key dilemma is how to strike a balance. How to keep startups innovative, ensuring technological independence, but also make sure citizens and their rights are safe.</p>
<p>Apart from mentioned models, there are of course also other countries/organizations that try to create independent laws within themselves.</p>
<p>Let’s take Israel - their Director General of Ministry of Defense announced plans to make Israel an AI superpower with a focus on military applications. They put a record-high budget for AI development with the focus on laws and research regarding defense needs.</p>
<p>Or recently France, Germany, and Italy agreeing on unified regulatory framework AI concerning companies in Europe. They don’t want sanctions on businesses to be immediate, rather put focus on incentives, and then later, if necessary, penalties for serious breaches.</p>
<p>Not long ago we also saw Italy was blocking ChatGPT being concerned with lack of proper processes regarding handling personal data processing and absent age restrictions.</p>
<p>There’s a multitude of solutions being discussed and implemented. Some try to regulate things that don’t exist yet (AGI). Some suggest putting restrictions that seem to benefit bigger players by raising the cost of operating AI systems.</p>
<p>I’m watching the space with moderate interest, as it can potentially affect me. That said, even if regulation will harm the innovation, it’s still more interesting to spend time on the actual tech, not the accompanying initiatives.</p>
<h3 id="heading-what-regulatory-issues-i-faced-when-running-my-own-ai-startup">What regulatory issues I faced when running my own AI startup?</h3>
<p>Machine learning model development depends on training data. When companies talk business, a lot of the time is spent discussing how to keep data safe and hand it over lawfully.</p>
<p>From what I've seen, China is tough when it comes to data safety. Everything has to get approved by the authorities, and the rules are strict. Because of this, many companies don't want to do business there (also political reasons).</p>
<p>The EU aims to keep its users' data within its own borders. Regulations say only the EU companies and EU citizens can process it. To put it in perspective, this is very similar to what China enforces.</p>
<p>Banks and financial institutions have the strictest rules. In many sales conversations I had for my AI projects, things came to halt when companies said they couldn't make data anonymous or didn't have good enough policies to share user data safely.</p>
<p>But this situation also creates a niche for products that work on premise. We can have an AI product that can run in client’s data center, and that opens up a lot of possibilities. This is the route I chose with Sentimatic.</p>
<h3 id="heading-poland-in-ai-development">Poland in AI development</h3>
<p>I bring in my local perspective and a mix of opinions I've heard during the debate.</p>
<p>When it comes to EU laws, Poland plays a role in the establishment process, but simply because we're a member state, so we are sort of ‘forced’ to do so. For example, the recent EU AI Act involvement was a bit disappointing - only 12 people from our country participated, half from private companies, and the rest of them directly connected with Ministry of Digitization.</p>
<p>Polish citizens and companies aren't showing a strong interest in AI regulations. Our current attitude is that we're more on the receiving end than actively contributing. Additionally, Poland does not have enough strong AI startups. We are in danger of falling behind other European countries.</p>
<p>On a brighter note, there are some AI startups like Eleven Labs which are doing well. We do a lot of AI research, but turning it into something commercial is a challenge. There's money available for AI projects in places like PARP (government agency), but the paperwork required is massive, so you have to consider if you are down to take on a lot of bureaucratic risk.</p>
<p>I think that even though so far we've (as in Polish engineers) mostly worked for Western European and US companies, we've learned a lot. With a bit of optimism, we can use that experience and money to figure out how to build our AI scene. So surely, I’m curious of what’s coming. And happy to contribute.</p>
<h3 id="heading-what-you-should-do-to-gain-the-most-out-of-ai-development">What you should do to gain the most out of AI development?</h3>
<p>With the rapid development of AI it is easy to miss out on different upcoming opportunities. Here is what I think is useful to think of to gain the most out of this technological breakthrough in the context of this debate:</p>
<p><strong>Securing Funding:</strong></p>
<ul>
<li><p>Explore private and government initiatives to fund your AI projects.</p>
</li>
<li><p>Be mindful of the consequences of getting funding. As a specialist, you can bootstrap a small product as well.</p>
</li>
</ul>
<p><strong>Being Aware:</strong></p>
<ul>
<li><p>Stay informed about new AI solutions, but avoid getting swept up in the hype.</p>
</li>
<li><p>Keep an eye on government regulations that could impact your rights and business.</p>
</li>
</ul>
<p><strong>Looking for Job Opportunities:</strong></p>
<ul>
<li><p>Look for emerging roles in the AI industry and find your niche.</p>
</li>
<li><p>Use AI tools to streamline tasks, as companies are still trying to integrate AI.</p>
</li>
</ul>
<p><strong>Thinking of Privacy:</strong></p>
<ul>
<li>Protect your data by applying privacy settings in AI chat applications. Pay for avoiding your data to be used for training.</li>
</ul>
<p><strong>Balancing Personalization and Automation:</strong></p>
<ul>
<li><p>Use AI for content creation but not in a direct way. Be inspired. Get criticism. Always add value.</p>
</li>
<li><p>Always check the quality and relevance of AI-generated content.</p>
</li>
<li><p>Approach AI tools with caution, they make mistakes with striking confidence.</p>
</li>
</ul>
<h3 id="heading-conclusions">Conclusions</h3>
<p>AI development is unavoidable. It is better to prepare ourselves for it rather than avoid the topic or threaten the society with a possible danger of new technologies. This technological revolution can be different than others, but fear mongers have been among us since the beginning of humanity. Sure, regulations are needed and we need to protect societies from AI-driven collapse but decisions should be evidence-based, not fear-driven.</p>
<p>From my personal perspective I hope to see Poland becoming more active in establishing AI laws and I'm closely watching any new regulations coming out which might affect me personally and professionally.</p>
<p>I think the best thing we can do right now is educate ourselves and try to gain the most out of the whole thing. It is silly to let some of the opportunities pass us by and not use the whole potential (generative) AI has to offer.</p>
<p>Your thoughts? More worried or excited? Regulation: BS or necessity?</p>
<ul>
<li><p><a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a></p>
</li>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_">@horosin_</a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
<li><p>threads: <a target="_blank" href="https://www.threads.net/@horosin">@horosin</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Taming the smart home monster: a quest for peaceful sleep]]></title><description><![CDATA[On Saturday, I slayed the dragon. The digital monster lurked in my electrical relay box, and overcoming it gave me a primal sense of domination. It also left me pondering the cumbersome nature of technology.
All week, I had suspected something was wr...]]></description><link>https://horosin.com/taming-the-smart-home-monster-for-peaceful-sleep</link><guid isPermaLink="true">https://horosin.com/taming-the-smart-home-monster-for-peaceful-sleep</guid><category><![CDATA[smart home]]></category><category><![CDATA[essay ]]></category><category><![CDATA[technology]]></category><category><![CDATA[Problem Solving]]></category><category><![CDATA[QA]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Tue, 19 Dec 2023 07:00:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1702835353946/e3b84a02-3eda-4862-9e60-234edd117506.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On Saturday, I slayed the dragon. The digital monster lurked in my electrical relay box, and overcoming it gave me a primal sense of domination. It also left me pondering the cumbersome nature of technology.</p>
<p>All week, I had suspected something was wrong with my apartment. The smart switch controlling the water supply seemed dimmed as if it was off, even though it was allowing the water to flow.</p>
<p>Simultaneously, I started hearing more about ghosts. My coworker had signed up for a New Year's Eve party at an abandoned psychiatric hospital involving Ouija boards. Inspired by this, my girlfriend and I spent hours discussing the existence of unseen forces around us.</p>
<p>Imagine our despair when, on Friday night, our electrical box began making cracking noises, and our lights started flickering on and off. The water supply was being cut, disrupting the washing machine and dishwasher. I suspected the smart home relay, installed by the building developer, was the culprit. Exhausted and clueless, I couldn't fix it. There was no "off" switch, so the disturbances continued sporadically throughout the night.</p>
<p>We woke up tired, angry, and slightly creeped out.</p>
<p>The next day, I tried to resolve the issue. I hit the reset button repeatedly and looked for a "bypass" switch, but nothing worked. The problem persisted, albeit less frequent. The relay had never been configured because as it’s a new apartment I didn't have a Wi-Fi router yet, and frankly, I hadn't been interested in it before. I attempted to configure several times it using my girlfriend's phone as a router to follow the official guide, but to no avail.</p>
<p>In the end, I decided to go primal. My girlfriend and I scoured online manuals for different parts of the electrical box. We discovered a pin that could lock the digital switch for electricity in the always-on position. There wasn't one for the water, but fortunately, the smart home hub had mixed up the off/on states...</p>
<p>In our final test, I turned the smart home relay switch off. Previously, this action would cut off everything, but now it had no effect. The monster was defeated.</p>
<p>This experience got me thinking. If there's a lesson to be learned, it's the importance of having an "off" switch on every device.</p>
<p>I hope this ordeal isn't a prelude to a future dominated by AI overlords. Imagine if my building developer were a company like Microsoft/OpenAI, the smart home hub a deeply integrated AI model in our lives, and I, an oblivious user. A sleepless night could symbolize a future where we have no control over what's happening.</p>
<p>Perhaps I'm taking this too far, but I need sleep. Take care, everyone, and may your smart devices spare your sleep. Were you ever attacked like this?</p>
<ul>
<li><p><a target="_blank" href="https://horosin.com/newsletter"><strong>Newsletter</strong></a></p>
</li>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_"><strong>@horosin_</strong></a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/"><strong>linkedin</strong></a></p>
</li>
<li><p>threads: <a target="_blank" href="https://www.threads.net/@horosin"><strong>@horosin</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Tech Talks: Insider Tips for Making Your Mark at Conferences]]></title><description><![CDATA[Introduction
I first thought about speaking at tech conferences as a way to get a free ticket – let's face it, they're not cheap. But what started as a practical move turned out to be a journey of incredible learning and networking. Over time, as I s...]]></description><link>https://horosin.com/tech-talks-insider-tips-for-making-your-mark-at-conferences</link><guid isPermaLink="true">https://horosin.com/tech-talks-insider-tips-for-making-your-mark-at-conferences</guid><category><![CDATA[conference]]></category><category><![CDATA[AI]]></category><category><![CDATA[tips]]></category><category><![CDATA[tech ]]></category><category><![CDATA[networking]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Fri, 15 Dec 2023 16:48:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742296101112/0dcb8a0c-82c1-4cc3-a5f8-8950411bd045.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction">Introduction</h3>
<p>I first thought about speaking at tech conferences as a way to get a free ticket – let's face it, they're not cheap. But what started as a practical move turned out to be a journey of incredible learning and networking. Over time, as I spoke at more than 19 events, I realized these conferences were more than just stages to share ideas; they were opportunities to connect with CEOs, top experts, authors, and so many inspiring people.</p>
<p>Back then, my experience included only teaching JavaScript to my colleagues at work and presenting startup ideas at local hackathons. These experiences were my stepping stones into the world of tech conferences. Today, with a wealth of presentations behind me, I want to share why stepping onto that stage is so rewarding. It's not just about discussing cutting-edge topics like programming techniques, AI advancements, boosting productivity, or nurturing startup growth. It's about the interactions, the shared knowledge, and the diverse perspectives you encounter. In this guide, I'll take you through how to kickstart your journey in conference speaking and make the most of it.</p>
<h3 id="heading-advantages-of-becoming-a-conference-speaker">Advantages of becoming a conference speaker</h3>
<ul>
<li><p><strong>TOP ONE: opportunity to meet new people from your field</strong>; share what you're working on and get inspiration for new projects. Listen to other speakers who are way better than you in engineering. I’ve met notable figures in engineering through conferences and had a chance to ask them questions about how they work or run their companies. I know I wouldn’t have a chance of meeting them anywhere else.</p>
</li>
<li><p><strong>Getting valuable credentials</strong>; speaking at conferences provides you an opportunity to present your expertise, skills, and background, potentially strengthening your current job position and boosting credibility with both existing and potential clients. Your presentation typically undergoes a selection process conducted by an experienced panel of professionals on the program committee, so you get the chance to see if there's any audience for your knowledge. From my experience, when applying for jobs, interviewers really appreciate some background connected with public speaking. And finally, once you become more recognizable, further speaking opportunities are unlocked and organizers start to reach out to you themselves.</p>
</li>
<li><p><strong>Collaborating</strong>; participating in a conference is a good way to meet and run your ideas through experts. Or expand your professional network if you're into LinkedIn lingo. You can (and I recommend doing so) engage with other speakers, event coordinators, and sponsors. This may lead to potential collaborations and career opportunities. Sharing your contact information for future communication is a must. After some of the conferences I got offers to run thematic courses in some companies, so once the market sees you as an expert in the field, you can really expand your brand.</p>
</li>
<li><p><strong>Reinforcing your skills</strong>; when preparing your presentation, you have the chance to structure your know-how in an organized and effective way. Additionally, while presenting your talk, you practice the delivery of your hard-earned knowledge and you get a chance to be challenged by participants’ questions related to your topic. Preparing presentations helped me look critically at my work and improve how I do things.</p>
</li>
<li><p><strong>Expanding your online presence</strong>; you can distribute your presentation slides to the audience through social media, post your talk video on platforms like YouTube, or interact with individuals involved in the event through LinkedIn and other platforms. I also noticed that even though I never promoted a product, a book, or anything in particular, there were always some new followers or contacts on all my platforms.</p>
</li>
</ul>
<h3 id="heading-how-to-find-conferences-to-participate-in">How to find conferences to participate in?</h3>
<p>To find the top events to speak at, I do a brief scheduled conference research every two weeks. I search for IT conferences that match my area of expertise and check if they're accepting talk proposals.</p>
<p>For international conferences, having a speaker profile on a platform like <a target="_blank" href="https://sessionize.com/">https://sessionize.com/</a> is the best way to discover new, interesting events.</p>
<p>I also recommend checking out the following websites:</p>
<ul>
<li><p><a target="_blank" href="https://dev.events/tech">https://dev.events/tech</a></p>
</li>
<li><p><a target="_blank" href="https://www.eventyco.com/events/conferences/tech">https://www.eventyco.com/events/conferences/tech</a></p>
</li>
<li><p><a target="_blank" href="https://confs.tech/">https://confs.tech/</a></p>
</li>
</ul>
<p>I am based in Poland, Europe, so I also look at local events and my go-to place is <a target="_blank" href="https://crossweb.pl/wydarzenia/it/">Crossweb</a>. Every country will likely have an event aggregator like that.</p>
<p>At this point I also get directly invited by past event’ organizers I spoke at or by conference managers who saw me on other event's websites.</p>
<p>Once I find a conference I like, I either apply right away (if I have a suitable talk ready or in the pipeline) or I set a deadline for myself in Notion (note-taking/project management app) to create a proposal.</p>
<p>To keep track of all this, I've made a special section in notes for conference applications. It helps me see which events I've applied to, whether the organizers accepted my proposal (or not), and the topic of the talk I submitted. When you take part in conferences regularly, it's easy to get lost in all application forms and emails, so this system keeps everything organized and makes sure I don't forget or miss out on opportunities.</p>
<h3 id="heading-what-you-can-prepare-prior-to-applying">What you can prepare prior to applying?</h3>
<p>To make applying for conferences easier and faster, I organize important information in one place. This way, I can quickly access it whenever I need it. For each conference application, you usually need to provide:</p>
<ul>
<li><p><strong>Basic data</strong>; your email, phone number, Twitter and LinkedIn handles, a profile picture, your current job title, and a short bio. I have all this information saved in a dedicated section in Notion, and just copy paste it when I apply for a conference.</p>
</li>
<li><p><strong>The bio;</strong> you provide should include details about your current job, any extra projects you're involved in, and fields of expertise you feel confident about and can present at the conferences. As an example, here's my latest bio for reference:</p>
<blockquote>
<p>Software Engineer Manager at Netacea, an AI cybersecurity SaaS startup based in the UK. Creator of <a target="_blank" href="http://sentimatic.io/">sentimatic.io</a> - a product for analyzing emotions and content of customer conversations based on AI technology. I run a blog on <a target="_blank" href="https://horosin.com/">horosin.com</a>, where I write about topics related to programming, AI products, and startups. I am professionally and personally interested in biotechnology, medicine, space technologies, filmmaking, and literature. I enjoy sharing knowledge in the field of programming and AI-based solutions while gaining inspiration from discussions with new people I meet.</p>
</blockquote>
</li>
<li><p><strong>Overview of the talk you want to present</strong>; I keep track of all my talks and ideas in Notion, so if I'm short of inspiration, I simply update it and add necessary changes, instead of starting from scratch every time.</p>
</li>
<li><p><strong>Past conference experience</strong>; I keep a record of my public appearances on my blog at <a target="_blank" href="https://horosin.com/conference">https://horosin.com/conference</a>, and I always link it as a part of my application. It's important to have a portfolio that shows your experience, as it makes you a more reliable candidate. Here's a note I attach:</p>
<blockquote>
<p>I have spoken at several conferences so far. I like talking about programming, startups, innovative products and advancements in the AI field. Here is a link to all my conference experiences gathered:</p>
<p><a target="_blank" href="https://horosin.com/conference">https://horosin.com/conference</a></p>
<p>I have also been on the program board of a 2021 4developers Live conference. I was responsible for the selection of talks for the Python track.</p>
</blockquote>
</li>
</ul>
<h3 id="heading-how-to-prepare-and-present-a-talk">How to prepare and present a talk?</h3>
<p>Successful conference participation depends mostly on what and how you present. Take the time to establish your personal strengths, define what you’re good at, and narrow down your field of expertise. Find something in your work that is valuable to share with your audience and make sure the content is genuinely useful to your listeners.</p>
<p>Remember that talking about your mistakes can be as beneficial as celebrating your successes. Consider your failures as lessons that can prevent others from making similar mistakes. I've done this numerous times, for example, while discussing the five startups I failed at. I talked about this at three different conferences, sharing my business journey and what I learnt from it <a target="_blank" href="https://horosin.com/i-launched-6-startups-in-8-months-and-5-of-them-failed">https://horosin.com/i-launched-6-startups-in-8-months-and-5-of-them-failed</a>. It is to this day one of my most popular talks and blog articles.</p>
<p>While preparing the slides, I follow a process:</p>
<ol>
<li><p>Craft an outline for your speech.</p>
</li>
<li><p>Draft your initial version.</p>
</li>
<li><p>Refine your draft, brainstorm fresh ways to share your ideas, and then settle on the final version.</p>
</li>
<li><p>Create slides for your presentation.</p>
</li>
<li><p>Improve your presentation with engaging elements such as data, graphics, personal anecdotes, or connections to certain individuals to make it stand out.</p>
</li>
</ol>
<p>Remember to practice your speech, watch your timing, and make adjustments as necessary to fit the given time frame.</p>
<p>During your presentation, don’t forget about a few key tips while still trying to stay natural. It's not necessary for everything to go according to the script, however, small adjustments can improve the final result:</p>
<ul>
<li><p><strong>Body language</strong>: Try to pay attention to how you move. Use your body to show your ideas and to better connect with your audience. Move around the stage, keep eye contact with various listeners and use hand gestures or facial expressions to underline some main points.</p>
</li>
<li><p><strong>Focus on your personal expertise</strong>: Your main goal should be to provide value to the audience, not to sell products. If you give people what they came for, they'll want more from you. You just need to tell them how to find it. I put my social media information on my presentation’s slides and sometimes at the end of the talk, depending on the conference, share new product ideas connected with the speech.</p>
</li>
<li><p><strong>Get the audience involved</strong>: Keep the conversation going. Plan a time for questions and answers with the listeners. This part can help you look at your material from different perspectives. You can get authentic feedback on your work.</p>
</li>
<li><p><strong>Ask for feedback</strong>: Don't forget to ask for thoughts and suggestions. It can help you get better for your next talk and develop your portfolio. At some conferences, feedback is collected through forms and later visible in your speaker’s profile. Otherwise, simply collect it by directly sharing feedback forms.</p>
</li>
</ul>
<p>While presenting, never downplay yourself. If you were selected to speak, then your expertise is enough. You also put your name and time on the line. We're all busy and trying the best we can. If you "had no time to make the slides pretty", don't say that, it's annoying to people that give you their attention. Just pretend your lack of design polish is just your cute nerdiness. Much better effect. Or better just have a look at my article about MARP so your slides are never ugly again and take 10x less effort: <a target="_blank" href="https://horosin.com/effortlessly-create-powerpoint-presentations-with-chatgpt-and-marp">https://horosin.com/effortlessly-create-powerpoint-presentations-with-chatgpt-and-marp</a></p>
<h3 id="heading-how-to-get-the-most-out-of-conferences-before-and-after">How to get the most out of conferences (before and after)?</h3>
<p>OK, you got an acceptance email and now what? If you want to get the most out of the conference, it's important to plan ahead. Here's a simple guide on what to do after being selected as a speaker. It's a process I loosely follow. If you do any of the steps, you're already ahead.</p>
<p>All that said, the key is a great submission and a value-packed presentation. Everything below is pointless without those.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702657926522/9cdd30af-099e-4f8d-a341-5aa90089b77b.png" alt class="image--center mx-auto" /></p>
<ul>
<li><strong>3 weeks before the conference</strong></li>
</ul>
<p>Start researching the people involved, like the event organizers, other speakers, and sponsors. Identify those you'd like to connect with and reach out to them online. Contact them to schedule a meeting or chat before the event.</p>
<ul>
<li><strong>2.5 weeks before the conference</strong></li>
</ul>
<p>Share any promotional graphics the organizers provide, which usually feature your photo, topic, track, and maybe a discount code. Publish content on your social media and tag people involved. You can also create your own announcement post if needed.</p>
<ul>
<li><strong>2 weeks before the conference</strong></li>
</ul>
<p>Consider talking to the organizers about creating a post for their social media. This could be a video inviting others to the conference or a short post about your talk.</p>
<ul>
<li><strong>1.5 weeks before the conference</strong></li>
</ul>
<p>Create a teaser post with a summary and key points from your presentation to generate more interest. This is a good way to share some knowledge online in preparation for the event. Provide real value, don't advertise.</p>
<ul>
<li><strong>1 week before the conference</strong></li>
</ul>
<p>Make sure your presentation is well-prepared. Upload your slides online so you can easily share them with participants after your talk. Also, direct people to your social media for any updates.</p>
<p>On the day of the conference, connect with as many people as you can, especially those you researched earlier. Attend other talks, take notes, and share your thoughts about them on social media. Take photos during other speakers' presentations and share them later to provide fresh content for their social media. Engage with your audience after your presentation, perhaps over a coffee. Respond to social media posts related to the conference and share your behind-the-scenes impressions.</p>
<p>Now that the conference is over, expanding your network and benefiting from participating doesn't really stop. Here are some things that I do afterwards.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702658003267/6ecf14c4-a6d2-45a4-b1ab-1992e9c65675.png" alt class="image--center mx-auto" /></p>
<ul>
<li><strong>1 Day After the Conference: Share Your Thoughts</strong></li>
</ul>
<p>After the event, post your impressions on social media. Share what you learned and your general thoughts on the conference. Tag people to start discussions.</p>
<p>Here is an example of my LinkedIn post, which I published after speaking at Devoxx Poland:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.linkedin.com/feed/update/urn:li:activity:7070351872594690048/">https://www.linkedin.com/feed/update/urn:li:activity:7070351872594690048/</a></div>
<p> </p>
<p>As you see it was quite successful - got 52 likes with only 500 followers I had at a time.</p>
<ul>
<li><strong>2 Days After the Conference: Update Your Portfolio</strong></li>
</ul>
<p>Add the conference experience to your speaker portfolio. This could be a website, LinkedIn article, your resume, or wherever you gather your accomplishments. Keep a list of the talks you've given, including dates, titles, and event names. It's a good place to share notes, slides, and related materials from your talks.</p>
<ul>
<li><strong>5 Days After the Conference: Share Your Presentation Materials</strong></li>
</ul>
<p>Share the slides you used in your presentation. You can post the original materials or create a quick guide on the topic you presented. Sharing knowledge is one of the key points of being a conference speaker, so I try to come forward to my listeners with as many useful sources as possible.</p>
<ul>
<li><strong>10 days After the Conference: Gather Feedback</strong></li>
</ul>
<p>Collect feedback from the audience and organizers. You can use social media polls, direct messages, or check for opinions on your speaker profile. Reading feedback helps you find areas for improvement and can be useful for future conference applications or negotiations regarding payment.</p>
<ul>
<li><strong>2 Weeks After the Conference: Reuse Your Content</strong></li>
</ul>
<p>You can reuse your slides by turning them into articles, posts or applying feedback and refining them for another conference. I post articles related to my talks on my blog so they're always available for reference.</p>
<h3 id="heading-money-how-much-does-it-cost-can-you-get-paid">Money. How much does it cost? Can you get paid?</h3>
<p>Usually, the conference will cover all your travel expenses and admission. You will also get invited to speaker dinners and extra networking opportunities. Some events may ask if you have an option to pay for travel yourself or ask your employer for coverage, it will raise your odds of being admitted.</p>
<p>My experience with travelling for conferences is mixed. Some booked very low quality, scary even, accommodation. These events are often sponsored by big companies and generate a lot of profit. Saving on speakers feels unacceptable to me, as they usually contribute to the event's success for free.</p>
<p>I lived abroad for some time and a few conferences paid for my flights back home in order to speak.</p>
<p>Getting paid? If you have a lot of experience, are an established author, and work in a great company - yeah, possible. Usually, a good conference will pay a few speakers, especially the keynote ones. Many, even very experienced ones, will just get their expenses covered but it's a welcome enough benefit for them and should be for you. Speaking is often a community contribution, a way of giving back to the community.</p>
<p>Do you have to take time off to speak at a conference? My arrangement is usually to present as a part of my job. The company I work with always gets mentioned, I try to generate sales leads and do a bit of hiring at every event I attend. I also provide photos and short written reports for the marketing department so they can prepare some content for the company's social media channels. Don’t be afraid to talk to your manager to work out the details. You being at a conference as a speaker is a pure benefit for the company - you get free training and generate free publicity. Make sure it is well understood.</p>
<p>I’m also spending a bit of my own money on the whole process. I work with a part-time personal assistant who helps me, among others, with managing talk submissions and refining slides from the design point of view.</p>
<h3 id="heading-conclusions">Conclusions</h3>
<p>Summing up, I recommend taking part in conferences as a speaker. You get the opportunity to expand your network, organize knowledge and practice public speaking.</p>
<p>Making conference research a part of your regular tasks will result in more event opportunities, I did so and the number of conferences I took part in 2023 is higher than ever before.</p>
<p>If you prepare for your speech in a smart way you can reuse your content.</p>
<p>Following a few simple steps before and after the conference will help you make the most out of your presentation and will result in the development of your personal brand.</p>
<p>For the next year, my plan would be to speak mostly at international events so I can mix it with some travelling. I'm also aiming to get some number of paid gigs, as I have plenty of experience to do a solid agenda-boosting keynote.</p>
<p>Do you plan to speak at any tech conference? Any stories or thoughts?</p>
<p>Reach out to me if you need any help and follow my newsletter and social media accounts for more stories from the software engineering trenches.</p>
<ul>
<li><p><a target="_blank" href="https://horosin.com/newsletter">Newsletter</a></p>
</li>
<li><p>x/twitter: <a target="_blank" href="https://twitter.com/horosin_">@horosin_</a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/horosin/">linkedin</a></p>
</li>
<li><p>threads: <a target="_blank" href="https://www.threads.net/@horosin">@horosin</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Just do you: (engineering) career insight]]></title><description><![CDATA[This is a post about positioning yourself in your career. Coming from a guy who loved all his jobs. Maybe I did something right. I was always able to build trust and get enough independence to move fast and work on my terms.
We often try to live up t...]]></description><link>https://horosin.com/just-do-you-engineering-career-insight</link><guid isPermaLink="true">https://horosin.com/just-do-you-engineering-career-insight</guid><category><![CDATA[Career]]></category><category><![CDATA[software development]]></category><category><![CDATA[personal development]]></category><category><![CDATA[motivation]]></category><category><![CDATA[thoughts]]></category><dc:creator><![CDATA[Karol Horosin]]></dc:creator><pubDate>Mon, 23 Oct 2023 08:09:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698047979540/fc3e6fe5-d8d5-4bc4-8235-292bb01faa19.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a post about positioning yourself in your career. Coming from a guy who loved all his jobs. Maybe I did something right. I was always able to build trust and get enough independence to move fast and work on my terms.</p>
<p>We often try to live up to our job position titles. From my experience, you can find yourself in two difficult situations. In one scenario, you might feel underqualified for your position, constantly chasing perfection without ever feeling satisfied. In contrast, you may feel constrained by your job title, which keeps you from exploring other areas of interest.</p>
<p>Here’s my advice in one sentence: do “you”, create a unique mix of skills amplifying your talents, and become irreplaceable by raising your value regardless of what the current title says. Exceed expectations. But make sure you're looking at the right ones.</p>
<p>Let me tell you a story. I trained Judo (a Japanese martial art focused on using an opponent’s strength against them) for years. When I competed in tournaments, I was always wearing the most beginner belt colours. My coach never provided us with many options to get certified and pass exams. I felt I was worse than my competitors. I told him I wanted to pass the blue and brown belts (just one step before black) exams.</p>
<p>He told me I could wear them, that I could wear whatever I want really and he’ll have my back. He said that I would have easily passed the exams years ago and it’s not important what colour I wear but how I fight. He asked me how many times I beat brown and black belts. The answer was plenty enough.</p>
<p>I think about this conversation every time I feel like my job title doesn’t describe what I do (or want to do).</p>
<p>I’ve been influencing how software is built in companies while working at every level of my career, even as a junior software engineer. I trained people, proposed interview questions, built templates, and provided advice. The position titles came afterwards. They always came. Perhaps I was lucky to work with leaders that noticed. Now I try to be the one.</p>
<p>“Don’t give any of yourself away” is the quote I remember from excellent Austin Kleon’s books. Don’t drop your hobbies. Nurture small things you like. Apart from coding, do you like cooking, designing, painting, parenting, running, whatever else? Keep it as a part of you. You don’t need laser focus, you’re much better off being an interesting diverse individual that created their own path.</p>
<p>You still need to be great at something, don’t get me wrong. While there are already great engineers, artists, and data analysts, none of them have your unique perspective… Unless you’ve abandoned your uniqueness.</p>
<p>Don’t feel you’ve ever had the ingredients for your perfect mix? Go out, experience new things. Contact distant friends that you see doing cool stuff on Instagram.</p>
<p>When I was younger I felt terrible not feeling an expert at one thing. I felt I’m wasting time filmmaking or writing short stories instead of coding. I envied people who seemed so focused and were high achievers. But most of them created their own paths. And their perceived focus was often just a result of good branding.</p>
<p>Look at Tim Cook. At first, everyone compared him to Steve Jobs. Now he gets his own biographies. His Apple is different, yet it still flourishes. He didn’t imitate Jobs, he became his own flavour of Apple CEO. Now many want to be Tim, less erratic, better at working with people, persistent over time.</p>
<p>My writing skills and a gut for good image composition prove useful every day. I can edit promo videos. I can write okay product copy. I can contribute to UX designs. My hobbies and interests feed my work indirectly and are to my advantage.</p>
<p>In my recent podcast appearance, I discussed the cybersecurity challenges associated with handling genetic testing data. I realize that not many individuals have experience in both biotech and cybersecurity, which makes this conversation somewhat unique.</p>
<p>You do you. Embrace your uniqueness and respect the uniqueness of others as well. All parts of you are valuable. Find places where you can utilise this potential. Don’t give any of yourself away.</p>
]]></content:encoded></item></channel></rss>