<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Colin Armstrong</title><description>Writing about startups, product, and engineering.</description><link>https://armstr.ng</link><language>en-us</language><atom:link href="https://armstr.ng/feed.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Mon, 05 Oct 2026 22:21:07 GMT</lastBuildDate><item><title>Worth building</title><link>https://armstr.ng/writing/worth-building</link><guid isPermaLink="true">https://armstr.ng/writing/worth-building</guid><description>What changes when a tool doesn&apos;t have to pay for itself</description><pubDate>Mon, 05 Oct 2026 22:21:07 GMT</pubDate><content:encoded>&lt;p&gt;Years ago, if an app annoyed me, I shrugged and dealt with it. I could sometimes have fixed it or built my own, but the annoyance rarely justified the effort. With LLMs that calculation has changed, and this past weekend it changed for a domain lookup tool.&lt;/p&gt;&lt;p&gt;Occasionally I need to check DNS records or registration info for a domain. At my computer I use &lt;code&gt;dig&lt;/code&gt; or &lt;code&gt;whois&lt;/code&gt;. Otherwise I Google &quot;domain lookup tool,&quot; click on one, and paste in the domain. Most of what comes up is slow, buried in upsells, and unpleasant to use for something that should be simple.&lt;/p&gt;&lt;figure float=&quot;none&quot; class=&quot;paragraph-figure paragraph-figure-none&quot;&gt;&lt;img src=&quot;https://storage.googleapis.com/papyrus_images/8ad414aee13f7e50a58d87907fb972a367dfb4d3a5bb659be63950a4b716074e.png&quot; blurdataurl=&quot;data:image/png;base64,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&quot; nextheight=&quot;2386&quot; nextwidth=&quot;2396&quot; class=&quot;paragraph-editor-image&quot;&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;They aren&apos;t badly made -- they&apos;re well made for a different job. A domain lookup answers a question nobody pays to ask, so many of the free tools that show up in search are the ones a sales funnel justifies: the lookup brings in someone who might buy a domain or hosting, and the page is built to rank for &quot;domain lookup tool&quot; and to sell. The tools you find are the ones built to be found. Being good at the lookup was never what paid for them.&lt;/p&gt;&lt;p&gt;I wanted something I&apos;d actually enjoy using, so I opened Claude Code and built &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; class=&quot;dont-break-out&quot; href=&quot;https://heade.rs&quot;&gt;heade.rs&lt;/a&gt;. In three prompts and about an hour I had a first working version deployed to Cloudflare: DNS and registration records, live latency, email configuration, HTTPS details. The rest of the day went into making it feel good: animations, graphics, an interface I like looking at. It costs less than a cup of coffee a month to run. It doesn&apos;t have to sell anything, so it can just be good.&lt;/p&gt;&lt;figure float=&quot;none&quot; class=&quot;paragraph-figure paragraph-figure-none&quot;&gt;&lt;img src=&quot;https://storage.googleapis.com/papyrus_images/713aeca5e5a7aac4469922bb7b483fa3e8e23a66211816145d55f608c5f660dc.png&quot; blurdataurl=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAgCAIAAABo/kRRAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEH0lEQVR4nI2WPWgbSRTHp1BtLoWrVK5UuXOnSp0qddup227K7dRNc2FjsIeEg+EOjonJJQOBTK4abBxNjDmPv+7GSYjWkPhG5nKe+MBebIi1OfBFx+7Dy8aSQ36IZUd6+2b03v+9twghND09TSl1zhFCpJSj0Sj7Brz3zjmMcZIkYRhKKZFSSkqplEqSRClljHHfRlJgjEmSRGttrUVCCClllmXOuTRNvffW2iRJ4ArYgmQS4BSu6BozMzNZlpU/lw9UlzeB4jiOoqjdbgdB0Gq14jjmnMcFnHPGmJSyXFJKGWM3+mKMgSmltNvtgutuQRRFpABjDMsoijDGSqmJx0SwuVKKc66UEkKogsePH/V6PSHEw18erj5fXXqwZIyBwN0UOxSGYavVklISQpRSjDEhBOf87t147YUmhMTz86vPVwkhWmt4pvRYdZ3n0TmXZVla4K9wzpVXiD0YuBvEAe6QtRZjzBgzxmRZNhqD3f/Be//9nTthGBpjxg2SJAGpI2PM7du3a7VavV5P03TcdDQapWnabrcRQkEQQFVMtEFwQlPgx/j306fzszPnHNhYa8dtvPfw93NfSilrLVRZWuHk9GTxx3uPnop/jo+ttf1+fzi8GA4vPn++hA8sP1583Hv1x5t+H2mt6/V6s9lsNBoY4+qxL/+7XFnrbexunp+dt1qthUXaW99+8uvKxs7L318lGzsve+vb4tmyeLb84MnTg4OD/FyU0jiOMcYTQzsajT58OI7jeGGRzi/c29zZTd66vdf7e6/3k7fuYjjcMNs//bz099FRfi5rLWjnK0Xn3MD7oz+vOBy4w4Erl0dH73NN3PruFlR1p9MBNYGygFKKb/r96mbODYr7/apc0eH7vza2NpVS7w7eZVkmpaSUCiEYY5RSyNHhwK2vaagwY4zWGmyEEGWi8jz2+/3139ZXlldAXOALijSOYxDwyenJzu4W1GmapsYYxhjnHJow4L1HhwO39iIPGXwFhUkIYVd4709OT8z2lhACzKpdCEIB9ZfXo7W2asc5J4R478E7iM5aK6Uszx6GIS/odruEEGNMrnvnHDR7eFIpRSmFnSmlhJDyyF8n91VWAKC1hlEipdQ6j7e1tpK+nC+1kgMCQLOzswihZrNZDpVyHACQ8up9Fcgm51xrjTDGtVoNAlT1UnXnnINx5caA5MKfyHtOGayJwFiF3p+N8UW8ut0uQiiKojAMgyBot9vNZjMMw7m5OYxxmqbgC7bMxkjTlDEGHTyftVNTU51Op9FohAVBEHSuiKII8q21LlVy7dRRFMHEyPMIpaO1nvjSAIEvG1w2RpIkIM+853DOx0MGsbDWgvRBKxO7M7yhGGPy2QEzceKeUkrGWFUNpbJKXyCuXF/lvBvPd7XnyALOOQjqmj5Aq/8Dj/JLQmcmP1QAAAAASUVORK5CYII=&quot; nextheight=&quot;2484&quot; nextwidth=&quot;1944&quot; class=&quot;paragraph-editor-image&quot;&gt;&lt;figcaption&gt;The &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out&quot; href=&quot;http://heade.rs&quot;&gt;heade.rs&lt;/a&gt; overview page&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure float=&quot;none&quot; class=&quot;paragraph-figure paragraph-figure-none&quot;&gt;&lt;img src=&quot;https://storage.googleapis.com/papyrus_images/b52f0edf4a37eb72cbf5ac040d9a6dd432b054b70ac1c8d45f931481f6fc703e.png&quot; blurdataurl=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAgCAIAAACDyf9SAAAACXBIWXMAABYlAAAWJQFJUiTwAAAFPUlEQVR4nKVW7U8URxzeD37pX2AamzRpPxr7yaQf/NKkMdGaNjH2KjWkNhJTuaakPYJ4GOmBBRaqnMid67HgAmdYQRkouczdITccL84R7Y28DlBlKrG5pL6cdy2XhQu4zTLnuiCmNn0ymfwyN/vM7+X5zZywbds2QRDy8/MTiYSiKE6nU3szJNcBIRRFEQDgcDgopYKqqqIoqqpKCIEQAgDYfwHGGCGEMYYQGnSUUoyxpmm6rvMzKaVkI8wVq2HCpDbodu3aJWwEQsj8mc/mN9aVLSG43W6bzcYzKAjCjh07AADKOsA6eAYURVFVlRsY49eRCgAAu93ucrkURRFF0el0OhyOAgvsdjs38l+AZ31LCBhj0wX4Amgd0cFBbtyMRG5GIjzlhBCM8dZklBqZ4v6Louh2u1VVdblcqqru3r37p7OVba2tJcUlV6+pHq+3rrb2dSwbgrXZbC6Xy1pQhJDT6VQUhfvCF03DCrwuFIRQOBjEGAuiKAqCsGfPHk3TrApIJBJmKefn5hljXEObgNaDkwxcNugYYy6Xi3cFl55V9Lqu+290BAfD4VD40OeHCCHWPdq6Wjk6/c3jd4khYy5ofStomtbZ1cUdAQDwA0wwxhBCsVgMoUEIA0buGGM8R9YzzagxxqfLyhBCPKE8dnMbpVSSpAvnao8dPVJ+ujRHxzsumUyaYXLRIIQAAFy0ZtZ5R5rQdX1suF+5JIb7OhhjBh1CiCfODFDXdUKIoig804QQxhiE0LqNYymT0XUdDg3UyY05OgCAqqqbtqZTaUmSnE7n4cN5AAAelyRJEEJtI7Ir2bKaHx2VpS+988m+W2OxO3fjt8ZicnsLCPwyPjN5m9yGYdjW4Z+YnODxmgFawRi7czc+NTuToyOEjIyOLGWWdF3/8/EjT5MXRRH/kjE2OjrK9yiKwhhbWV7WdX11bY3Pq2trGOPenp5wMJgrBa8ahJA35vj4+FB0CKEco67ryWSSEBIOBm9GIk+ePk2nUulUKplMplPpdCqdSCR4lXLX56uts+mOnJ+bb25ptuUdPvb10fwvbeWVFY3SpVZ/a835WlmRL17y+FqaIIQjoyMG3b9ibnbWr3Z88PGHez/7qNie91XBoU++2N8gew4e+6L4zMnjPxR+U/KdqqoIIcHanpTSqempLUeoP1RWXX7iZFGT7Kk4Zfe3SBAGw6HwUHQoHArzXL3sWVEUrcp8FZq2nF3JZleyS5lMdnV1KZMxCrX4oOGKNzo8zOWdK4Xb7ZYkiRM9/ONhZ2/39T7Q2dttjtZr/vgkef581Tp0/fnfS3+RafL4yaOFhYVYLGYIhVLKXwYu46nZmZqL56rq66rq62oaz1fV15XXnG2UPQeOFAjCO8Jb720e23cK23d29YaDMDA9M2PQdXZ1ITTc3Q0gDAxE0LUbfdFIZKB/YGJ8YvHB4lgsNj0xOTg4pF7vubewuGnQ39i9hcVnz1Iry8s5GW96W+/E41vqZm6WLrwG1LzcHQ6HIBjPYzKZZOx3Smfp/4DQ2wMO7N975rTzarsi+7z9ITgcRW84+kOwPwSDMMDnsRgWhqPRouN577/7Nn+2D36670ypo7iosLio0FF0wlF0ghsl339rrhQVFnBDkT09XX7V32zM7XJ/CArxePzX27G+XtCp+q+2K/62FtnnbbrsabrsUa7Iss/rb2vhi9x95YqvsaFe7WiXfd6fa6sbG+obG+q5YXjH77tAIMAYs4hWu3ChoaKiUpKkqupqCCEh5EZ39/379617+P8Fu92+b9/+0lOnKKX/AMUaRWPjl3dLAAAAAElFTkSuQmCC&quot; nextheight=&quot;2450&quot; nextwidth=&quot;1978&quot; class=&quot;paragraph-editor-image&quot;&gt;&lt;figcaption&gt;Tabs for deep-dives&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Before, a project like this needed justifying. I&apos;d have asked whether I&apos;d use it enough to make the time worthwhile. If I expected to spend weeks on it, I&apos;d have started thinking about other users, pricing, how it could pay for itself. A small annoyance would have become a much bigger commitment; a weekend is much easier to justify. I wanted it to exist, and that was enough.&lt;/p&gt;&lt;p&gt;A few hours used to get a programmer a script; now it gets a polished app, and it gets a non-programmer something instead of nothing. More people can clear the bar, and more ideas clear it: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; class=&quot;dont-break-out&quot; href=&quot;https://armstr.ng/projects&quot;&gt;small tools that solve a personal annoyance&lt;/a&gt;, weird ideas you&apos;re curious about, things you build simply because you&apos;d enjoy having them or the world would be more beautiful if they existed. The audience can be one person.&lt;/p&gt;&lt;p&gt;The same economics cut the other way: heade.rs will probably not rank for &quot;domain lookup tool,&quot; and neither will most things built like it. Cheaper software means more people shipping, and another demo in a crowded feed is easy to scroll past. If you want to reach more people, you still have to give them a reason to care. I&apos;m more interested when someone explains what bothered them, what they made, and the choices they cared about along the way. The story gives me a reason to try the thing.&lt;/p&gt;&lt;p&gt;Sometimes sharing a personal project leads somewhere unexpected. Four years ago &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; class=&quot;dont-break-out&quot; href=&quot;https://paragraph.com/&quot;&gt;Paragraph&lt;/a&gt; started as something I built for myself, then other people wanted to use it too. I&apos;m glad I shared it.&lt;/p&gt;&lt;p&gt;But I also like that &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; class=&quot;dont-break-out&quot; href=&quot;https://heade.rs&quot;&gt;heade.rs&lt;/a&gt; doesn&apos;t need to follow that path. If it stays a little tool I use occasionally to look up a domain, it was still worth building.&lt;/p&gt;</content:encoded><author>colin@armstr.ng (Colin Armstrong)</author></item><item><title>The data spectrum: portable, usable &amp; interoperable</title><link>https://armstr.ng/writing/data-spectrum</link><guid isPermaLink="true">https://armstr.ng/writing/data-spectrum</guid><description>Moving beyond just exportable data.</description><pubDate>Tue, 15 Oct 2024 07:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Portability&lt;/h2&gt;&lt;p&gt;Many platforms and companies enable users to &lt;em&gt;export &lt;/em&gt;their data. Twitter lets you export your followers, Substack lets you export your email subscribers, and Facebook lets you export a list of all your Facebook friends.&lt;/p&gt;&lt;p&gt;This data is considered &lt;strong&gt;portable&lt;/strong&gt;: you can export it, archive it, back it up, print it out, frame it - the choice is yours. &lt;/p&gt;&lt;h2&gt;Usability&lt;/h2&gt;&lt;p&gt;However, the &lt;strong&gt;usability&lt;/strong&gt; of this portable data varies. While you can export your Twitter followers&amp;apos; usernames, it doesn&amp;apos;t grant you the ability to interact with them on other platforms or contact them outside Twitter. As such, Twitter data, though exportable, has limited usefulness.&lt;/p&gt;&lt;p&gt;Substack is a bit better: exporting a list of your subscriber&amp;apos;s emails allows you to contact them directly.&lt;/p&gt;&lt;p&gt;The intersection of portability and usability can be visualized on a spectrum, with companies frequently moving along the scale as they evolve and achieve product-market fit (or, if they simply change their mind). Mailchimp allows users to export their subscriber emails but later changed their rules, &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://decrypt.co/107099/intuit-owned-mailchimp-is-banning-crypto-content-creators&quot;&gt;locking out several cryptocurrency newsletters&lt;/a&gt; from accessing their subscriber list and abruptly shifting their position on the spectrum, moving from portable &amp;amp; usable to unportable.&lt;/p&gt;&lt;p&gt;The concept of data ownership is also relevant here. On Mailchimp, users technically did not own their email lists, as access to them could be revoked at any time. Truly owned data would always be both portable and usable, with no possibility for companies to alter the rules and restrict access.&lt;/p&gt;&lt;p&gt;Generally, companies lack incentives to make their data both portable and usable, as some degree of lock-in benefits them. For example, Twitter&amp;apos;s network effects would be weakened if users could seamlessly migrate to Mastodon with a single click, taking their audience with them.&lt;/p&gt;&lt;h2&gt;Interoperability&lt;/h2&gt;&lt;p&gt;If a platform provides both portable &amp;amp; usable data, it&amp;apos;s generally easy for users to migrate to competitors if they decide they dislike the platform. Though, why should leaving the platform be the user&amp;apos;s only option if they dislike a particular feature or want a specific functionality not offered? Why should there not be some level of extensibility built in by default, letting developers customize to their liking and giving users more choice?&lt;/p&gt;&lt;p&gt;This is where &lt;em&gt;interoperability&lt;/em&gt; comes into play. If data and functionality is &lt;em&gt;interoperable, &lt;/em&gt;it can be extended and augmented and modified and built upon. Data isn&amp;apos;t just portable anymore - it&amp;apos;s composable. &lt;/p&gt;&lt;p&gt;Interoperability typically occurs using APIs: companies can expose core bits of their product through APIs, allowing other developers to interact with the underlying data or functionality.&lt;/p&gt;&lt;p&gt;Like portability and usability, interoperability exists on a spectrum, ranging from non-interoperable (no APIs) to partially interoperable (exposing a subset of functionality) to fully interoperable. Again, like portability and usability, companies can and often do move along this spectrum.&lt;/p&gt;&lt;p&gt;Substack data is currently portable - you can export your email subscribers - but they offer no APIs, and thus no ability to customize or extend the platform beyond what they offer. &lt;/p&gt;&lt;p&gt;Twitter, on the other hand, famously had a thriving developer ecosystem - they had nearly full interoperability, letting developers build whatever they wish. Users had an array of choice in how to interact with their followers - if they disliked the official Twitter client they were free to use an array of others.  Retweets and other core primitives came as a result of developers - not Twitter! - building 3rd party apps. Then, some of these apps emerged as competitors, so Twitter crippled API access and shut them off.&lt;/p&gt;&lt;p&gt;Striving for full interoperability can align platform owners&amp;apos; incentives with those of their users, encouraging competition based on product quality rather than lock-in. Though, even with fully interoperable platforms, there&amp;apos;s no guarantees that they&amp;apos;ll &lt;em&gt;continue &lt;/em&gt;to be fully interoperable in the future, as evident by Twitter.&lt;/p&gt;&lt;h2&gt;The future&lt;/h2&gt;&lt;p&gt;Users &amp;amp; developers alike should strive for portability, usability, and interoperability on the far end of the spectrum:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Fully portable. Users can export it and take it elsewhere if they wish.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Completely usable. Exporting the data actually allows the user to make use of it elsewhere.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Entirely interoperable. All data &amp;amp; features are exposed via APIs, letting developers extend &amp;amp; build upon functionality.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Of course, this is not sufficient - we also need strong guarantees that the level of portability, usability and interoperability won&amp;apos;t change. To make these guarantees, the data needs to be owned by the user that created it or governed by some code that can&amp;apos;t change. There can&amp;apos;t be a centralized entity determining what data users are allowed to export, or what functionality is exposed via APIs (and how), since we&amp;apos;ve seen repeatedly that rules can be changed on a whim.&lt;/p&gt;&lt;p&gt;Building on top of sufficiently decentralized protocols and their associated primitives - not platforms - can help provide these guarantees. Networks like Ethereum, applications like Farcaster, and primitives like NFTs are making strides toward this future. For a developer building on top of Ethereum and using NFTs as a core primitive, there are protocol-level guarantees that rules won&amp;apos;t abruptly change.  &lt;/p&gt;</content:encoded><author>colin@armstr.ng (Colin Armstrong)</author></item><item><title>Wallets instead of emails for identity</title><link>https://armstr.ng/writing/identity-wallets-over-emails</link><guid isPermaLink="true">https://armstr.ng/writing/identity-wallets-over-emails</guid><description>Why open &amp; permissionless systems can lead to better identity.</description><pubDate>Sat, 14 Jan 2023 17:51:00 GMT</pubDate><content:encoded>&lt;h2&gt;Email&lt;/h2&gt;&lt;p&gt;Most websites require your email address for creating an account. Email-based authentication allow the website to 1) contact you and 2) uniquely identify you. &lt;/p&gt;&lt;p&gt;Email does a good job at enabling contact - you can retrieve forgotten passwords just as easily as you can email your Grandmother - but it does a pretty poor job at identifying you. Given your email address, there&amp;apos;s no understanding nor context &lt;em&gt;about you&lt;/em&gt;, the user. Because of this, companies like Clearbit have built large businesses around enriching email addresses with data sourced from around the web, to make them more useful as identities.&lt;/p&gt;&lt;h2&gt;OAuth&lt;/h2&gt;&lt;p&gt;This lack of context &amp;amp; understanding is in contrast to other common mechanisms of identifying users, such as OAuth. Login with Google, Twitter or Facebook are a few examples of OAuth-based logins that go beyond just an identity - they let 3rd party developers tap into the closed data these companies have (for better or for worse).&lt;/p&gt;&lt;img src=&quot;https://storage.googleapis.com/papyrus_images/0e58b7ceb3356adde117f7073340b474.jpg&quot; blurDataURL=&quot;data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAEAAIDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAdEAACAgIDAQAAAAAAAAAAAAABAgMEAAUGESFh/8QAFQEBAQAAAAAAAAAAAAAAAAAAAQL/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwt6fQ0rc1WLjejaOB2iVnpqWIU9An75jGMoP/2Q==&quot; width=&quot;344&quot; float=&quot;none&quot; class=&quot;image-node img-center embed&quot;&gt;&lt;p&gt;This helps identifiers moves beyond &lt;strong&gt;just&lt;/strong&gt; an identifier, and toward becoming a &lt;strong&gt;user&lt;/strong&gt;, with interests, friends, contacts, followers, profile pictures, genders, birthdates, and more.&lt;/p&gt;&lt;p&gt;This is powerful. As a developer, supporting “Login with [social platform]” lets you tap into this enriched data for numerous things:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Bootstrap network effects &lt;em&gt;(eg prompting users to subscribe to all their Twitter followers immediately after registration)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Provide mechanisms of virality &lt;em&gt;(eg automatically crosspost content they create to Facebook)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Provide better onboarding&lt;em&gt; (eg pre-populating user accounts with avatars, interests and more)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There are several downsides to this method of OAuth based authentication, for both developers building atop these platforms and for users that authenticate with them: &lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Platforms can change the rules. &lt;/strong&gt;Developers are constrained by what platforms allow. Google and Facebook are &lt;em&gt;not &lt;/em&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://nakamoto.com/credible-neutrality/&quot;&gt;credibly neutral&lt;/a&gt; open protocols - they&amp;apos;re closed platforms with everchanging functionality. There have been &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://geoawesomeness.com/developers-up-in-arms-over-google-maps-api-insane-price-hike/&quot;&gt;many&lt;/a&gt; different &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://www.theregister.com/2012/08/17/twitter_api_rules/&quot;&gt;instances&lt;/a&gt; in the past where platforms changed the rules and crippled developers building atop of them.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Developers get an incomplete picture. &lt;/strong&gt;A user&amp;apos;s audience is commonly split across multiple platforms. OAuth&amp;apos;ing with Facebook isn’t very beneficial to myself or the developer&amp;apos;s app if 90% of my social graph exists on Twitter. It&amp;apos;s unrealistic for developers to request or expect users to manually link with every single social platform to get a full picture.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;User&amp;apos;s can&amp;apos;t exit with interoperability. &lt;/strong&gt;If I decide I want to exit Twitter after the Elon Musk acquisition, it’s not possible to export my Twitter followers and use it elsewhere. Even worse - if Twitter decides to ban me, that’s it: there’s no way 3rd party developers building atop Twitter can make use of this data anymore. &lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Wallets&lt;/h2&gt;&lt;p&gt;Using cryptowallets alongside crypto-enabled social protocols are an alternative that I believe will bridge the gap between an &lt;strong&gt;&lt;em&gt;identity&lt;/em&gt;&lt;/strong&gt; and a &lt;strong&gt;&lt;em&gt;user&lt;/em&gt;&lt;/strong&gt; and improve upon the aforementioned issues:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;They are credibly neutral. Building atop &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://www.varunsrinivasan.com/2022/01/11/sufficient-decentralization-for-social-networks&quot;&gt;sufficiently decentralized&lt;/a&gt; &lt;em&gt;protocols, &lt;/em&gt;not&lt;em&gt; platforms,&lt;/em&gt; guarantee that the underlying functionality won&amp;apos;t change after I, as a developer, built a sustainable business.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;It&amp;apos;s easier to get a full picture of a user&amp;apos;s social graph. As a developer, I can permissionlessly ingest &lt;em&gt;the full social graphs &lt;/em&gt;of all web3 social platforms, just given a user&amp;apos;s wallet address with no intervention from the user needed. If a user has never used Farcaster but is active on Lens, no problem -- I can still programmatically ingest both to get the complete picture.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Users can exit with interoperability. If users dislike (or are banned from) a specific client built atop a certain social graph, they can move to another client and continue interacting with the followerbase they&amp;apos;ve built at the protocol-level. Similarly, developers can continue making use of all the underlying data.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Though, crypto wallets are not without downsides. Adoption is currently low - everyone has an email but not everyone has a crypto wallet. Additionally, emails allow for easy message exchange but wallets currently do not (though many protocols are trying to fix this), so it&amp;apos;s challenging to reach users strictly by their wallets. Lastly, privacy controls are non-existent: if I &lt;em&gt;didn’t&lt;/em&gt; want to share a list of my on-chain followers with a certain platform, there’s no current method of preventing this (in contrast to the granular, user-controlled permissions that OAuth uses).&lt;/p&gt;&lt;p&gt;At &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://paragraph.xyz&quot;&gt;Paragraph&lt;/a&gt;, we&amp;apos;ve built &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow ugc&quot; class=&quot;dont-break-out dont-break-out&quot; href=&quot;https://paragraph.xyz/@blog/discovery&quot;&gt;several integrations&lt;/a&gt; involving wallets-as-an-identity and it’s been a very positive experience. For example, when a user connects their wallet and signs in, we detect if they have a Faracaster account. If so, we permissionlessly pull in their avatar, name, &amp;amp; users they’re following, and bootstrap their Paragraph account by prompting them to subscribe to their followers. No gatekeeping, no OAuth, no action needed from the user - nothing beyond just a wallet address &amp;amp; credibly neutral social protocols.&lt;/p&gt;&lt;p&gt;Collect this post as an NFT:&lt;/p&gt;&lt;div data-type=&quot;highlightEmbed&quot; nftId=&quot;63c2e9fe3de4fce815d46692&quot; imgUrl=&quot;https://highlight-creator-assets.highlight.xyz/main/image/6a0fc1e1-f184-4f58-9cd4-1301bcea3fea&quot;&gt;
    &lt;img src=&quot;https://highlight-creator-assets.highlight.xyz/main/image/6a0fc1e1-f184-4f58-9cd4-1301bcea3fea&quot; 
    style=&quot;
    width: 100;
    margin-left: auto;
    margin-right; auto;
    &quot;
    /&gt;
    &lt;div style=&quot;text-align: center;&quot;&gt;
        &lt;a  style=&quot;
    display: inline-block;
    border-radius: 4px;
    margin-top: 4px;
    background-color: rgb(59, 130, 246);
    padding: 16px 24px;
    font-weight: 600;
    line-height: 100%;
    color: rgb(255, 255, 255);
    text-decoration: none;
    &quot;
    href=&quot;https://mint.highlight.xyz/63c2e9fe3de4fce815d46692&quot;&gt;
    Collect NFT
        &lt;/a&gt;
      &lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</content:encoded><author>colin@armstr.ng (Colin Armstrong)</author></item><item><title>Spending time deliberately</title><link>https://armstr.ng/writing/spending-time-deliberately</link><guid isPermaLink="true">https://armstr.ng/writing/spending-time-deliberately</guid><description>Thoughts on doing my best work.</description><pubDate>Sun, 23 Oct 2022 22:16:09 GMT</pubDate><content:encoded>&lt;p&gt;Throughout any given day, my mood &amp;amp; state of mind fluctuates depending on many factors: quality of sleep, what I ate that day, how my personal relationships are sgoing, how my company is doing, etc.&lt;/p&gt;&lt;p&gt;Because of this, I try to avoid a fixed schedule as much as possible. I view my day as fluid: I have a collection of different states of mind throughout the day, and I have a collection of work items that need to be done, so I try to match these up.&lt;/p&gt;&lt;p&gt;When I&amp;apos;m feeling creative, I write code or do frontend design. When I&amp;apos;m feeling insightful, I think about longer-term company strategy. When I&amp;apos;m feeling extroverted, I focus on user outreach and customer conversations. Mapping my states of mind to the appropriate deliverable lends itself to my best work. The opposite is also true - forcing tasks in an improper state of mind often produces worse results.&lt;/p&gt;&lt;p&gt;This is a simplified view - it certainly won&amp;apos;t be possible in all jobs or on all days - but I&amp;apos;m fortunate enough right now that I&amp;apos;m able to abide by this as much as possible, given that an early-stage startup has time spent mainly between building and talking to users (and both of these have a spectrum of sub-tasks that tap into different states of mind). This is in contrast to my time spent at Google, where I often had days filled with back-to-back meetings.&lt;/p&gt;&lt;p&gt;Deliberately &lt;em&gt;not &lt;/em&gt;working is also important. If I&amp;apos;m feeling awful and particularly unproductive, I prefer to make a conscious decision to step away and rest, in contrast to making little progress on something while beating myself up over the lack of productivity. &lt;/p&gt;</content:encoded><author>colin@armstr.ng (Colin Armstrong)</author></item><item><title>Personal automation with Huginn using Slack, Docker and GCP</title><link>https://armstr.ng/writing/personal-automation-with-huginn-using-slack-docker-and-gcp</link><guid isPermaLink="true">https://armstr.ng/writing/personal-automation-with-huginn-using-slack-docker-and-gcp</guid><pubDate>Tue, 20 Sep 2022 20:52:20 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Paragraph is a blazing-fast, privacy-first, no-frills-attached blogging &amp;amp; newsletter platform. &lt;/em&gt;&lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://paragraph.xyz?utm_source=papyrus&amp;amp;utm_medium=colin_blogpost&amp;amp;utm_campaign=huginn_blogpost&quot;&gt;&lt;em&gt;Sign up&lt;/em&gt;&lt;/a&gt;&lt;em&gt; and &lt;/em&gt;start&lt;em&gt; writing posts just like this one.&lt;/em&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;Productivity &amp;amp; automation tools can be powerful. I was delighted to come across a recent &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://news.ycombinator.com/item?id=21772610&quot;&gt;thread on Hacker News&lt;/a&gt; discussing a supercharged automation tool: &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://github.com/huginn/huginn&quot;&gt;Huginn&lt;/a&gt;. This open-source software performs automated tasks by using &amp;apos;agents&amp;apos; to watch for &amp;apos;events&amp;apos;, and triggering &amp;apos;actions&amp;apos; based on these events.&lt;/p&gt;&lt;p&gt;For example, if there&amp;apos;s a sudden spike in discussion on Twitter with the terms &amp;quot;San Francisco Earthquake&amp;quot;, Huginn can send a text to my phone. Or, if a time-sensitive flight deal is posted on one of the many deal-finding websites out there, Huginn can send me an email with the price and a link to Google Flights.&lt;/p&gt;&lt;p&gt;Compared to other popular automation tools (IFTTT, Zapier), Huginn has the following benefits:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Self-hosted &amp;amp; completely private&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Powerful data processing: write your own JS or use shell scripts&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Liquid templating&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I wanted to go beyond just automation and introduce some organization - I wanted all notifications to be cataloged &amp;amp; delivered in a centralized way. A personal Slack workspace seemed like the perfect solution for this - I can have a &lt;code&gt;#flights&lt;/code&gt; channel for flight deals, or a &lt;code&gt;#trending&lt;/code&gt; channel for the, er, pending San Francisco emergencies.&lt;/p&gt;&lt;p&gt;I also wanted all of this to be free. Huginn has pretty lax runtime resource requirements (even able to run on a &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://github.com/huginn/huginn/wiki/Running-Huginn-on-minimal-systems-with-low-RAM-&amp;amp;-CPU-e.g.-Raspberry-Pi&quot;&gt;Raspberry Pi&lt;/a&gt;, with some tweaking), so a free GCP micro tier instance was perfect for this.&lt;/p&gt;&lt;h2&gt;Automation Goals&lt;/h2&gt;&lt;p&gt;Let&amp;apos;s formalize what I specifically wanted to accomplish with Huginn. Note that this is a small subset of the things possible with Huginn - check out the project&amp;apos;s &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://github.com/huginn/huginn#here-are-some-of-the-things-that-you-can-do-with-huginn&quot;&gt;Github&lt;/a&gt; for more inspiration.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Twitter notifications&lt;/strong&gt;: whenever keywords of interest are tweeted (such as my projects or blog), I want to get notified immediately. Whenever a spike occurs for other keywords (&amp;quot;San Francisco Emergency&amp;quot;), notify me.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hacker news notifications&lt;/strong&gt;: whenever an article hits the frontpage discussing something I&amp;apos;m interested in, notify me.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flight deals&lt;/strong&gt;: if a flight deal is posted online to one of the many websites I follow (Secret Flying, ThePointsGuy, FlyerTalk), and the flight originates from a nearby airport, notify me.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Product deals&lt;/strong&gt;: if a product I&amp;apos;m interested in is posted on Slickdeals, notify me.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Amazon price drops&lt;/strong&gt;: if a product I&amp;apos;m interested in drops below some predefined price threshold, notify me.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I want all notifications to be sent to me via a personal Slack workspace, on different channels.&lt;/p&gt;&lt;h2&gt;Deploying Huginn&lt;/h2&gt;&lt;h3&gt;Prerequisites&lt;/h3&gt;&lt;p&gt;The easiest way to &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://github.com/huginn/huginn/blob/master/doc/docker/install.md&quot;&gt;install Huginn&lt;/a&gt; is via Docker. Luckily, Google Compute Engine supports deploying Docker containers natively on a lean &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://cloud.google.com/container-optimized-os/docs&quot;&gt;container-optimized OS&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There are a few key things we need to do in order to have a successful Huginn deploy on the f1-micro (free tier) instances.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Enable and create a swap file&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;f1-micro instances have 614MB of memory. This is not enough to run Huginn out of the box - doing so will cause Docker to encounter &lt;code&gt;Error Code 137&lt;/code&gt; &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://success.docker.com/article/what-causes-a-container-to-exit-with-code-137&quot;&gt;(out of memory)&lt;/a&gt; errors. To solve this, we need to create a swap file in the VM. Note that a swapfile will decrease the performance of Huginn - if you&amp;apos;re interested in better performance for a price, consider deploying on a better VM.&lt;/p&gt;&lt;p&gt;Disk-based swap is &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://stackoverflow.com/questions/58210222/how-to-enable-swap-swapfile-on-google-container-optimized-os-on-gce&quot;&gt;disabled&lt;/a&gt; by default in container-optimized OS. To enable and set the swap file every time the VM is booted, we can use a custom startup script (shown below).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Mount the Huginn MySQL database to a directory on the host&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;By default, Huginn creates a MySQL database inside the container. This is problematic, as the container now relies on &lt;em&gt;state&lt;/em&gt;, and your database will &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://github.com/huginn/huginn/blob/master/docker/multi-process/README.md&quot;&gt;get deleted every Huginn upgrade&lt;/a&gt;. We can use a volume mount to mount the database in the container to a directory in the host. Alternatively, you can mount a persistent disk and write the database to it.&lt;/p&gt;&lt;h3&gt;Deployment on GCP using Docker&lt;/h3&gt;&lt;p&gt;Head over to GCP, create a new project, and create a new instance.&lt;/p&gt;&lt;p&gt;On the instance creation page, use the following settings:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;f1-micro&lt;/code&gt; machine type&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Check &amp;apos;Deploy a container image to this VM instance&amp;apos;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Container image URL is &lt;code&gt;docker.io/huginn/huginn&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Add a Directory volume mount. The mount and host paths should be &lt;code&gt;/var/lib/mysql&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We also need to add in a statup script. This script lets us 1) enable and turn on a swap file, and 2) change permissions of the volume mount on the host. The latter is required or MySQL won&amp;apos;t be able to start.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;#! /bin/bash
sysctl vm.disk_based_swap=1
fallocate -l 2G /var/swapfile
chmod 600 /var/swapfile
mkswap /var/swapfile
swapon /var/swapfile
chmod 777 /var/lib/mysql&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After the VM is created, head to your VM&amp;apos;s external URL (port 3000) and you should be greeted with the default Huginn login page!&lt;/p&gt;&lt;p&gt;I suggest &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address&quot;&gt;reserving a static external IP&lt;/a&gt; for this VM, so the IP doesn&amp;apos;t change. You can even take it a step further and associate this to a domain name - like &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;http://automation.colinarms.com&quot;&gt;automation.colinarms.com&lt;/a&gt; - for ease of access.&lt;/p&gt;&lt;p&gt;Now, let&amp;apos;s dive into Huginn.&lt;/p&gt;&lt;h2&gt;How Huginn Works: Agents &amp;amp; Events&lt;/h2&gt;&lt;p&gt;At a high level, Huginn relies on two key things: agents and events. Agents are things that monitor for you and create events (possibly if some criteria is met). An example agent is an &lt;code&gt;RssAgent&lt;/code&gt;, which monitors an RSS feed for new articles. Events created by the &lt;code&gt;RssAgent&lt;/code&gt; can be passed to a &lt;code&gt;TriggerAgent&lt;/code&gt;, which uses some regex filter to only listen to keywords of interest; and finally it emits a formatted message, perhaps to a &lt;code&gt;SlackAgent&lt;/code&gt;, that finally sends a message to a Slack channel.&lt;/p&gt;&lt;p&gt;You can imagine how this works in practice. For the flight deals usecase, for example: we can create an &lt;code&gt;RssAgent&lt;/code&gt; for the &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://www.secretflying.com/feed/&quot;&gt;Secret Flying RSS feed&lt;/a&gt;. The &lt;code&gt;TriggerAgent&lt;/code&gt; can listen to these events, filter for &amp;quot;San Francisco Airport&amp;quot;, and the &lt;code&gt;SlackAgent&lt;/code&gt; can message my &lt;code&gt;#flights&lt;/code&gt; channel when this happens&lt;/p&gt;&lt;p&gt;Multiple agents for a single usecase can be grouped into a &lt;code&gt;Scenario&lt;/code&gt; - in the above example, a &lt;code&gt;Flight Deal Scenario&lt;/code&gt; would make sense.&lt;/p&gt;&lt;p&gt;Let&amp;apos;s walk through this example. If you want to get started immediately, you can &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://gist.github.com/seeARMS/103b3399f3f925fb6c366600f0bad3c6&quot;&gt;download my agents&lt;/a&gt; and import them into Huginn directly.&lt;/p&gt;&lt;h2&gt;Creating your First Agent&lt;/h2&gt;&lt;h3&gt;1) Monitoring the RSS feed&lt;/h3&gt;&lt;p&gt;On Huginn, create a new RSS Agent. Configure the following params:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Name&lt;/strong&gt; your agent something descriptive. I used &amp;quot;Secret Flying RSS Agent&amp;quot;.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schedule&lt;/strong&gt; your agent for however frequently you&amp;apos;d like it to check for updates. I used 30 mins.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep events&lt;/strong&gt; for some period of time. I used 7 days.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Use the following JSON in the options:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;{
  &amp;quot;expected_update_period_in_days&amp;quot;: &amp;quot;2&amp;quot;,
  &amp;quot;clean&amp;quot;: &amp;quot;true&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://www.secretflying.com/feed/&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Expected update period is the period at which Huginn should expect the agent to be updated - if it doesn&amp;apos;t happen, the agent is considered not working.&lt;/p&gt;&lt;p&gt;Save your agent, and give it a manual run - you should see events populate from the underlying feed.&lt;/p&gt;&lt;h3&gt;2) Filtering for nearby airports&lt;/h3&gt;&lt;p&gt;Now, create a &lt;code&gt;TriggerAgent&lt;/code&gt;. We want to filter newly posted articles for only nearby airports - in my case, San Francisco or San Jose airport.&lt;/p&gt;&lt;p&gt;Fill it out similar to the first agent. But, this time select your RSS Agent as this agent&amp;apos;s &lt;code&gt;source&lt;/code&gt;. Events from the RSS Agent will be fed into this.&lt;/p&gt;&lt;p&gt;Use the following JSON in the options:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;{
  &amp;quot;expected_receive_period_in_days&amp;quot;: &amp;quot;2&amp;quot;,
  &amp;quot;keep_event&amp;quot;: &amp;quot;false&amp;quot;,
  &amp;quot;rules&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;regex&amp;quot;,
      &amp;quot;value&amp;quot;: &amp;quot;(sfo|san francisco|sjc|san jose)&amp;quot;,
      &amp;quot;path&amp;quot;: &amp;quot;title&amp;quot;
    }
  ],
  &amp;quot;message&amp;quot;: &amp;quot;[Secret Flying] &amp;lt;a href=\&amp;quot;{{url}}\&amp;quot;&amp;gt;{{title}}&amp;lt;\/a&amp;gt; {{description}}&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;apos;re filtering the RSS feed&amp;apos;s title to contain my nearby airports, and emitting a &lt;code&gt;message&lt;/code&gt; with the URL, title and a description.&lt;/p&gt;&lt;h3&gt;3) Notifying on Slack&lt;/h3&gt;&lt;p&gt;Lastly, create a &lt;code&gt;SlackAgent&lt;/code&gt;. After &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://slack.com/get-started#/&quot;&gt;registering a new Slack workspace&lt;/a&gt; and &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://my.slack.com/services/new/incoming-webhook&quot;&gt;creating a new Slack webhook&lt;/a&gt;, set the &lt;code&gt;source&lt;/code&gt; to the Trigger Agent you just created.&lt;/p&gt;&lt;p&gt;Put the following JSON in the options:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;{
  &amp;quot;webhook_url&amp;quot;: &amp;quot;https://hooks.slack.com/services/your-webhook-url&amp;quot;,
  &amp;quot;channel&amp;quot;: &amp;quot;#flights&amp;quot;,
  &amp;quot;username&amp;quot;: &amp;quot;Huginn&amp;quot;,
  &amp;quot;message&amp;quot;: &amp;quot;{{message}}&amp;quot;,
  &amp;quot;icon&amp;quot;: &amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Save your agent, and eventually you should begin receiving flight deals!&lt;/p&gt;&lt;div&gt;&lt;div class=&quot;react-component&quot; data-drag-handle=&quot;true&quot; data-node-view-wrapper=&quot;&quot; style=&quot;white-space:normal&quot;&gt;&lt;img src=&quot;https://storage.googleapis.com/papyrus_images/ee7b868109ff0eb7bb8dc89ed2920083&quot; class=&quot;image-node img-center embed&quot;/&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;Wrapping it up&lt;/h2&gt;&lt;p&gt;This example describes a single usecase for what&amp;apos;s possible with Huginn. If you&amp;apos;re interested in the other usecases I described above, you can &lt;a target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://gist.github.com/seeARMS/103b3399f3f925fb6c366600f0bad3c6&quot;&gt;download and import them&lt;/a&gt; into your own installation.&lt;/p&gt;&lt;p&gt;This post just scratches the surface of what&amp;apos;s possible. With Huginn, let your &lt;code&gt;Agents&lt;/code&gt; monitor on your behalf and free up your time for more important things.&lt;/p&gt;</content:encoded><author>colin@armstr.ng (Colin Armstrong)</author></item></channel></rss>