<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[JonLuca's Blog]]></title><description><![CDATA[Mostly reverse engineering with some product and analysis thrown in]]></description><link>https://jonluca.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!8aum!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fjonluca.substack.com%2Fimg%2Fsubstack.png</url><title>JonLuca&apos;s Blog</title><link>https://jonluca.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sat, 11 Apr 2026 12:57:04 GMT</lastBuildDate><atom:link href="https://jonluca.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[JonLuca DeCaro]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[jonluca@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[jonluca@substack.com]]></itunes:email><itunes:name><![CDATA[jonluca]]></itunes:name></itunes:owner><itunes:author><![CDATA[jonluca]]></itunes:author><googleplay:owner><![CDATA[jonluca@substack.com]]></googleplay:owner><googleplay:email><![CDATA[jonluca@substack.com]]></googleplay:email><googleplay:author><![CDATA[jonluca]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Noticing when an app has servers in different regions]]></title><description><![CDATA[I was talking to a friend of mine about native apps and how some apps not built using UIKit/SwiftUI just feel laggier, and it got me thinking about other latency and small performance things you notice.]]></description><link>https://jonluca.substack.com/p/snappy-internet</link><guid isPermaLink="false">https://jonluca.substack.com/p/snappy-internet</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Mon, 26 Jun 2023 16:26:53 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9ee6fa33-d539-42f0-b1e3-7e1a633bb4fe_800x538.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was talking to a friend of mine about native apps and how some apps not built using UIKit/SwiftUI just feel laggier, and it got me thinking about other latency and small performance things you notice.</p><p>It reminded me about how every time I land back in Europe or in Asia, I can immediately tell which apps have a global CDN or edge computing. Everything just immediately feels a little slower.</p><p>There&#8217;s some debate about when users start feeling latency, but it&#8217;s widely accepted that increased latency will impact conversions and user behavior significantly.</p><p>The figure often thrown around is that <a href="https://www.conductor.com/academy/page-speed-resources/faq/amazon-page-speed-study/">for every 100ms of latency amazon lost ~1% of sales</a>. While I don&#8217;t know how true this is (especially given how slow the amazon app feels), it is something I know first hand. While at Pinterest we spent a ton of time improving the performance of our landing pages, as the faster it was the more users would convert and the more content they would consume.</p><h2>Floored Latency</h2><p>The speed of the experience you can offer your users is floored by a few variables, but most notably the physical distance from the origin server to where the user is actually sitting.</p><p>Us-east-1 (appropriately located right by &#8220;Centreville, Virginia&#8221;) is one of the main data centers run by AWS. If you&#8217;re a startup (or even quite a few mature companies), you are likely to deploy your application here. If you manage a stateful service, or your architecture doesn&#8217;t support distributed compute, you are likely to <em>only</em> deploy here. If you have users in Sydney, Australia, any network request you make will need to travel 15,677km to make it there, as the crow flies.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nmnQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nmnQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 424w, https://substackcdn.com/image/fetch/$s_!nmnQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 848w, https://substackcdn.com/image/fetch/$s_!nmnQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 1272w, https://substackcdn.com/image/fetch/$s_!nmnQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nmnQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png" width="2140" height="1438" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1438,&quot;width&quot;:2140,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Distance from Centreville, Virginia to Sydney, Australia&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Distance from Centreville, Virginia to Sydney, Australia" title="Distance from Centreville, Virginia to Sydney, Australia" srcset="https://substackcdn.com/image/fetch/$s_!nmnQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 424w, https://substackcdn.com/image/fetch/$s_!nmnQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 848w, https://substackcdn.com/image/fetch/$s_!nmnQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 1272w, https://substackcdn.com/image/fetch/$s_!nmnQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13967db7-c2e4-4f2b-a9c0-f1bc2549f970_800x538.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Distance from Centreville, Virginia to Sydney, Australia</p><p>If you&#8217;re traveling at:</p><p>1) The speed of light</p><p>2) as the crow flies</p><p>3) with no overhead</p><p>then that means that you are <em>floored</em> at 104ms of latency for your request.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aHoG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aHoG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 424w, https://substackcdn.com/image/fetch/$s_!aHoG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 848w, https://substackcdn.com/image/fetch/$s_!aHoG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 1272w, https://substackcdn.com/image/fetch/$s_!aHoG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aHoG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png" width="1630" height="1532" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1532,&quot;width&quot;:1630,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Time in takes for the speed of light to travel from us-east-1 to Sydney&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Time in takes for the speed of light to travel from us-east-1 to Sydney" title="Time in takes for the speed of light to travel from us-east-1 to Sydney" srcset="https://substackcdn.com/image/fetch/$s_!aHoG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 424w, https://substackcdn.com/image/fetch/$s_!aHoG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 848w, https://substackcdn.com/image/fetch/$s_!aHoG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 1272w, https://substackcdn.com/image/fetch/$s_!aHoG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4369969e-ba28-42d5-b472-b84c0fe6bdbd_800x752.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Time in takes for the speed of light to travel from us-east-1 to Sydney</p><p>And this is assuming no interference, other traffic, or time spent handling the request.</p><p>In reality, the ping you&#8217;ll experience will be worse, at around 215ms (which is a pretty amazing feat in and of itself - all those factors above only double the time it takes to get from Sydney to the eastern US).</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ll0L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ll0L!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 424w, https://substackcdn.com/image/fetch/$s_!ll0L!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 848w, https://substackcdn.com/image/fetch/$s_!ll0L!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 1272w, https://substackcdn.com/image/fetch/$s_!ll0L!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ll0L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png" width="2000" height="1064" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1064,&quot;width&quot;:2000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;screenshot shwing ping to various cities around the globe&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="screenshot shwing ping to various cities around the globe" title="screenshot shwing ping to various cities around the globe" srcset="https://substackcdn.com/image/fetch/$s_!ll0L!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 424w, https://substackcdn.com/image/fetch/$s_!ll0L!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 848w, https://substackcdn.com/image/fetch/$s_!ll0L!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 1272w, https://substackcdn.com/image/fetch/$s_!ll0L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F128524ad-27fd-4d32-afe2-a68df1219cf6_800x426.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Ping to various cities around the globe</p><h2>Realized latency</h2><p>Having spent so much time trying to optimize web pages and API responses for performance, I&#8217;ve gotten a pretty good internal model for latency. I can&#8217;t quite tell the difference between a us-east-1 server when I&#8217;m in New York versus San Francisco, but I can definitely tell if you&#8217;ve got an instance deployed in <code>eu-central-1</code> or not when I&#8217;m in Italy, or <code>ap-east-1</code> when I&#8217;m in Sydney.</p><p>Using a global CDN can help get your assets to your users quicker, and most companies by this point are using something like Cloudflare or Vercel, but many still only serve static or cached content this way. Very frequently the origin server will still be a centralized monolith deployed in only one location, or there will only be a single database cluster.</p><p>As soon as you land back in the United States and turn of Airplane mode on your phone everything just starts feeling&#8230; snappier? A little more fluid? As much as T-Mobile and Verizon would like to take credit for that I don&#8217;t think theres much more to it than the physical location of the servers and where you are at that moment.</p>]]></content:encoded></item><item><title><![CDATA[Exploring how Magic Link works]]></title><description><![CDATA[Magic Link is a web3 wallet-as-a-service.]]></description><link>https://jonluca.substack.com/p/magic-link</link><guid isPermaLink="false">https://jonluca.substack.com/p/magic-link</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Wed, 31 May 2023 00:53:42 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/bcff46af-6db2-4a11-9725-46b1e6e69e79_766x576.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://magic.link/">Magic Link</a> is a web3 wallet-as-a-service. They provide an SDK that enables users to have a crypto wallet linked to just their email address, instead of having to install a chrome extension or local wallet.</p><p>I wanted to explore how it worked, and what it was actually doing under the hood.</p><h2>Web3 Wallets</h2><p>There are broadly two options for storing your crypto assets - custodial and non-custodial.</p><p>A custodial solution is something like coinbase - typically a single company, which manages the private key to your wallet for you, and which is the centralized gatekeeper to your funds.</p><p>A non custodial solution is one in which you store the private key yourself - the wallet simply provides the software that you run locally that generates and stores the key. This looks like Metamask or Phantom - you will typically install a chrome extension, go through an onboarding flow, save your recovery pass phrase, and only then can you begin to use it on various dapps or to move tokens around.</p><p>Generally speaking, the user experience with custodial services is better, and allows for easier user onboarding. However, it comes with some fairly major downsides, including centralization and greater likelihood of regulatory scrutiny.</p><p>Magic is somewhere in between - they say they are non custodial, but offer the UX of a custodial solution. No need for a user to install a separater chrome extension - they just need an email address and their browser.</p><h2>Under the hood</h2><p>Magic is relies on AWS for their product. Their sign in and user logic uses AWS Cognito, and their key storage uses KMS.</p><p>AWS Cognito is the identity solution offered by AWS - they will handle authenticating your users for you, and offer a wide variety of sign on methods, including email/password, phone number, 3rd party, and magic email.</p><p>When setting up a Magic wallet, you start with your email address. This will talk to AWS, which will send a link to your email address containing a unique token.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zOX2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zOX2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 424w, https://substackcdn.com/image/fetch/$s_!zOX2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 848w, https://substackcdn.com/image/fetch/$s_!zOX2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 1272w, https://substackcdn.com/image/fetch/$s_!zOX2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zOX2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png" width="766" height="576" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:576,&quot;width&quot;:766,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;magic link form&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="magic link form" title="magic link form" srcset="https://substackcdn.com/image/fetch/$s_!zOX2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 424w, https://substackcdn.com/image/fetch/$s_!zOX2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 848w, https://substackcdn.com/image/fetch/$s_!zOX2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 1272w, https://substackcdn.com/image/fetch/$s_!zOX2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93b365b9-8e7d-485f-8b8a-015c040fcd96_766x576.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Embedded Magic Link form (in this case, ImmutableX)</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mOJj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mOJj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 424w, https://substackcdn.com/image/fetch/$s_!mOJj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 848w, https://substackcdn.com/image/fetch/$s_!mOJj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 1272w, https://substackcdn.com/image/fetch/$s_!mOJj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mOJj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png" width="744" height="948" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:948,&quot;width&quot;:744,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;magic link form 2&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="magic link form 2" title="magic link form 2" srcset="https://substackcdn.com/image/fetch/$s_!mOJj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 424w, https://substackcdn.com/image/fetch/$s_!mOJj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 848w, https://substackcdn.com/image/fetch/$s_!mOJj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 1272w, https://substackcdn.com/image/fetch/$s_!mOJj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7c944d3-e1f4-4d1c-b5ca-f87d73823467_744x948.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Confirmation code for auth sign in</p><p>Once you&#8217;re on the verification page, your browser will talk to AWS to authenticate you, and Magic will start creating your key.</p><p>They will first generate the private key for the chain you&#8217;re using in the browser, using javascript, and keep that in memory. This is the root key material, which is used to sign transactions for whichver chain you&#8217;re using - this can be imported into metamask, a hardware wallet, etc.</p><p>This key, which we&#8217;ll call user key (UK), is never shown to the user. If you intercept the network request for initial encryption you&#8217;ll see it, base64 encoded.</p><p>It will then talk to KMS, using the user account that was just authenticated for you, and create a new key within KMS. This is the key that lives inside of Amazons data centers, and which will be used to encrypt and decrypt the UK.</p><p>Your browser will send the UK in plain text to KMS directly, never speaking to Magic links servers. It will retrieve the encrypted material, and store that both within the browser cache and with magic link.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AofM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AofM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 424w, https://substackcdn.com/image/fetch/$s_!AofM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 848w, https://substackcdn.com/image/fetch/$s_!AofM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 1272w, https://substackcdn.com/image/fetch/$s_!AofM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AofM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png" width="2822" height="870" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:870,&quot;width&quot;:2822,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;raw private key&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="raw private key" title="raw private key" srcset="https://substackcdn.com/image/fetch/$s_!AofM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 424w, https://substackcdn.com/image/fetch/$s_!AofM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 848w, https://substackcdn.com/image/fetch/$s_!AofM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 1272w, https://substackcdn.com/image/fetch/$s_!AofM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F567cb0b0-da83-4f15-83ae-92f99845f09c_800x247.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Decrypted private key coming back from KMS</p><p>If you copy and paste the &#8220;Plaintext&#8221; from above into Metamask, you can use your Magic.link private key outside of the Magic ecosystem.</p><p>Once they have the encrypted key contents, the decrypted key in memory, and the public key, magic will create an account for you on their centralized servers.</p><pre><code>{
  "data": {
    "auth_user_id": "my-magic-link-id",
    "auth_user_mfa_active": false,
    "auth_user_wallet_id": "my-wallet-id=",
    "challenge_message": null,
    "client_id": "my-client-id",
    "consent": {},
    "delegated_wallet_info": {
      "delegated_access_token": "{\"ciphertext\": \"cipher text\"}",
      "delegated_identity_id": "us-west-2:aws-key-id",
      "delegated_key_id": "delegated-key-id",
      "delegated_pool_id": "us-west-2:delegated-pool-id",
      "should_create_delegated_wallet": false
    },
    "encrypted_private_address": "long base64 encoded encrypted private key",
    "encrypted_seed_phrase": null,
    "hd_path": null,
    "login": {
      "identifiers": [],
      "oauth2": null,
      "type": "email_link",
      "webauthn": null
    },
    "public_address": "0xmypublickey",
    "recovery_factors": [],
    "utc_timestamp_ms": 1687790408203
  },
  "error_code": "",
  "message": "",
  "status": "ok"
}
</code></pre><p>Any subsequent logins follow this same setup, with the only difference being that once you&#8217;ve authenticated with AWS, your browser will first check with Magic to see if you already have an encrypted key stored with them, and if so, will retrieve the encrypted key and decrypt it using AWS KMS.</p><h2>Is this safe?</h2><p>The common refrain amongst crypto purists is &#8220;not your keys not your tokens&#8221; - this is in reference to custodial services, like Coinbase and FTX.</p><p>Magic is being a bit disingenous when they say they say they are non custodial - while it&#8217;s true that in their current set up they don&#8217;t directly have access to your raw private keys, they are still the admins of their AWS account. They are limited only by the policies they themselves have put in place - they can just go into their AWS console and change the key policy, and decrypt the encrypted private keys they have for every account.</p><p>Additionally, every time you authenticate on a new device, the raw key is transmitted over HTTPS to your machine. If your machine is being man in the middle&#8217;d, an attacker will be able to see your <em>raw key materials</em> if you sign in. Additionally, because this is in-browser, Magic is not using any form of certificate pinning for AWS.</p><p>This isn&#8217;t ideal. This isn&#8217;t MPC or any sort of advanced cryptography - it&#8217;s just using AWS as your trust layer, and some clever engineering to make the user experience good. I&#8217;m a bit skeptical of their claims that this isn&#8217;t custodial, since Magic technically has the ability to just recover everyone&#8217;s private keys. This might be good enough, though, and the users magic is targeting (web3 gamers and casual users) are&#8217;t likely to care about the security implications.</p>]]></content:encoded></item><item><title><![CDATA[Semantic search in iMessage, iMessage Wrapped, and AI conversations]]></title><description><![CDATA[TLDR - You can download Mimessage on GitHub]]></description><link>https://jonluca.substack.com/p/mimessage</link><guid isPermaLink="false">https://jonluca.substack.com/p/mimessage</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Fri, 14 Apr 2023 04:31:20 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b43c86e6-fbdc-4443-9fe1-bb6c8c6f2d44_800x484.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TLDR - You can <a href="https://github.com/jonluca/mimessage/releases/latest">download Mimessage on GitHub</a></p><p>I&#8217;ve always been surprised at how slow and and clunky it is to search in iMessage. It feels like it doesn&#8217;t search through your whole history, the UI for the results is too small given the prominence of the feature, and there exist quite literally no ways to filter or refine your search.</p><p>I realized that iMessage just stores its database locally as a sqlite file, so I went about building an alternate UI for searching, and adding in a few features that I thought would be interesting. These include:</p><ul><li><p><strong>Semantic Search</strong> - I wanted to create embeddings for every message/conversation and then add proper semantic search on top of it</p></li><li><p><strong>Wrapped</strong> - I really like seeing stats about my life, and I really enjoy what Spotify has done with Wrapped, so I set out to do the same for iMessage</p></li><li><p><strong>AI Conversations</strong> - Talk with your friends, AI-ifiied; once you have all the context around a conversation, you can understand how that person texts and what their tone it. Then it&#8217;s as simple as plugging it into ChatGPT</p></li><li><p><strong>Export</strong> - Export your conversations in JSON or plain text</p></li></ul><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2kCk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2kCk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 424w, https://substackcdn.com/image/fetch/$s_!2kCk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 848w, https://substackcdn.com/image/fetch/$s_!2kCk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 1272w, https://substackcdn.com/image/fetch/$s_!2kCk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2kCk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png" width="4336" height="2622" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fbd8e536-684b-497b-a91a-a34814641072_800x484.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2622,&quot;width&quot;:4336,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;imessage wrapped&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="imessage wrapped" title="imessage wrapped" srcset="https://substackcdn.com/image/fetch/$s_!2kCk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 424w, https://substackcdn.com/image/fetch/$s_!2kCk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 848w, https://substackcdn.com/image/fetch/$s_!2kCk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 1272w, https://substackcdn.com/image/fetch/$s_!2kCk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbd8e536-684b-497b-a91a-a34814641072_800x484.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Your iMessage Wrapped</p><h2>Reverse Engineering the Database Schema</h2><p>The database sits at <code>/Library/Messages/chat.db</code>, while all the attachments live in <code>~/Library/Messages/Attachments</code>. I made a copy of it and then opened it up and datagrip and then was ready to go. The database schema is relatively straightforward to understand - there where <code>message</code>, <code>chat</code>, <code>handle</code>, and <code>attachment</code> tables, each containing the data you&#8217;d expect. A rust library named <a href="https://github.com/ReagentX/imessage-exporter">imessage-exporter</a> was particularly useful for understanding how the tables were joined together and what queries to make.</p><p>A few gotchas were:</p><ul><li><p>iMessage does not receive raw text as its messages - it actually receives a <code>typedstream</code>, and then later on post processes it and backfills in the <code>text</code> column. There&#8217;s a fairly obscure library for parsing these in typescript (with a bug fixed) <a href="https://github.com/jonluca/node-typedstream">here</a></p></li><li><p>When using Messages in iCloud, your messages app won&#8217;t have all your conversations and attachments. This will make your old conversations look much more sparse than they really are. You can force iMessage to download all your conversations by toggling it on and off in iMessage settings.</p></li></ul><h2>Wrapped</h2><p>Wrapped gives you a breakdown of your iMessage habits, broken down by year and by conversation/person.</p><p>I actually used ChatGPT to generate the product features included in iMessage Wrapped - as soon as it can do figma mocks I&#8217;ll redesign the UI using that, as well.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RuGg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RuGg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 424w, https://substackcdn.com/image/fetch/$s_!RuGg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 848w, https://substackcdn.com/image/fetch/$s_!RuGg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 1272w, https://substackcdn.com/image/fetch/$s_!RuGg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RuGg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png" width="1788" height="1908" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1908,&quot;width&quot;:1788,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;chatgpt product suggestions&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="chatgpt product suggestions" title="chatgpt product suggestions" srcset="https://substackcdn.com/image/fetch/$s_!RuGg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 424w, https://substackcdn.com/image/fetch/$s_!RuGg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 848w, https://substackcdn.com/image/fetch/$s_!RuGg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 1272w, https://substackcdn.com/image/fetch/$s_!RuGg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b5a98d8-eeb1-4405-be09-2f8124f22c6b_800x854.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>ChatGPT's suggestions for the features in wrapped</p><h2>AI Conversations</h2><p>I&#8217;ve always thought the amount of information stored in iMessage wasn&#8217;t being used appropriately - I thought that it could learn more about you, or the person you&#8217;re talking to, and suggest more things or be smarter about your interactions.</p><p>One of the first ideas that came to mind when I was building this was to use GPT4 to just continue any conversation, right where you left off.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BO-D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BO-D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 424w, https://substackcdn.com/image/fetch/$s_!BO-D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 848w, https://substackcdn.com/image/fetch/$s_!BO-D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 1272w, https://substackcdn.com/image/fetch/$s_!BO-D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BO-D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png" width="2710" height="1082" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1082,&quot;width&quot;:2710,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Chatting with someone&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Chatting with someone" title="Chatting with someone" srcset="https://substackcdn.com/image/fetch/$s_!BO-D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 424w, https://substackcdn.com/image/fetch/$s_!BO-D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 848w, https://substackcdn.com/image/fetch/$s_!BO-D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 1272w, https://substackcdn.com/image/fetch/$s_!BO-D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32788e6-a322-49e2-9b9d-88dea834eaec_800x319.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>AI generated chat that really can't make it to dinner</p><p>I started building Mimessage on April 1st, and just a few days ago <a href="https://www.izzy.co/blogs/robo-boys.html">I saw someone on hackernews had had the same idea to clone their friends</a>. I think that training a model is actually a much smarter way of accomplishing this, and seems to lead to better results than naively continuing the conversation with GPT4. I&#8217;ll try and get it running locally with LLaMA soon.</p><h2>Better Search</h2><p>I added in global search with filters, as well as a chat specific search that allows you to do either fuzzy matching using <a href="https://fusejs.io/">fuse.js</a> or just writing raw regex in the query itself.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qOuv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qOuv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 424w, https://substackcdn.com/image/fetch/$s_!qOuv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 848w, https://substackcdn.com/image/fetch/$s_!qOuv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 1272w, https://substackcdn.com/image/fetch/$s_!qOuv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qOuv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png" width="1366" height="368" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f2873d48-3b52-4cca-8085-acaf7218003b_800x216.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:368,&quot;width&quot;:1366,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;global search filter&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="global search filter" title="global search filter" srcset="https://substackcdn.com/image/fetch/$s_!qOuv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 424w, https://substackcdn.com/image/fetch/$s_!qOuv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 848w, https://substackcdn.com/image/fetch/$s_!qOuv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 1272w, https://substackcdn.com/image/fetch/$s_!qOuv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2873d48-3b52-4cca-8085-acaf7218003b_800x216.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Global search with filters</p><p>I also created a virtual FTS5 table in the sqlite messages copy, which does <code>MATCh</code> searches ordered by <code>rank</code>, and raw <code>LIKE %QUERY%</code> queries as well. This is really really fast, and a way better text searching experience than iMessage.</p><h3>Semantic Search</h3><p>I also wanted to add semantic search, as those results will often blow pure text searches out of the water. I used OpenAI and ChromaDB to create and store the embeddings for each text message, respectively.</p><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!y2xa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!y2xa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 424w, https://substackcdn.com/image/fetch/$s_!y2xa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 848w, https://substackcdn.com/image/fetch/$s_!y2xa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 1272w, https://substackcdn.com/image/fetch/$s_!y2xa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!y2xa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png" width="2280" height="2608" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2608,&quot;width&quot;:2280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;semantic search&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="semantic search" title="semantic search" srcset="https://substackcdn.com/image/fetch/$s_!y2xa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 424w, https://substackcdn.com/image/fetch/$s_!y2xa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 848w, https://substackcdn.com/image/fetch/$s_!y2xa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 1272w, https://substackcdn.com/image/fetch/$s_!y2xa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa2d059-2b38-4c25-8b6a-79b4b6361ca7_800x915.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><p>Enabling semantic search on top of imessage</p><p>This takes quite a while and costs money, as well as requiring you to send your data off to OpenAI, so I&#8217;m planning on migrating to an entirely on device version soon.</p><h2>Yak shaving the actually cool part</h2><p>The coolest part of this project was actually in using a tool I created for this project called <code>repo-refactor</code> - it&#8217;s a library that will convert a github repo from one language to another https://github.com/jonluca/repo-refactor. The rust library I mentioned above had already done some of the heavy lifting in creating data models and structures representing the schema, as well as writing some of the base queries. This was really useful for creating and understanding types early on, and for actually reading the rust code (admittedly not my forte).</p><p>The tool is very much in its infancy, and works successfully somewhere between 40 and 80 percent of the time. It doesn&#8217;t do great on long files, and some clear failure modes (going from weakly typed -&gt; strongly typed is pretty bad), but I think there are some cheap techniques that can be implemented to make it way more accurate. It&#8217;s still pretty incredible that it works this well with very little prompt engineering or manual cleanup.</p><h2>Next Steps</h2><p>The whole project was built in open source <a href="https://github.com/jonluca/mimessage">here</a> and you can grab a copy of the latest working code from the <a href="https://github.com/jonluca/mimessage/releases/latest">releases page</a>. I want to get the inference running entirely locally, probably with alpaca or with vicuna weights, so that it&#8217;s free and privacy preserving.</p><p>I also want to add more stats to the wrapped page, and make it more of an experience like Spotify&#8217;s is - if there&#8217;s any designer reading this that wants to collaborate on it, <a href="hi@jonlu.ca">shoot me an email</a></p>]]></content:encoded></item><item><title><![CDATA[Getting a vanity phone number with 4 repeating digits]]></title><description><![CDATA[I find that it&#8217;s pretty useful to have access to multiple phone numbers.]]></description><link>https://jonluca.substack.com/p/verizon-rare-numbers</link><guid isPermaLink="false">https://jonluca.substack.com/p/verizon-rare-numbers</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sat, 28 May 2022 15:06:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!O4je!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I find that it&#8217;s pretty useful to have access to multiple phone numbers. Any time a site offers a discount if you give them their phone number, or when you want to be more anonymous online and a service needs a number, it&#8217;s nice to have a number that&#8217;s more than a burner but not your main number. I had heard of the Verizon My Numbers service and thought it would be cool to have a few extra numbers for cases like these. I also wanted a number with multiple consecutive digits, both because it was easier to remember and for the cool factor.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!O4je!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!O4je!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 424w, https://substackcdn.com/image/fetch/$s_!O4je!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 848w, https://substackcdn.com/image/fetch/$s_!O4je!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 1272w, https://substackcdn.com/image/fetch/$s_!O4je!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!O4je!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/d340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Verizons search UI&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Verizons search UI" title="Verizons search UI" srcset="https://substackcdn.com/image/fetch/$s_!O4je!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 424w, https://substackcdn.com/image/fetch/$s_!O4je!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 848w, https://substackcdn.com/image/fetch/$s_!O4je!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 1272w, https://substackcdn.com/image/fetch/$s_!O4je!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd340e54a-a42f-41e7-bc53-72a1f3130de4_800x963.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>Their UI was pretty bad, and it was quite slow. It also wouldn&#8217;t let you search without an area code, and the majority of area codes that I tried said that they didn&#8217;t have any numbers associated with them. I figured it would be faster to do this programatically.</p><h2>Finding the endpoints</h2><p>I used burpsuite to find out which API endpoint Verizon was hitting to fetch the available phone numbers.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!msu5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!msu5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 424w, https://substackcdn.com/image/fetch/$s_!msu5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 848w, https://substackcdn.com/image/fetch/$s_!msu5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 1272w, https://substackcdn.com/image/fetch/$s_!msu5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!msu5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Verizons search endpoint&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Verizons search endpoint" title="Verizons search endpoint" srcset="https://substackcdn.com/image/fetch/$s_!msu5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 424w, https://substackcdn.com/image/fetch/$s_!msu5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 848w, https://substackcdn.com/image/fetch/$s_!msu5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 1272w, https://substackcdn.com/image/fetch/$s_!msu5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb64630fd-b93b-4ba0-9040-3082bceef9ac_800x564.png 1456w" sizes="100vw"></picture><div></div></div></a><p>From here I could just export it to code - my preferred method is to copy the request as curl from within BurpSuite and then use <a href="https://curlconverter.com/">a tool like CurlConverter</a> to turn it into the language of choice, although BurpSuite does have plugins that do this natively as well.</p><h2>Calling the API programatically</h2><p>Curl converter gave me a nice little code snippet that I could use. I put it into a jupyter notebook and was happy to see it all worked. Their endpoint didn&#8217;t seem to use any signing or on-device auth that would&#8217;ve made this difficult, besides a static basic auth token as an HTTP header.</p><pre><code>import requests

headers = {
    'Host': 'api-v.vzmessages.com',
    'Cache-Control': 'no-cache',
    'Connection': 'close',
    'Accept': '*/*',
    'User-Agent': 'Verizon%20My%20Numbers/1 CFNetwork/1333.0.4 Darwin/21.5.0',
    'Accept-Language': 'en-US,en;q=0.9',
    'Authorization': 'Basic &lt;Auth-Token&gt;',
}

params = {
    'state': 'CA',
    'acode': '213',
}

response = requests.get('https://api-v.vzmessages.com/VirtualNumber/listOfMVNs/US', params=params, headers=headers, verify=False)
</code></pre><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ekno!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ekno!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 424w, https://substackcdn.com/image/fetch/$s_!Ekno!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 848w, https://substackcdn.com/image/fetch/$s_!Ekno!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 1272w, https://substackcdn.com/image/fetch/$s_!Ekno!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ekno!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/adcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Verizons search endpoint response&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Verizons search endpoint response" title="Verizons search endpoint response" srcset="https://substackcdn.com/image/fetch/$s_!Ekno!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 424w, https://substackcdn.com/image/fetch/$s_!Ekno!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 848w, https://substackcdn.com/image/fetch/$s_!Ekno!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 1272w, https://substackcdn.com/image/fetch/$s_!Ekno!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fadcfc6f3-a026-41e7-a9ed-eaa741409878_576x158.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>It seemed like multiple calls to this endpoint with the same params gave different replies, which meant it was non deterministic.</p><p>It also seemed like if you removed the params, it would remove the filter and return back many more numbers - I didn&#8217;t <em>really</em> care what the area code was, so removing the params should make it easier to find a number I liked.</p><p>I wrote a quick script to repeatedly call the API and try and find a number with at least 3 consecutive numbers.</p><pre><code>def contains_cons(num):
    for i in range(len(num) - 2):
        if num[i] == num[i + 1] and num[i + 1] == num[i + 2]:
            return True
    return False
</code></pre><p>and then just kept calling that infinitely.</p><pre><code>seen = set()
while True:
    response = requests.get('https://api-v.vzmessages.com/VirtualNumber/listOfMVNs/US', headers=headers)
    nums = response.json()
    for num in nums:
        if num in seen:
            continue
        seen.add(num)
        is_valid = contains_cons(num)
        if is_valid:
            print(num)
            break
</code></pre><h2>Finding valid numbers</h2><p>I let this run for a few minutes and checked back and found that there were quite a few good candidates.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GA1o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GA1o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 424w, https://substackcdn.com/image/fetch/$s_!GA1o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 848w, https://substackcdn.com/image/fetch/$s_!GA1o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 1272w, https://substackcdn.com/image/fetch/$s_!GA1o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GA1o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Verizons numbers with consecutive digits&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Verizons numbers with consecutive digits" title="Verizons numbers with consecutive digits" srcset="https://substackcdn.com/image/fetch/$s_!GA1o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 424w, https://substackcdn.com/image/fetch/$s_!GA1o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 848w, https://substackcdn.com/image/fetch/$s_!GA1o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 1272w, https://substackcdn.com/image/fetch/$s_!GA1o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F37c7a231-298f-40ae-9537-7bc6186bc13e_800x413.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>These are the numbers that had at least three consecutive digits (along with the number I actually ended up using, blacked out).</p><h2>Purchasing the number</h2><p>I went through and bought a dummy number to figure out how to purchase the number programatically, and then did the same method as above to purchase it. Luckily there weren&#8217;t any unique IDs or anything, I could just swap out the number and it would work.</p><pre><code>import requests

headers = {
    'Host': 'api-v.vzmessages.com',
    'User-Agent': 'Verizon%20My%20Numbers/1 CFNetwork/1333.0.4 Darwin/21.5.0',
    'Connection': 'close',
    'Accept': '*/*',
    'Accept-Language': 'en-US,en;q=0.9',
    'Cache-Control': 'no-cache',
}

json_data = {
    'countryCode': 'US',
    'zipcode': '&lt;your billing zipcode',
    'deviceId': '&lt;your device id&gt;',
    'contentId': 'Content2',
    'price': '15',
    'mvn': '&lt;number you want&gt;',
    'mdn': '&lt;your hard line number&gt;',
}

response = requests.post('https://api-v.vzmessages.com/VirtualNumber/purchase', headers=headers, json=json_data)
</code></pre><h2>Success</h2><p>Using this I was able to purchase the number, and now it shows up in the app. One drawback is that it&#8217;s not really integrated fully in your phone - for instance, iMessages to that number won&#8217;t work, and the calls need to be made through the app. It&#8217;s still nice to have, and a nice number to give out when you don&#8217;t want to give out your real one.</p>]]></content:encoded></item><item><title><![CDATA[Reversing Resy’s API to create a javascript client]]></title><description><![CDATA[A couple months ago I made a post about building an OpenTable bot, which lets me monitor and will automatically book reservations that are hard to get or currently sold out.]]></description><link>https://jonluca.substack.com/p/resy-api</link><guid isPermaLink="false">https://jonluca.substack.com/p/resy-api</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Mon, 09 May 2022 03:14:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Lehf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A couple months ago I made <a href="/posts/opentable">a post about building an OpenTable bot</a>, which lets me monitor and will automatically book reservations that are hard to get or currently sold out. Well, recently I wanted to go to Le Bernardin (in New York) and Amex wasn&#8217;t able to help, so I thought it would be a good reason to build something similar for Resy.</p><p>Luckily Resy&#8217;s APIs were fairly straightforward - their web and mobile APIs were very similar, an didn&#8217;t seem to have any rate limiting.</p><h2>Notifications</h2><p>Resy already supports &#8220;priority notifications&#8221;, where you can sign up for notifications for specific restaurants.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Lehf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Lehf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 424w, https://substackcdn.com/image/fetch/$s_!Lehf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 848w, https://substackcdn.com/image/fetch/$s_!Lehf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 1272w, https://substackcdn.com/image/fetch/$s_!Lehf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Lehf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/ebf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Resys notification popup&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Resys notification popup" title="Resys notification popup" srcset="https://substackcdn.com/image/fetch/$s_!Lehf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 424w, https://substackcdn.com/image/fetch/$s_!Lehf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 848w, https://substackcdn.com/image/fetch/$s_!Lehf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 1272w, https://substackcdn.com/image/fetch/$s_!Lehf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Febf13b8d-c760-48eb-9196-dd9a3d9df568_800x367.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>However, there are a few issues with this.</p><ol><li><p>You have to manually set these up for each date you&#8217;re interested in</p></li><li><p>These won&#8217;t automatically book them for you - if availability comes up when you&#8217;re not near a device to book it, you&#8217;ll probably miss out</p></li><li><p>I&#8217;m not sure how quickly they actually push these out. I <em>think</em> there&#8217;s some delay to allow their concierge team to nab reservations first</p></li></ol><p>I wanted to build a full end to end flow which will make both notify you that a reservation is available and book it for you.</p><h2>Clients</h2><p>I started by checking out their front end code and trying to de-minify it.</p><p>Interestingly, the webpack config Resy is using isn&#8217;t mangling their code or removing comments, so it makes directly using their client fairly straightforward. You can rip code segments like these almost one to one.</p><pre><code>getReservations(params) {
  return RequestBuilder.get({
    endpoint: "4/find",
    data: {
      lat: 0,
      long: 0,
      day: params.day,
      party_size: params.party_size,
      venue_id: params.venue_id,
      resy_token: params.resy_token,
    },
  }).then((response) =&gt; ResyTransformer.finder4(response));
},
</code></pre><p>There was also some classic hacky code, like specifically excluding some assets from the UI when they match a property ID.</p><pre><code>function getServiceTypes(serviceTypes, venue, venueId) {
  // a default for a service type that is not an available service type for the venue
  const temp = [
    {
      available: [],
    },
  ];
  serviceTypes.forEach((service) =&gt; {
    // EXCEPTIONS - lol
    // Noble Experiment doesn't serve dinner.
    if (venueId === 569 &amp;&amp; service.value === "dinner") {
      temp[temp.length - 1].serviceTypeName = "Cocktails";
    }
  });
  // Robert Sinskey Vineyards doesn't serve food either (885)
  // and one more time because europe The Mulwray UK (2040)
  if (venueId === 885 || venueId === 2040) {
    temp.forEach((item) =&gt; {
      item.serviceTypeName = "";
    });
  }
  return temp;
}
</code></pre><p>and some nice copy + paste</p><pre><code>// shamelessly stolen from https://toddmotto.com/understanding-javascript-types-and-reliable-type-checking/
[
  "Array",
  "Object",
  "String",
  "Date",
  "RegExp",
  "Function",
  "Boolean",
  "Number",
  "Null",
  "Undefined",
  "Window",
].forEach((type) =&gt; {
  Utils["is".concat(type)] = (
    (self) =&gt; (elem) =&gt;
      Object.prototype.toString.call(elem).slice(8, -1) === self
  )(type);
});
</code></pre><p>Their API is pretty full featured and clear, though, and understanding how their availability and searching worked was easy.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!o6mW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!o6mW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 424w, https://substackcdn.com/image/fetch/$s_!o6mW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 848w, https://substackcdn.com/image/fetch/$s_!o6mW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 1272w, https://substackcdn.com/image/fetch/$s_!o6mW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!o6mW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Resys network response&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Resys network response" title="Resys network response" srcset="https://substackcdn.com/image/fetch/$s_!o6mW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 424w, https://substackcdn.com/image/fetch/$s_!o6mW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 848w, https://substackcdn.com/image/fetch/$s_!o6mW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 1272w, https://substackcdn.com/image/fetch/$s_!o6mW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F79412ba8-c936-4424-9d0b-19ffdc2d4323_800x296.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h2>Hooking it all up</h2><p>I wrote a few small services for wrapping their code and logging the user in to get their authentication token, as well as for sending a text message with twilio. This will refresh my auth every day, and fetch availability every 5 minutes.</p><pre><code>const refreshAvailability = async () =&gt; {
  log.info("Finding reservations");

  await venuesService.init();
  const venuesToSearchFor = await venuesService.getWatchedVenues();
  // You get more availability if you have an amex card and you log in

  for (const venue of venuesToSearchFor) {
    await refreshAvailabilityForVenue(venue);
  }
  await venuesService.save();
  log.info("Finished finding reservations");
};

const regenerateHeaders = async () =&gt; {
  await service.generateHeadersAndLogin();
};
// every day fetch every post
cron.scheduleJob("*/5 * * * *", refreshAvailability);
cron.scheduleJob("1 * * * *", regenerateHeaders);

regenerateHeaders().then(() =&gt; {
  refreshAvailability();
});
</code></pre><p>And then the logic for actually understanding if the dates work</p><pre><code>const refreshAvailabilityForVenue = async (venue: VenueToWatch) =&gt; {
  try {
    const availableDates = await service.getAvailableDatesForVenue(
      venue.id,
      venue.partySize
    );
    if (!availableDates.length) {
      return;
    }
    for (const dateToCheck of availableDates) {
      const slots = (await service.getAvailableTimesForVenueAndDate(
        venue.id,
        dateToCheck.date,
        venue.partySize
      )) as EnhancedSlot[];

      const possibleSlots = slots.filter((slot) =&gt; {
        const start = dayjs(slot.date.start);
        const minTime = dayjs(`${start.format("YYYY-MM-DD")} ${venue.minTime}`);
        const maxTime = dayjs(`${start.format("YYYY-MM-DD")} ${venue.maxTime}`);
        slot.start = start;
        return start &gt;= minTime &amp;&amp; start &lt;= maxTime;
      });

      if (possibleSlots.length) {
        await parsePossibleSlots(venue, possibleSlots);
        return;
      }
    }
    log.debug(`Found no valid slots for ${venue.name}`);
  } catch (e) {
    console.error(e);
  }
};
</code></pre><h2>Setting up the monitoring</h2><p>I wanted some form of persistence so that if a restaurant released a lot of availability all at once my script wouldn&#8217;t rebook the same restaurant over and over. I used <a href="https://npmjs.com/package/lowdb">lowdb</a>, a small JSON based &#8220;database&#8221; that stores your data in a file. I wrote a simple schema that you fill out, like what your time ranges are and what your preferred time is, and whether it should attempt to book it for you.</p><pre><code>{
  "venues": [
    {
      "name": "Le Bernardin",
      "id": 1387,
      "notified": true,
      "minTime": "18:00",
      "preferredTime": "19:30",
      "maxTime": "22:00",
      "shouldBook": true,
      "uuid": "7088f322-578d-4b10-816f-44c4213118dd",
      "partySize": 2
    }
  ]
}
</code></pre><h2>Success</h2><p>A couple days later it worked, and I got a booking at Le Bernardin for my girlfriend and I.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MqRq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MqRq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 424w, https://substackcdn.com/image/fetch/$s_!MqRq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 848w, https://substackcdn.com/image/fetch/$s_!MqRq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 1272w, https://substackcdn.com/image/fetch/$s_!MqRq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MqRq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/df6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Resys successful reservation at Le Bernardin&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Resys successful reservation at Le Bernardin" title="Resys successful reservation at Le Bernardin" srcset="https://substackcdn.com/image/fetch/$s_!MqRq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 424w, https://substackcdn.com/image/fetch/$s_!MqRq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 848w, https://substackcdn.com/image/fetch/$s_!MqRq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 1272w, https://substackcdn.com/image/fetch/$s_!MqRq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf6077e9-acbe-48f8-aa88-c1a1c1d4b6dd_800x430.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Last one left to do now is Tock!</p><h2>Code</h2><p>The code is open source on GitHub <a href="https://github.com/jonluca/resy-api">here</a>. It&#8217;s probably in a state that a software engineer could get it working with the right environment variables, but it&#8217;s not necessarily ready for anyone to be able to use it out of the box. PRs are welcome though!</p>]]></content:encoded></item><item><title><![CDATA[Web3, Free Candy, and exploits galore]]></title><description><![CDATA[On 1/4/22, nearly 4000 Solana NFT projects were drained of their funds due to a reinitialization bug present in the Candy Machine v1 smart contract on Solana.]]></description><link>https://jonluca.substack.com/p/candy-machine</link><guid isPermaLink="false">https://jonluca.substack.com/p/candy-machine</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sat, 26 Feb 2022 23:59:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4feC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On 1/4/22, nearly 4000 Solana NFT projects were drained of their funds due to a reinitialization bug present in the Candy Machine v1 smart contract on Solana. The account, <a href="https://solscan.io/account/cHfYkrVAwfEoe3Mr2GbvzpNQJboDL6AiBoFZDsf8dxj">cHfYkrVAwfEoe3Mr2GbvzpNQJboDL6AiBoFZDsf8dxj</a>, converted 1,027 SOL into 155k USDC using Raydium, and then transferred the USDC into their FTX account. The vulnerability was patched while the attack was actively going on, at 6:20am on 1/4/22.</p><p>This investigation uncovered similar vulnerabilities in NFT exchanges, yet to be publicized.</p><h2>Background</h2><p>Metaplex&#8217;s <a href="https://docs.metaplex.com/candy-machine-v2/introduction">Candy Machine</a>, a Solana program which handles the logistics of NFT issuance, just launched last September. You instantiate it with their CLI, feed it your images, and it handles the rest. It will deal with all the technically complex parts of putting the images on chain and creating the smart contracts to mint them to the buyers.</p><p>It&#8217;s extremely simple to launch an NFT sale with Metaplex; you choose the price you want to set, the timing of the collection drop and any other configs - it handles the rest and mints right to recipients wallets.</p><p>This simplicity greatly lowered the barrier to entry - you didn&#8217;t need to have any Rust knowledge or Solana API experience to use it. When it first came out it led to a huge increase in NFT collections.</p><p>Since its inception, over 14,800 candy machines have been created, each corresponding to an NFT collection.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4feC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4feC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 424w, https://substackcdn.com/image/fetch/$s_!4feC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 848w, https://substackcdn.com/image/fetch/$s_!4feC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 1272w, https://substackcdn.com/image/fetch/$s_!4feC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4feC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Candy Machine program&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Candy Machine program" title="Candy Machine program" srcset="https://substackcdn.com/image/fetch/$s_!4feC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 424w, https://substackcdn.com/image/fetch/$s_!4feC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 848w, https://substackcdn.com/image/fetch/$s_!4feC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 1272w, https://substackcdn.com/image/fetch/$s_!4feC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3f52d64c-5612-49b2-8c13-7bcbc4a9fe07_800x625.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><h2>Impact</h2><p>The goal of this research was to identify how the attacker exploited the vulnerability, trace the funds and their total dollar denominated value, and then to determine which projects were impacted.</p><p>The attacker targeted 4,410 of the 14,800 candy machines that were created at the time. I&#8217;m guessing they didn&#8217;t target every vulnerable program because they had trouble pulling the historical candy machine creation records.</p><p>They fired off withdrawal transactions that took advantage of the reinitialization bug over the period of an hour.</p><p>The withdrawal transactions lasted between <a href="https://solscan.io/tx/coSeMNsGKebMGP1vqPZcEbu6rYiF4BbCRrtBRNLFi4TbMo3Psd7KZyvDTPv6KyeqZNDyMVU3o6D3rgQPG1aV94J">5:57am</a> and <a href="https://solscan.io/tx/3zhZDtCV2vr5fSG2TxEjXXTdMmfk8rfnM4mNAavKdZM1Cy6627hN8vDnu7gaUk6oPmzLLcacJpTopK1bsscX9MbB">6:49am</a> EST on January 4th 2022. At <a href="https://solscan.io/tx/3zhZDtCV2vr5fSG2TxEjXXTdMmfk8rfnM4mNAavKdZM1Cy6627hN8vDnu7gaUk6oPmzLLcacJpTopK1bsscX9MbB">6:20am</a>, the patched contract was deployed, causing every subsequent transaction by the attacker to fail.</p><p>Of the 4,410 candy machines targeted, 3,470 were completely drained. The vulnerability didn&#8217;t give the attacker permanent control of the candy machines - only for the duration of that transaction, which means that the candy machines that were impacted are not currently vulnerable.</p><p>Some of the notable projects impacted by this vulnerability are SolSteads, Contrastive, and Degen Ape Society, with a full list below.</p><h2>Vulnerability</h2><p>The bug was subtle - the attacker was injecting pre-initialized accounts and the program was not checking if the account had already been initialized, meaning an attacker could populate their own address as the authority of the contract.</p><p><a href="https://github.com/metaplex-foundation/metaplex/commit/4ddc13ea29070172f358e054baa9d4c47687a26b">The fix itself was fairly straightforward.</a></p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xegt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xegt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 424w, https://substackcdn.com/image/fetch/$s_!Xegt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 848w, https://substackcdn.com/image/fetch/$s_!Xegt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 1272w, https://substackcdn.com/image/fetch/$s_!Xegt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xegt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Fix for the vulnerability&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Fix for the vulnerability" title="Fix for the vulnerability" srcset="https://substackcdn.com/image/fetch/$s_!Xegt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 424w, https://substackcdn.com/image/fetch/$s_!Xegt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 848w, https://substackcdn.com/image/fetch/$s_!Xegt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 1272w, https://substackcdn.com/image/fetch/$s_!Xegt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F90afb9aa-24b3-41a5-b4e3-a72c218f9176_800x315.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>The hack seems fairly unsophisticated - the damage this vulnerability could do was pretty high, as the bug effectively allowed any account to control the Candy Machine. The attacker submitted the transactions slowly, and would probably have been able to capture the entirety of the vulnerable set of candy machines had they submitted the transactions through their own RPC pool without rate limits.</p><p>What&#8217;s also interesting about the fix is that it was <a href="https://github.com/metaplex-foundation/metaplex/commit/e9ef376443c3c8fd2f5b151dd0b09f757b1bf35c">actually fixed in code on December 31st for Candy Machine v2</a>, but the CMv1 contract wasn&#8217;t redeployed until it was actively being exploited.</p><h2>Fund extraction</h2><p>The attacker used Serum DEX and RaydiumSwapV2 to convert the SOL to USDC, then sent the USDC to a FTX address. It should be fairly easy to reverse their idea from FTXs end if they&#8217;ve KYC&#8217;d properly.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HAew!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HAew!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 424w, https://substackcdn.com/image/fetch/$s_!HAew!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 848w, https://substackcdn.com/image/fetch/$s_!HAew!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 1272w, https://substackcdn.com/image/fetch/$s_!HAew!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HAew!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Withdrawal transaction&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Withdrawal transaction" title="Withdrawal transaction" srcset="https://substackcdn.com/image/fetch/$s_!HAew!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 424w, https://substackcdn.com/image/fetch/$s_!HAew!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 848w, https://substackcdn.com/image/fetch/$s_!HAew!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 1272w, https://substackcdn.com/image/fetch/$s_!HAew!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F97c6a6aa-a63d-4576-bd36-007d26ac7c9b_800x434.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h2>Candy Machine</h2><p>Candy Machine v1 is now deprecated, and any new candy machines created should be v2s. <a href="https://docs.metaplex.com/candy-machine-v2/introduction">From their docs</a>:</p><blockquote><p>The second iteration of the well-known Candy Machine, a fully on-chain generative NFT distribution program, provides many improvements over its predecessor. The new version also allows you to create a whole new set of distribution scenarios and offers protection from bot attacks, while providing the same easy-to-use experience.</p></blockquote><h2>Research</h2><p>Querying for historical data on chain in Solana is a time consuming process. I tried doing research in jupyter notebook at first, but the volume of data made it hard to parse and query.</p><p>I ended up cloning the historical transactions into a local database, and indexing that for faster queries.</p><pre><code>export class MongoClient {
  init = async () =&gt; {
    log.info("Connecting...");
    await connect("mongodb://127.0.0.1:27017", {
      keepAlive: true,
      keepAliveInitialDelay: 300000,
      dbName: "candymachine",
      minPoolSize: 50,
      maxPoolSize: 500,
    });
    log.info("Connected to mongo db");
  };

  saveHashes = async (hashes: object[]) =&gt; {
    log.debug("Saving batch...");
    try {
      await Txhashes.insertMany(hashes, { ordered: false });
    } catch (e: any) {
      // ignore dup key errors
      if (!e.message.includes("E11000")) {
        log.error(e);
      }
    }
    const count = await Txhashes.count();
    log.debug(`Saved batch - ${count} total documents`);
  };

  getHashes = async (filter: FilterQuery&lt;typeof Txhashes&gt; = {}) =&gt; {
    const docs = await Txhashes.find(filter).limit(25000);
    return docs;
  };
}
</code></pre><p>I first cloned all the transaction hashes into Mongo - I set up a connection pool of various RPCs to accomplish this, as there&#8217;s no way of getting it from the Solana mainnet-beta RPC in a reasonable amount of time.</p><pre><code>const history = await con.getSignaturesForAddress(
  new PublicKey(publicKey),
  options
);

for (const c of chunk(history, 100000)) {
  await mc.saveHashes(c);
  log.info("Completed chunk");
}
</code></pre><p>Then, after fetching all the hashes, I would clone the parsed transaction details into Mongo</p><pre><code>const run = async () =&gt; {
  await mc.init();
  log.info("Fetching hashes");
  let hashesToFetch = await mc.getHashes({ tx: null });
  log.info("Fetched hashes");
  let i = 0;
  while (hashesToFetch.length) {
    let isNearEnd = hashesToFetch.length &lt; 10000;
    const chunkSize = isNearEnd ? 10 : 200;
    if (isNearEnd) {
      shuffle(hashesToFetch);
    }
    const chunkedHistory = chunk(hashesToFetch, chunkSize) as string[][];
    const processChunk = async (hashes: any[]) =&gt; {
      let message = `Fetched txs ${i}`;
      let savedTxMessage = `Saved txs ${i}`;
      i++;

      console.time(message);
      const hashMap = {};
      hashes.forEach((hash) =&gt; {
        if (hash.signature) {
          hashMap[hash.signature] = hash;
        } else {
          console.error("what");
        }
      });

      try {
        const { c, tx: txs } = await fetchTxsWithFallbackWithConnection(
          hashes.map((p) =&gt; p.signature)
        );

        console.timeLog(message, c?._rpcEndpoint);
        console.timeEnd(message);
        let failureCount = 0;
        txs.forEach((tx) =&gt; {
          if (!tx) {
            failureCount++;

            return;
          }
          tx.transaction.signatures.forEach((s) =&gt; {
            if (hashMap[s]) {
              hashMap[s].tx = tx;
            }
          });
        });
        if (failureCount) {
          console.error(`Invalid txs: ${failureCount}`);
        }
        console.time(savedTxMessage);
        await Txhashes.bulkSave(hashes);
        console.timeEnd(savedTxMessage);
      } catch (e) {
        return;
      }
    };
    try {
      const promises = chunkedHistory.map((h) =&gt; limit(() =&gt; processChunk(h)));
      await Promise.all(promises);
    } catch (e) {
      console.error(e);
      console.error(
        `Chunk failed with total history length of ${hashesToFetch.length}`
      );
    }
    console.log("Finished chunk");
    log.info("Fetching hashes");
    hashesToFetch = await mc.getHashes({ tx: null });
    log.info("Fetched hashes");
  }
};
</code></pre><p>I also pulled every transaction (legitimate and the attackers) that called the withdraw function on the candy machines.</p><p>Of the 14,800 candy machines, 11,848 have had the withdraw function executed on them. The top accounts associated with these functions are below.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j4LC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j4LC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 424w, https://substackcdn.com/image/fetch/$s_!j4LC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 848w, https://substackcdn.com/image/fetch/$s_!j4LC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 1272w, https://substackcdn.com/image/fetch/$s_!j4LC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j4LC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Top withdrawer callers&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Top withdrawer callers" title="Top withdrawer callers" srcset="https://substackcdn.com/image/fetch/$s_!j4LC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 424w, https://substackcdn.com/image/fetch/$s_!j4LC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 848w, https://substackcdn.com/image/fetch/$s_!j4LC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 1272w, https://substackcdn.com/image/fetch/$s_!j4LC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b552daf-ab0b-486a-94be-3d5a7896c8f8_800x448.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Only <code>cHfYkrVAwfEoe3Mr2GbvzpNQJboDL6AiBoFZDsf8dxj</code> seems to be doing this maliciously - the other accounts are all calling legitimate withdraw functions.</p><p><code>F9fER1Cb8hmjapWGZDukzcEYshAUDbSFpbXkj9QuBaQj</code> actually seems to have created over 2,000 candy machines, and then attempted to call withdraw on them, single handedly creating ~14% of all candy machines on Solana.</p><h2>[Redacted Pending Vulnerability Disclosure]</h2><p>[Redacted pending vulnerability disclosure of Solana exchange]</p><h3>[Redacted Pending Vulnerability Disclosure]</h3><p>[Redacted pending vulnerability disclosure of Solana exchange]</p><h2>Identified Projects</h2><p>I went through and fetched the public keys to all known affected projects and tried to map them back to the hacked machines. I identified 334 unique projects that were actively listed on Magic Eden, Solanart, DigitalEyez, and Solsea as being affected.</p><p>012Funksaicy 0xDRIP 100Radials 169 Pixel Gang 3D Flowers A Pixel Art Abstergo Aeterna Civitas Afrobubble AI: Baby Bots Aircrafts AIRDOGS Alphabet Originals Angomon Angry BaboonS Angry Bunny Club Angry Citizens AngryWorms Anti Artist Club ApexDucks Halloween Arcade &#8216;88 Art by NRG &#8211; Pop Sushi Artificial Irrelevants Autistic Reindeer Herd Baby Ape Social Club Baby Bait Baby Frogs Baby Goblin Bad Bromatoes BADBOYS BalisariNFT Bannurs Bare Bones Society: The Kingdom Of Secrets Beat Drops V1 Boat Boys BoldFrames Boopie Gen 1 Bored Ape Social Club Boss Babes Broken Robot Burger Bar bugbearz Bunny Warriors CASSETS Audio CatPunk CATPUNK OG PASS Cats Club NFT Chickenz ChihuahuaSol Classic Art Mashups Coherence Combat Women NFT Contrastive Crazy Pickles Creepy Girls Crypto Greeks Crypto Idolz - Faces CryptoCream CryptoCubs CryptoCubs Mutants Cryptone&#8482; Cryptonic Creations CryptoRock CryptoTeds CryptZero Season 2 - Ghosts Cult of Meerkats CyberKeys Danger Valley Danuki Dojo Deadass Deeps Degen Ape Society DegenEggs Degeneggs - Gen 2 Desolates Metaverse Dessert Girls DigiLife Dinos Zone Dippy Dragons NFT Doll Society Dragon Eggs NFT Dragon Slayerz Dreamland Monkeys Element Art NFT 2D Enigma Expanses enviro Enviro Epoch Labs Ethereans Eyeballz Eyes WTF Fallen Traveler Fast Food Thugbirdz Finefolk Floppy Disk Nft Flutter Fractures G.O.A.T. Collection Galactic Goose GAMEKIDZ GANder g0 Gem Heroes Ghostface Ghostly Sols Ghoulie Gang Gremlins NFT Hallow Birds Happy Pups Hellish Party Boollons Hello World! NFT Hemp Heroes and Villains: PASS High Roller Hippo Clique HOAG play Honorary Space Bums Hot Bunnies NFT Houses Of pixel Iceland - 0xDRIP Icy Bearz NFT Idle Planets - Autumn 2021 Moon Idle Planets - Holiday 2021 Moon Infamous Apes InnerMind Intersolar iTrading_Bot Jelly Beasts Jingle Monkeys Joeian&#8217;s Collection JOSEPHTAYLOR.ART: CRYSTAL BEAMS Jungle Cats Just Blocks Kitten Coup Knightdom KoreanPunkz Krunk Roach NFT Kyoudai Academy: Solana Arcade Games Labyrinth Lanabots Lazy Heroes Little Noots Los Cactus Hermanos LuchaLucha NFT Lucky Kittens LuxAI Mad Vikings Arms Collection Magic Solana Shits Make your own NFT Mark McKenna&#8217;s Heroes &amp; Villains: Origins Megumi Meta Homes Metadroids Metakatz metaSpheres MetaSpheres Mickey Degods Millionaire Apes Club Mindfolk Mini Royale: Nations - Season 1 (Premium) Mob Monkettez MogulWars Monkey Ball MonkeyBall Gen Zero My name Is Sol My Name is Sol Myopa Mystic Potion Neopets Metaverse NFT Poetry NFTrees Solana NGMIPandas Nifty Nanas NON-FUNGIBLE &#8220;BEES&#8221; Nyan Heroes OGbottles Oink Club NFT OinkClub NFT PEEPS Pengu Love Personify Pesky Ice Cube Phantom Pilgrim Society PIMP MY THUG Pirates of Sol Bay - Bottles Pirates of Sol Bay - Treasures pitcrew Pix World NFT Pixel Island NFT PixelWorms PixWorldNFT Platypusol Family Playground Waves Playground: Waves PopsicleNFT Posh Dolphs Powder Heroes Prickly Pete&#8217;s Platoon - OG Cactoon Series PSY Network | PlanetZ Pudgy Pigeons Rabbit Punks Realm Kings Red paperclip RowdyRex Rug Toadz Ruled by Randomness: The Genesis SantaClaus Savage Dray by Squeak Brigade SavagesTotsys SawBunny Secret Duck Society SGF United Shadowy Super Coder Shadowy Super Coder DAO SharkBros shatteredmarble ShroomZ Slimeballz sLoot Smileys Smolpenguins Snek Gang soAlien SocksOnSolana Sol Diamond Hands Sol Lions SOL NFL PLAYER&#8217;S SOL NFL Players SOL Parasites Sol Slugs Sol Tamagotchi Sol Tapes SOLadies Solagon SolAlbums Solamids Solana Baby Monkey Business Solana Bananas Solana Birbs Solana Bros Solana Cat Gang Solana Fan The Game of Squid Solana Feline Business Solana Havana Cigar Club Solana Locks Solana Mystery Box Items Solana Mystery Items Solana Pickles Solana Reversed Monkey Business Solana Robot Business Solana Samurai Journey Solana Slugs SOLANA SUPERCAR CLUB Solana Surfers Solana Tactical RPG STACC solanabets | The Clique NFT SolArc NFT Solarnauts: Mission Bravo SolBlocks Solbusters Solccoons SolCrocos Soldalas SolDice SOLDIER RABBITS SolEmoji Solez SolFoxes SolGalaxy SolGangsta Solloons Sollyfish Solmon Solmoverse: Collection 0 Solmushies SOLNANA SolNauts SolOrbs Solryx SolSlimes SolSneakers Solsteads Surreal Estate SolStoners SolTowers Solutions Solvaders SolWatchers Soul Dogs Soulofox Space Bums Space Bums: Galaxy Mint Pass Spiderverse Spirits of Solana Squareheadz Squid Society Squirrelz Stash StratosNFT Structs Superballz Surging Bulls Synesthesia, by Labyrinth Terrarium Tanks Test Guys Test Guys Item Outpost The Assembly The Baby Boogles The Beverly Hills Car Club The Collectoooooor The Elementies The Exiled Apes The Nasty Boys The Rock theBULL by metaCOLLECTIVE TheDragonClub Thirsty Cactus Garden Party Thoughtful Folk NFT ThugDragonz Tiny Tigers Titanz ToneBox Undead Sols Vale Unleashed Rel Vampires of SOL Vampires Of SOL WallStreetPunkS Wicked Pigeon Posse Wieners Wieners Club Wildfire Native Winter Tiny Tigers Wolves On Wallstreet WOOFers World of Deities NFT WUKONGSOL Xperiment</p><h2>Bug Bounty</h2><p>In conjunction with this vulnerability research, Metaplex has launched a <a href="https://www.metaplex.com/posts/bug-bounty-blog">bug bounty program.</a></p><blockquote><p>Earlier this week the <a href="https://twitter.com/metaplex?ref_src=twsrc%5Etfw">@Metaplex</a> Foundation announced a Bug Bounty program&#8212;our commitment to white-hat developers we&#8217;ve been spinning up for months.<br><br>Our first contributor, <a href="https://twitter.com/jonluca?ref_src=twsrc%5Etfw">@jonluca</a>, uncovered a vulnerability in CMv1 back in January. More below. &#128071; <a href="https://t.co/sq0cjtLtTj">https://t.co/sq0cjtLtTj</a></p><p>&#8212; Metaplex (@metaplex) <a href="https://twitter.com/metaplex/status/1504846982954762290?ref_src=twsrc%5Etfw">March 18, 2022</a></p></blockquote><h2>Timeline</h2><ul><li><p>Dec 31st - <a href="https://github.com/metaplex-foundation/metaplex/commit/e9ef376443c3c8fd2f5b151dd0b09f757b1bf35c">Fix for CMv2 is landed</a></p></li><li><p>Tue Jan 04 2022 05:57:11 GMT-0500 - <a href="https://solscan.io/tx/coSeMNsGKebMGP1vqPZcEbu6rYiF4BbCRrtBRNLFi4TbMo3Psd7KZyvDTPv6KyeqZNDyMVU3o6D3rgQPG1aV94J">First attacker transaction is executed</a></p></li><li><p>Tue Jan 04 2022 06:20:29 GMT-0500 - <a href="https://solscan.io/tx/3zhZDtCV2vr5fSG2TxEjXXTdMmfk8rfnM4mNAavKdZM1Cy6627hN8vDnu7gaUk6oPmzLLcacJpTopK1bsscX9MbB">First transaction that tries to interact with the newly updated contracted is executed</a></p></li><li><p>Tue Jan 04 2022 06:49:27 GMT-0500 - <a href="https://solscan.io/tx/3zhZDtCV2vr5fSG2TxEjXXTdMmfk8rfnM4mNAavKdZM1Cy6627hN8vDnu7gaUk6oPmzLLcacJpTopK1bsscX9MbB">Last transaction that tries to interact with the newly updated contracted is executed</a></p></li><li><p>Tue Jan 06, 2022, 18:25 GMT-0500 - <a href="https://github.com/metaplex-foundation/metaplex/commit/4ddc13ea29070172f358e054baa9d4c47687a26b">Fix for CMv1 is landed</a></p></li><li><p>Tue Jan 15, 2022, 21:15 GMT-0500 - Metaplex is alerted to this specific vulnerability.</p></li><li><p>Fri Mar 11, 2022 - <a href="https://www.metaplex.com/posts/bug-bounty-blog">Metaplex bug bounty program is launched in conjunction with this post</a></p></li><li><p>Fri Mar 18, 2022 - <a href="https://twitter.com/metaplex/status/1504846982954762290">Metaplex bug bounty for CMv1 is announced</a></p></li></ul><h2>Appendix</h2><p>This vulnerability was discussed in Discord&#8217;s and on Twitter but was not widely analyzed.</p><p>All the code for this research will be made public pending final vulnerability disclosures on various exchanges.</p>]]></content:encoded></item><item><title><![CDATA[Analyzing Seated’s restaurants by reversing their API]]></title><description><![CDATA[Last month I wrote about writing a bot to automatically get reservations for OpenTable. In that same vein, Seated is an app that offers a certain (sizable) percentage off your bill for certain restaurants in your area.]]></description><link>https://jonluca.substack.com/p/seated</link><guid isPermaLink="false">https://jonluca.substack.com/p/seated</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sun, 02 Jan 2022 18:28:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PxVV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last month I <a href="/posts/opentable?ref=seated">wrote about writing a bot to automatically get reservations for OpenTable</a>. In that same vein, Seated is an app that offers a certain (sizable) percentage off your bill for certain restaurants in your area.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PxVV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PxVV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 424w, https://substackcdn.com/image/fetch/$s_!PxVV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 848w, https://substackcdn.com/image/fetch/$s_!PxVV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 1272w, https://substackcdn.com/image/fetch/$s_!PxVV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PxVV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Seated screenshot&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Seated screenshot" title="Seated screenshot" srcset="https://substackcdn.com/image/fetch/$s_!PxVV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 424w, https://substackcdn.com/image/fetch/$s_!PxVV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 848w, https://substackcdn.com/image/fetch/$s_!PxVV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 1272w, https://substackcdn.com/image/fetch/$s_!PxVV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ce00b4-e00c-4e24-95a2-b29944a8f6a8_800x1731.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>Screenshot from Seated app of a block in the LES in NYC</p><p>The rebates actually get pretty substantial - up to 50% in some cases. Half off dinner in NYC is a pretty enticing proposition (although most of the restaurants seem to be in the 10 - 20% range).</p><p>It&#8217;s a pretty interesting app - I&#8217;m not entirely sure what their business model is (as I can&#8217;t image that a known low-margin industry like restaurants would be able to rebate 50% of the total value of a bill), but it does work and their rebates are easy to claim.</p><p>I wanted to see if I could grab all their restaurants and visualize the stats on them.</p><h2>Finding their API</h2><p>I used the same methodology as in the previous article - luckily, the app didn&#8217;t do any form of TLS pinning, and I was able to quickly reverse their routes.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R4d1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R4d1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 424w, https://substackcdn.com/image/fetch/$s_!R4d1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 848w, https://substackcdn.com/image/fetch/$s_!R4d1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 1272w, https://substackcdn.com/image/fetch/$s_!R4d1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R4d1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/c1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Seated API&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Seated API" title="Seated API" srcset="https://substackcdn.com/image/fetch/$s_!R4d1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 424w, https://substackcdn.com/image/fetch/$s_!R4d1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 848w, https://substackcdn.com/image/fetch/$s_!R4d1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 1272w, https://substackcdn.com/image/fetch/$s_!R4d1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1785c99-7d35-49ba-8a73-6b1bb61cebee_800x348.png 1456w" sizes="100vw"></picture><div></div></div></a><p>I then converted it to Python using a handy <a href="https://curlconverter.com/">curl converter</a>.</p><pre><code>import requests

headers = {
    'Host': 'api.seatedapp.io',
    'Content-Type': 'application/json',
    'Flavor': 'native_app_v1',
    'Accept': '*/*',
    'Authorization': f"Bearer {os.environ.get('BEARER_TOKEN')}",
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate',
    'Platform': 'ios',
    'User-Agent': 'Seated/1075 CFNetwork/1327.0.4 Darwin/21.2.0',
    'Device': '?unrecognized?',
    'Build': '1075',
}

params = (
    ('city', '2'),
    ('isRemoveFutureWalkInVariant', 'false'),
    ('latitude', '40.71'),
    ('longitude', '-73.98'),
    ('mapLatitude', '40.71'),
    ('mapLongitude', '-73.99'),
    ('maxSeats', '2'),
    ('minSeats', '2'),
    ('page', '1'),
    ('size', '50'),
    ('slotForDate', '2022-01-02T18:30:00.000Z'),
)

response = requests.get('https://api.seatedapp.io/v2/search/map/dinein', headers=headers, params=params, verify=False)

</code></pre><p>I then adapted this to save all the restaurants for later processing, and played around with the size of the response (it seemed to error out on values above 400).</p><pre><code>import requests
import json
import os

headers = {
  'Host': 'api.seatedapp.io',
  'Content-Type': 'application/json',
  'Flavor': 'native_app_v1',
  'Accept': '*/*',
  'Authorization': f"Bearer {os.environ.get('BEARER_TOKEN')}",
  'Accept-Language': 'en-US,en;q=0.9',
  'Accept-Encoding': 'gzip, deflate',
  'Platform': 'ios',
  'User-Agent': 'Seated/1075 CFNetwork/1327.0.4 Darwin/21.2.0',
  'Device': '?unrecognized?',
  'Build': '1075',
}


def get_params_from_page(page=1):
  return (
    ('city', '2'),
    ('isRemoveFutureWalkInVariant', 'false'),
    ('latitude', '40.71'),
    ('longitude', '-73.98'),
    ('mapLatitude', '40.71'),
    ('mapLongitude', '-73.99'),
    ('maxSeats', '2'),
    ('minSeats', '2'),
    ('page', str(page)),
    ('size', '400'),
    ('slotForDate', '2022-01-02T18:30:00.000Z'),
  )


restaurants = []
page = 1
while True:
  params = get_params_from_page(page)
  response = requests.get('https://api.seatedapp.io/v2/search/map/dinein', headers=headers, params=params)
  data = response.json()
  if 'restaurants' in data:
    restaurants += data['restaurants']
  else:
    print("Something went wrong")
  if 'metaData' in data and not data['metaData']['hasMoreItems']:
    break
  print(f"Completed page {str(page)}")
  page += 1

print(f"Found {len(restaurants)} restaurants")
with open('restaurants.json', 'w') as out:
  out.write(json.dumps(restaurants))
  out.close()
</code></pre><p>Each restaurant entry had the following shape:</p><pre><code>{
  "id": 6673,
  "name": "Memphis Seoul",
  "longitude": -73.9579086303711,
  "latitude": 40.67172622680664,
  "priceRating": 2,
  "neighborhood": {
    "id": 92,
    "name": "Crown Heights ",
    "branchNavigationLink": "https://seated.app.link/8kpY6fF558",
    "cityId": 2
  },
  "image": {
    "url": "https://storage.googleapis.com/voco_main_bucket/images/3382905/original/19b89f300df44f21.png"
  },
  "images": [
    {
      "url": "https://storage.googleapis.com/voco_main_bucket/images/3382911/original/df2174d0b63060e8.png"
    }
  ],
  "isSurging": true,
  "yelpRating": 3.5,
  "baseReward": 32,
  "isWalkInOnly": true,
  "yelpBusinessUrl": "https://www.yelp.com/biz/memphis-seoul-brooklyn",
  "menuUrl": "https://www.getmemphisseoul.com/menus/",
  "address": "569 Lincoln Pl, Brooklyn, NY 11238, USA",
  "thoughts": "\"Our thoroughly unique menu is a tasty selection of Southern favorites fused with Korean ingredients and influences. Come for the savory pulled pork and Korean BBQ Meatloaf, then stay for the scrumptious ",
  "deliveryUrl": "https://direct.chownow.com/order/14302/locations/20081",
  "pickupUrl": "https://direct.chownow.com/order/14302/locations/20081",
  "deliveryReward": 25,
  "pickupReward": 25,
  "maxReward": 47,
  "phoneNumbers": [
    {
      "phoneNumber": "3473492561",
      "phonePrefix": "1"
    }
  ],
  "providerName": "ChowNow",
  "orderProviderType": "ONLINE",
  "isDeliverySurging": true,
  "isPickupSurging": true,
  "branchNavigationLink": "https://seated.app.link/tRSa5nQEy4",
  "primaryCuisine": {
    "id": 1,
    "name": "American",
    "branchNavigationLink": "https://seated.app.link/H0lPkpGUf4"
  },
  "maxPartySize": 12,
  "isOpen": true,
  "isDineIn": false,
  "availableSeatingOption": "UNKNOWN",
  "city": {
    "id": 2,
    "name": "New York City",
    "nameLower": "new york city"
  },
  "delivery": true,
  "pickup": true
}
</code></pre><h2>Rewards</h2><p>Once I had all the data, I wanted to see where most restaurants fell, in terms of their max reward distribution.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0Pm7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0Pm7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 424w, https://substackcdn.com/image/fetch/$s_!0Pm7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 848w, https://substackcdn.com/image/fetch/$s_!0Pm7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 1272w, https://substackcdn.com/image/fetch/$s_!0Pm7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0Pm7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Histogram of number of restaurants at each discount bucket&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Histogram of number of restaurants at each discount bucket" title="Histogram of number of restaurants at each discount bucket" srcset="https://substackcdn.com/image/fetch/$s_!0Pm7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 424w, https://substackcdn.com/image/fetch/$s_!0Pm7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 848w, https://substackcdn.com/image/fetch/$s_!0Pm7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 1272w, https://substackcdn.com/image/fetch/$s_!0Pm7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb9cf3b8a-3597-4e8c-bd75-bfb76b187f91_800x522.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Rebate % # Restaurants (0, 5] 1 (5, 10] 289 (10, 15] 547 (15, 20] 266 (20, 25] 264 (25, 30] 116 (30, 35] 47 (35, 40] 3 (40, 45] 3 (45, 50] 2</p><p>Most restaurants fall in the 10-15% back, and only a handful are above 35%. I expected a more normal distribution, and fewer restaurants offering 20%+, but it&#8217;s a pretty healthy range and they&#8217;re clustered in a relatively high return zone.</p><h2>Visualizing locations</h2><p>I also wanted to see where all their restaurants were - I didn&#8217;t necessarily want to travel to Yonkers for 30% off a pizza.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_I3a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_I3a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 424w, https://substackcdn.com/image/fetch/$s_!_I3a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 848w, https://substackcdn.com/image/fetch/$s_!_I3a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 1272w, https://substackcdn.com/image/fetch/$s_!_I3a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_I3a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/e117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Map of all seated's restaurants&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Map of all seated's restaurants" title="Map of all seated's restaurants" srcset="https://substackcdn.com/image/fetch/$s_!_I3a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 424w, https://substackcdn.com/image/fetch/$s_!_I3a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 848w, https://substackcdn.com/image/fetch/$s_!_I3a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 1272w, https://substackcdn.com/image/fetch/$s_!_I3a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe117e369-a2f9-4416-aee5-2da2a01eb9ed_800x800.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>This worked very well - it was trivial to then cut the dataframe to only restaurants in downtown NYC:</p><pre><code>les_df = geo_df[geo_df['latitude'].between(40.7,40.767645)]
les_df[['name',reward]].head(10)
</code></pre><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a8zj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a8zj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 424w, https://substackcdn.com/image/fetch/$s_!a8zj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 848w, https://substackcdn.com/image/fetch/$s_!a8zj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 1272w, https://substackcdn.com/image/fetch/$s_!a8zj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a8zj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Downtown restaurants with highest rebates&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Downtown restaurants with highest rebates" title="Downtown restaurants with highest rebates" srcset="https://substackcdn.com/image/fetch/$s_!a8zj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 424w, https://substackcdn.com/image/fetch/$s_!a8zj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 848w, https://substackcdn.com/image/fetch/$s_!a8zj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 1272w, https://substackcdn.com/image/fetch/$s_!a8zj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99b58925-ba4e-4631-82b1-495e9fa71eaa_800x475.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h2>Rating vs Reward</h2><p>What&#8217;s interesting is that every restaurant also has a yelp rating. I wondered if the rating was correlated to the rebate size - were worse restaurants offering more (if it was even the restaurant&#8217;s choice on the max reward %?) due to low rewards?</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lK75!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lK75!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 424w, https://substackcdn.com/image/fetch/$s_!lK75!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 848w, https://substackcdn.com/image/fetch/$s_!lK75!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 1272w, https://substackcdn.com/image/fetch/$s_!lK75!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lK75!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b6388af4-76cc-4087-88ca-684226a43439_800x489.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Reward vs Yelp rating&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Reward vs Yelp rating" title="Reward vs Yelp rating" srcset="https://substackcdn.com/image/fetch/$s_!lK75!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 424w, https://substackcdn.com/image/fetch/$s_!lK75!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 848w, https://substackcdn.com/image/fetch/$s_!lK75!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 1272w, https://substackcdn.com/image/fetch/$s_!lK75!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6388af4-76cc-4087-88ca-684226a43439_800x489.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>And the answer is&#8230; kinda? There highest rewards show up at the 3 and 4 star ratings, but they also have the highest distribution of restaurants, so with more data points you&#8217;re more likely to have outliers.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fSl1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fSl1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 424w, https://substackcdn.com/image/fetch/$s_!fSl1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 848w, https://substackcdn.com/image/fetch/$s_!fSl1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 1272w, https://substackcdn.com/image/fetch/$s_!fSl1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fSl1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Yelp rating mean stats&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Yelp rating mean stats" title="Yelp rating mean stats" srcset="https://substackcdn.com/image/fetch/$s_!fSl1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 424w, https://substackcdn.com/image/fetch/$s_!fSl1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 848w, https://substackcdn.com/image/fetch/$s_!fSl1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 1272w, https://substackcdn.com/image/fetch/$s_!fSl1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7f80b516-49ec-4747-b1bf-0c14537890c3_800x640.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>The above shows the stats for the reward based on the yelp rating. If we assume that 1, 2 and 5 star ratings are low-volume outliers, we see a <em>slight</em> correlation, but not enough to draw any conclusions from.</p><h2>Reward vs Price</h2><p>Another interesting potential correlation is with the maximum rebate % and how expensive the restaurant is - you&#8217;d imagine that nicer restaurants would offer less back.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aFDz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aFDz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 424w, https://substackcdn.com/image/fetch/$s_!aFDz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 848w, https://substackcdn.com/image/fetch/$s_!aFDz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 1272w, https://substackcdn.com/image/fetch/$s_!aFDz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aFDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Reward vs Price&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Reward vs Price" title="Reward vs Price" srcset="https://substackcdn.com/image/fetch/$s_!aFDz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 424w, https://substackcdn.com/image/fetch/$s_!aFDz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 848w, https://substackcdn.com/image/fetch/$s_!aFDz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 1272w, https://substackcdn.com/image/fetch/$s_!aFDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb23f5245-c1de-4a38-a0ea-efd57c5b2c89_800x480.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>This is somewhat observed, although there are some 4 dollar sign restaurants that offer 30% back.</p><h2>Code</h2><p>The code for the querying and analysis can be found <a href="https://github.com/jonluca/seated-api">on GitHub, at this repo</a>.</p><h2>Joining Seated</h2><p>If you end up signing up for Seated, you can use my referral code <strong>jonluca1</strong> to get $15 bonus on your first meal.</p>]]></content:encoded></item><item><title><![CDATA[Building an OpenTable bot]]></title><description><![CDATA[New York City has some of the best restaurants in the world, with some of the most competitive reservations to match.]]></description><link>https://jonluca.substack.com/p/opentable</link><guid isPermaLink="false">https://jonluca.substack.com/p/opentable</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Fri, 03 Dec 2021 22:55:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8zM3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>New York City has some of the best restaurants in the world, with some of the most competitive reservations to match. Some of the hottest restaurants are booked out months in advance, and sell out within minutes of opening their reservations. There are a few restaurants that I&#8217;ve tried to book to no avail - I would search for future availability, but OpenTables tool only lets you set a notification for a single day and time range.</p><p>I wanted to see if it would be possible to set up a script to monitor for changes, and to alert me any time there was any availability. There was one restaurant in particular, Chef&#8217;s Table at Brooklyn Fare, that I wanted to book for early next year.</p><h1>Finding their API routes</h1><h2>Getting availability</h2><p>I fired up BurpSuite and set it up. I navigated to their restaurant page and was happy to see that there wasn&#8217;t any certificate stapling going on, and the response was clean JSON (the recent rise of protobufs and graphql have made clean API discovery a bit harder).</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8zM3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8zM3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 424w, https://substackcdn.com/image/fetch/$s_!8zM3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 848w, https://substackcdn.com/image/fetch/$s_!8zM3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 1272w, https://substackcdn.com/image/fetch/$s_!8zM3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8zM3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/d0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8zM3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 424w, https://substackcdn.com/image/fetch/$s_!8zM3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 848w, https://substackcdn.com/image/fetch/$s_!8zM3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 1272w, https://substackcdn.com/image/fetch/$s_!8zM3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0bfcdc6-bc0a-4d2f-bae0-53d568cc58db_800x497.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>Fortunately for us, it seemed like there was a pretty clean API to fetch the availability of any restaurant - a simple <code>PUT</code> to https://mobile-api.opentable.com/api/v3/restaurant/availability request with the following body:</p><pre><code>{
  "forceNextAvailable": "true",
  "includeNextAvailable": true,
  "availabilityToken": "&lt;jwt&gt;",
  "dateTime": "2021-12-03T19:00",
  "requestTicket": "true",
  "allowPop": true,
  "attribution": {
    "partnerId": "84"
  },
  "partySize": 2,
  "includeOffers": true,
  "requestPremium": "true",
  "requestDateMessages": true,
  "rids": ["211123"],
  "requestAttributeTables": "true"
}
</code></pre><p>And the response had exactly what we were looking for.</p><pre><code>{
  "dateTime": "2021-12-03T19:00",
  "availability": {
    "dateTime": "2021-12-03T19:00",
    "noTimesReasons": ["NoTimesExist"],
    "minPartySize": 2,
    "maxPartySize": 6,
    "maxDaysInAdvance": 42,
    "id": "211123",
    "timeslots": [],
    "dateMessages": [],
    "token": "eyJ2IjoyLCJtIjowLCJwIjowLCJzIjowLCJuIjowfQ",
    "hasPremiumTables": false,
    "availabilityToken": "eyJ2IjoyLCJtIjowLCJwIjowLCJzIjowLCJuIjowfQ"
  },
  "suggestedAvailability": ["... entry availability"],
  "experienceList": {
    "results": []
  },
  "hasPremiumTables": false,
  "dayAvailability": {}
}
</code></pre><p>A quick conversion to python then gets us:</p><pre><code>import requests
import json

cookies = {
'Some Cookies Here': '...'
}

headers = {
    'Host': 'mobile-api.opentable.com',
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'X-Ot-Sessionid': 'SessId',
    'Accept-Encoding': 'gzip, deflate',
    'Authorization': 'Bearer &lt;TOKEN&gt;',
    'User-Agent': 'com.contextoptional.OpenTable/15.2.0.16; iPhone; iOS/15.1.1; 3.0;',
    'Content-Length': '415',
    'Accept-Language': 'en-US;q=1.0,en-GB;q=0.7,en;q=0.5,*;q=0.3',
    'Connection': 'close',
}

data = {
    "forceNextAvailable": "true",
    "includeNextAvailable": True,
    "availabilityToken": "&lt;jwt&gt;",
    "dateTime": "2021-12-03T19:00",
    "requestTicket": "true",
    "allowPop": True,
    "attribution": {
        "partnerId": "84"
    },
    "partySize": 2,
    "includeOffers": True,
    "requestPremium": "true",
    "requestDateMessages": True,
    "rids": [
        "211123"
    ],
    "requestAttributeTables": "true"
}

response = requests.put('https://mobile-api.opentable.com/api/v3/restaurant/availability', headers=headers, cookies=cookies, data=json.dumps(data), verify=False)
</code></pre><p>Success! I then tried removing all non essential cookies and headers, and it still worked - the only thing that mattered were the <code>Authorization</code> and <code>Content-Type</code> request headers.</p><p>Another interesting thing was the <code>availabilityToken</code> in the request body. It clearly looked like a jwt (any time you see <code>ey</code> in a string your first thought should be jwt, much like a string ending in <code>=</code> should make you think base64), but when I put in a JWT debugger it looked like a simple flag mechanism.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jD8g!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jD8g!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 424w, https://substackcdn.com/image/fetch/$s_!jD8g!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 848w, https://substackcdn.com/image/fetch/$s_!jD8g!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 1272w, https://substackcdn.com/image/fetch/$s_!jD8g!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jD8g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jD8g!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 424w, https://substackcdn.com/image/fetch/$s_!jD8g!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 848w, https://substackcdn.com/image/fetch/$s_!jD8g!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 1272w, https://substackcdn.com/image/fetch/$s_!jD8g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F349783a9-8c38-4729-9b5c-b5ed3e9c95cb_800x479.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><pre><code>{
  "v": 2,
  "m": 1,
  "p": 1,
  "s": 0,
  "n": 0
}
</code></pre><p>My request to Chef&#8217;s Table at Brooklyn Fare had <code>m</code> and <code>p</code> set to 0, whereas my request for another restaurant with a lot of availability had them set to 1. Swapping out the jwt&#8217;s led to the same response, just with a different token for the available bookings.</p><p>The time slots were an array of objects with this format:</p><pre><code>{
  "dateTime": "2021-12-06T15:30",
  "available": true,
  "redemptionTier": "GreatDeal",
  "diningAreas": [
    {
      "id": "1",
      "isDefaultArea": true,
      "availableAttributes": ["default", "outdoor"]
    }
  ],
  "token": "&lt;jwt&gt;",
  "slotHash": "&lt;hash&gt;",
  "points": 100,
  "type": "Standard",
  "attributes": ["default", "outdoor"],
  "priceAmount": 0
}
</code></pre><p>The <code>slotHash</code> is the unique ID for this given reservation.</p><h2>Booking a time</h2><p>I made a dummy booking with a restaurant that had loads of availability. When you go through their flow, they first fire off a reservation &#8220;lock&#8221;, that guantees you that spot, and once you&#8217;ve finished whatever questionnaire the restaurant might have (seating preferences, etc), it fires off another request to complete the reservation.</p><p>The lock fires a <code>POST</code> to <code>/api/v1/reservation/&lt;restaurant_id&gt;/lock</code>, with this body:</p><pre><code>{
  "partySize": 2,
  "dateTime": "2021-12-06T15:30",
  "selectedDiningArea": {
    "tableAttribute": "default",
    "diningAreaId": "1"
  },
  "hash": "&lt;hash&gt;",
  "attribution": {
    "partnerId": "84"
  }
}
</code></pre><p>If your request is valid and that slot has not been locked by someone else, the response will now include a unique ID that corresponds to that reservation:</p><pre><code>{
  "id": "&lt;numericIdOfReservation&gt;",
  "rid": "19306",
  "date": "2021-12-06T15:30",
  "partySize": 2,
  "offerLockId": "0",
  "stripeKey": "pk_live_&lt;STRIPEKEY&gt;",
  "creditCardCancellationPolicy": {
    "cancellable": true,
    "cancellationCutoffDate": "2021-12-04T17:00"
  },
  "occasions": [
    "birthday",
    "anniversary",
    "date",
    "special_occasion",
    "business_meal"
  ]
}
</code></pre><p>The final step is to fire one last <code>POST</code> request to <code>/api/v1/reservation/&lt;restaurant_id&gt;</code> with a much larger body:</p><pre><code>{
  "diningFormOptIn": true,
  "partySize": 2,
  "gpid": "&lt;GPID&gt;",
  "countryId": "US",
  "attribution": {
    "partnerId": "84"
  },
  "loyaltyProgramOptIn": true,
  "optIns": {
    "smsNotifications": {
      "reservationSms": true,
      "waitlistSms": false
    },
    "openTableDataSharing": {
      "businessPartners": true,
      "corporateGroup": true,
      "pointOfSale": true
    },
    "dataSharing": {
      "guestShare": true,
      "dinerProfileShare": true,
      "sync": true
    },
    "restaurantEmailMarketing": {
      "restaurantEmails": true
    },
    "emailNotifications": {
      "diningFeedback": true
    },
    "emailMarketing": {
      "newHot": false,
      "restaurantWeek": false,
      "spotlight": false,
      "product": false,
      "promotional": false,
      "insider": false,
      "dinersChoice": false
    }
  },
  "rewardTier": "GreatDeal",
  "campaignId": "7",
  "hash": "&lt;HASH&gt;",
  "points": 100,
  "loadInvitations": false,
  "number": "&lt;PHONENUMBER&gt;",
  "notes": "",
  "slotAvailabilityToken": "&lt;TOKEN&gt;",
  "selectedDiningArea": {
    "diningAreaId": "1",
    "tableAttribute": "default"
  },
  "lockId": "PreviouslyObtainedLockId",
  "dinerId": "&lt;DINERID&gt;",
  "location": {
    "latitude": 40.74,
    "longitude": -73.98
  },
  "dateTime": "2021-12-06T15:30",
  "uberUuid": "&lt;UUID&gt;"
}
</code></pre><h2>Canceling a reservation</h2><p>Not that I&#8217;d necessarily want to cancel a reservation, but it looks as easy as a <code>DELETE</code> request to <code>DELETE /api/v3/reservation/&lt;restaurant_id&gt;/&lt;confirmation_number&gt;</code></p><h1>Hooking it all up</h1><p>I put it all together into a python script, <a href="https://github.com/jonluca/OpenTable-Reservation-Maker">which be found on GitHub</a>. This will take in a restaurant ID and your time range, and automatically book it once it becomes available.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WJLR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WJLR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 424w, https://substackcdn.com/image/fetch/$s_!WJLR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 848w, https://substackcdn.com/image/fetch/$s_!WJLR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 1272w, https://substackcdn.com/image/fetch/$s_!WJLR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WJLR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WJLR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 424w, https://substackcdn.com/image/fetch/$s_!WJLR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 848w, https://substackcdn.com/image/fetch/$s_!WJLR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 1272w, https://substackcdn.com/image/fetch/$s_!WJLR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5f086ebb-dead-40b2-93d6-587c3e85df74_800x288.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Finally, just for the hell of it, I hooked it up to twilio so that it would send a text message once the reservation had been booked. It will now send me a text message once a table has been booked, and save the details to a file so that it won&#8217;t double book a reservation.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j_Rx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j_Rx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 424w, https://substackcdn.com/image/fetch/$s_!j_Rx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 848w, https://substackcdn.com/image/fetch/$s_!j_Rx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 1272w, https://substackcdn.com/image/fetch/$s_!j_Rx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j_Rx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!j_Rx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 424w, https://substackcdn.com/image/fetch/$s_!j_Rx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 848w, https://substackcdn.com/image/fetch/$s_!j_Rx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 1272w, https://substackcdn.com/image/fetch/$s_!j_Rx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc2d15b-d715-4290-84dd-a46f6339c0e3_800x280.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>I tested it and it worked great. I&#8217;ve got it monitoring a few different restaurants right now, and it&#8217;ll book it as soon as someone cancels or it becomes available, or the restaurant releases availability. I still need to modify it so that it works with restaurants that require a credit card transaction, but that can be saved for the next post.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dCYa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dCYa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 424w, https://substackcdn.com/image/fetch/$s_!dCYa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 848w, https://substackcdn.com/image/fetch/$s_!dCYa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 1272w, https://substackcdn.com/image/fetch/$s_!dCYa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dCYa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/c60307fb-3c28-427f-882a-521a1724235f_800x1731.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dCYa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 424w, https://substackcdn.com/image/fetch/$s_!dCYa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 848w, https://substackcdn.com/image/fetch/$s_!dCYa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 1272w, https://substackcdn.com/image/fetch/$s_!dCYa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc60307fb-3c28-427f-882a-521a1724235f_800x1731.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>The code <a href="https://github.com/jonluca/OpenTable-Reservation-Maker">can be found on GitHub</a>. There isn&#8217;t a way to fetch your bearer token automatically right now, or search for a restaurant, so you&#8217;ll need to know those ahead of time to make this work, but you can follow the steps above (or open a PR!).</p><h2>Future work</h2><p>The next site I want to automate is Tock, as they have most of the best restaurants in NY, but their endpoints are protobufs, which are a bit harder to reverse engineer. Resy is also an option, but with the Amex acquisition it&#8217;s actually pretty easy to get reservations at sold out restaurants if you have an Amex Platinum card.</p>]]></content:encoded></item><item><title><![CDATA[Snapshotting memory to scrape encrypted network requests]]></title><description><![CDATA[Most web reverse engineering focuses on two attack surfaces - either DOM scraping through something like puppeteer or beatifulsoup, or on MITM attacks to reverse network calls.]]></description><link>https://jonluca.substack.com/p/heap-snapshot-scraping</link><guid isPermaLink="false">https://jonluca.substack.com/p/heap-snapshot-scraping</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sun, 08 Aug 2021 14:06:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mP1b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most web reverse engineering focuses on two attack surfaces - either DOM scraping through something like puppeteer or beatifulsoup, or on MITM attacks to reverse network calls.</p><p>The former is what most traditional scraping looks like - you manually inspect a web page, you determine the right xpath/css selectors to follow, and then instruct a scraper to statically request the page and scrape it.</p><p>The rise of SPAs has made this approach a bit less impractical - you now have to actually render the content, which is is significantly slower, to have the content you want to scrape show up in the DOM.</p><p>At the same time, it made web scraping as a whole much easier - sites that used to be server rendered now have nice, machine readable routes that serve JSON. This has lead to tools like Burp Suite and Charles Proxy being coopted from their original use of finding security vulnerabilities to being primarily used for web scraping.</p><p>In this article I want to introduce a third, more niche attack surface for scraping web pages - scraping through memory snapshots and their respective dominator graph. It&#8217;s particularly applicable when the client is encrypting its network requests and responses.</p><h2>Chrome Devtools</h2><p>Devtools has a nice feature to detect memory leaks - the Memory tab.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mP1b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mP1b!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 424w, https://substackcdn.com/image/fetch/$s_!mP1b!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 848w, https://substackcdn.com/image/fetch/$s_!mP1b!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 1272w, https://substackcdn.com/image/fetch/$s_!mP1b!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mP1b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Memory tab&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Memory tab" title="Memory tab" srcset="https://substackcdn.com/image/fetch/$s_!mP1b!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 424w, https://substackcdn.com/image/fetch/$s_!mP1b!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 848w, https://substackcdn.com/image/fetch/$s_!mP1b!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 1272w, https://substackcdn.com/image/fetch/$s_!mP1b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F76b8a2fd-2113-432c-9426-3346dc818eb3_794x234.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>It will take a snapshot of the current tabs memory, and built a graph out of it to inspect.</p><p>The memory tab is mostly used to compare different memory snapshots - you take a snapshot, perform some actions on the page, take another snapshot, and look at everything that got allocated to see if there are any memory leaks.</p><p>Since it&#8217;s pretty much a full memory snapshot, it stores all strings and objects - which, for something like React or Vue, will include all props and (typically) network request responses.</p><h2>Memory snapshots</h2><p>Inspecting memory isn&#8217;t new in the reverse engineering space - for native apps, it&#8217;s one of the most common ways of finding out what an application is doing and how its internal logic is structured.</p><p>Within the web world, though, it&#8217;s hardly ever used. Most of the literature online is regarding finding memory leaks. All the tools and software around it are therefore built around that - they don&#8217;t care about the <em>contents</em> of the memory, they care about the origination source/allocator and the references/dominators to it.</p><h2>When does this make sense?</h2><p>Using memory snapshots makes sense in two cases - 1), when the client encrypts the network response, and you don&#8217;t have the time or energy to reverse the bundle to understand how to decrypt it programatically (like in <a href="'/posts/decrypting-blind?ref=wsenr'">Blind&#8217;s case, which I did here</a>) and 2) when the client requests are machine readable, but there are multiple requests that get stitched together in memory to create the desired objects.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nABz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nABz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 424w, https://substackcdn.com/image/fetch/$s_!nABz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 848w, https://substackcdn.com/image/fetch/$s_!nABz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 1272w, https://substackcdn.com/image/fetch/$s_!nABz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nABz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;An encrypted memory response&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="An encrypted memory response" title="An encrypted memory response" srcset="https://substackcdn.com/image/fetch/$s_!nABz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 424w, https://substackcdn.com/image/fetch/$s_!nABz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 848w, https://substackcdn.com/image/fetch/$s_!nABz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 1272w, https://substackcdn.com/image/fetch/$s_!nABz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5b9f0aa1-caf7-4fef-b629-79c2d5bd662b_800x471.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h2>Reversing the spec</h2><p>Saving a memory snapshot generates a <code>heapsnapshot</code> file - this is a JSON file with a given format, that is not document anywhere as far as I can tell. These snapshots are the same in Node and in Chrome, and all documentation for both just tell you to upload it to the memory tab in a new devtools instance to view it.</p><p>Luckily the code for devtools is public, <a href="https://github.com/jonluca/javascript-heap-inspector">so I managed to reverse it and turn it into a CLI tool</a> - this accepts heapsnapshots as inputs, and can parse them into the graph.</p><h2>Extracting JSON</h2><p>At this point, you can just go through and try and parse every string in memory as JSON, and see what sticks around. This will be an easy way to find in memory JSON strings - however, these aren&#8217;t as common as you might think. The v8 compiler is pretty efficient in knowing what to keep in memory and what to discard, so it&#8217;ll typically just keep the parsed objects.</p><h2>Reconstructing objects</h2><p>You can reconstruct objects in memory by traversing through the graph and stitching the nodes together - everything ends up being a primitive, and graph nodes can just contain properties, so they&#8217;re proxies for objects.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HMhz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HMhz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 424w, https://substackcdn.com/image/fetch/$s_!HMhz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 848w, https://substackcdn.com/image/fetch/$s_!HMhz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 1272w, https://substackcdn.com/image/fetch/$s_!HMhz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HMhz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Blind's object containing interesting information&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Blind's object containing interesting information" title="Blind's object containing interesting information" srcset="https://substackcdn.com/image/fetch/$s_!HMhz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 424w, https://substackcdn.com/image/fetch/$s_!HMhz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 848w, https://substackcdn.com/image/fetch/$s_!HMhz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 1272w, https://substackcdn.com/image/fetch/$s_!HMhz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8d6042a7-0fa8-4947-afc7-8fdae59fc122_800x853.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>It&#8217;s useful to sort by retained size (which the code in the repo has a function for), and look at the heaviest objects - these are typically going to be your most interesting allocations, especailly if you perform an allocation recording.</p><h2>Future plans</h2><p><a href="https://github.com/jonluca/javascript-heap-inspector">The code for the project can be found here, which will parse any chrome or node heapsnapshot.</a>. In part 2 I&#8217;m going to add the ability to fully reconstruct objects from this graph.</p>]]></content:encoded></item><item><title><![CDATA[JavaScript gotchas]]></title><description><![CDATA[Here are some interesting JavaScript facts that I&#8217;ve encountered over the last few years.]]></description><link>https://jonluca.substack.com/p/javascript-facts</link><guid isPermaLink="false">https://jonluca.substack.com/p/javascript-facts</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sun, 18 Jul 2021 13:18:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8C-L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here are some interesting JavaScript facts that I&#8217;ve encountered over the last few years.</p><h1>Function.length</h1><p>Call <code>Function.length</code> will return the number of arguments a function is expecting.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8C-L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8C-L!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 424w, https://substackcdn.com/image/fetch/$s_!8C-L!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 848w, https://substackcdn.com/image/fetch/$s_!8C-L!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 1272w, https://substackcdn.com/image/fetch/$s_!8C-L!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8C-L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Javascript function length&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Javascript function length" title="Javascript function length" srcset="https://substackcdn.com/image/fetch/$s_!8C-L!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 424w, https://substackcdn.com/image/fetch/$s_!8C-L!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 848w, https://substackcdn.com/image/fetch/$s_!8C-L!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 1272w, https://substackcdn.com/image/fetch/$s_!8C-L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4faf1e85-2915-4666-a397-31bdf4a2c4bf_800x313.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>The spread operator will not be included in the count.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PQMt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PQMt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 424w, https://substackcdn.com/image/fetch/$s_!PQMt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 848w, https://substackcdn.com/image/fetch/$s_!PQMt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 1272w, https://substackcdn.com/image/fetch/$s_!PQMt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PQMt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/f9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Javascript function length&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Javascript function length" title="Javascript function length" srcset="https://substackcdn.com/image/fetch/$s_!PQMt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 424w, https://substackcdn.com/image/fetch/$s_!PQMt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 848w, https://substackcdn.com/image/fetch/$s_!PQMt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 1272w, https://substackcdn.com/image/fetch/$s_!PQMt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9c3283d-ea8d-4a45-8f88-90d745be6bda_800x300.png 1456w" sizes="100vw"></picture><div></div></div></a><h1>Array.map(func)</h1><p>when you call <code>Array.map(func)</code>, the mapped function gets called with 3 arguments, not just the value.</p><p>So for:</p><pre><code>["10", "11", "12"].map(parseInt);
</code></pre><p>You&#8217;d <em>expect</em> to get</p><pre><code>[10, 11, 12];
</code></pre><p>but in reality get</p><pre><code>[10, NaN, 1];
</code></pre><p>This is because <code>.map(parseInt)</code> calls the function with three arguments - <code>(currentValue, index, array)</code>. This is normally not an issue, but becomes an issue when the mapped function takes additional arguments that do not correspond to the ones being passed in.</p><p><code>parseInt</code> takes in two arguments - <code>value, [, radix]</code>, and thus tries to parse <code>11</code> with radix <code>1</code>, which is <code>NaN</code></p><h1>Values are truth-y by default</h1><p>The only falsey values are:</p><pre><code>[0, -0, 0n, "", "", null, undefined, NaN, false];
</code></pre><p><em>Everything</em> else is truthy - including <code>[]</code>, an empty Set(), and an empty object.</p><h1>Null comparisons to 0</h1><p>I ran into a nasty bug once where a value I thought was guaranteed to be a number was actually explicitly set to null. I was doing a comparison with <code>0</code> and ran into this weird behavior:</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!A5LR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!A5LR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 424w, https://substackcdn.com/image/fetch/$s_!A5LR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 848w, https://substackcdn.com/image/fetch/$s_!A5LR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 1272w, https://substackcdn.com/image/fetch/$s_!A5LR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!A5LR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/f4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Javascript null comparison&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Javascript null comparison" title="Javascript null comparison" srcset="https://substackcdn.com/image/fetch/$s_!A5LR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 424w, https://substackcdn.com/image/fetch/$s_!A5LR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 848w, https://substackcdn.com/image/fetch/$s_!A5LR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 1272w, https://substackcdn.com/image/fetch/$s_!A5LR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff4a89222-a2a0-4050-bf53-f53b9c43dd83_486x392.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h1>Array.sort sorts by string sequence code</h1><p>Call <code>.sort()</code> on an array of numbers will not sort them numerically. Which is perplexing</p><p>Null is not equal to zero, and is not greater than zero, but is greater than or equal to zero.</p><h1>String.replace() only replaces the first instance of a match</h1><p>It&#8217;s almost a rite of passage for javascript developers to be bewildered that <code>String.replace</code> only replaces the first instance of a match in a string.</p><p>Thankfully in ES2021 we now have <code>String.replaceAll</code>, which behaves as you&#8217;d expect.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eO_x!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eO_x!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 424w, https://substackcdn.com/image/fetch/$s_!eO_x!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 848w, https://substackcdn.com/image/fetch/$s_!eO_x!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 1272w, https://substackcdn.com/image/fetch/$s_!eO_x!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eO_x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/e5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Javascript replace&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Javascript replace" title="Javascript replace" srcset="https://substackcdn.com/image/fetch/$s_!eO_x!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 424w, https://substackcdn.com/image/fetch/$s_!eO_x!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 848w, https://substackcdn.com/image/fetch/$s_!eO_x!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 1272w, https://substackcdn.com/image/fetch/$s_!eO_x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5a0ec1b-9106-44ef-9eb2-5f211c98042b_718x400.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h1>WTFJS</h1><p>Someone also pointed out <a href="https://wtfjs.com/">WTFJS.com</a> which is a site that has a lot more javascript oddities and examples.</p>]]></content:encoded></item><item><title><![CDATA[Writing fast async HTTP requests in Python]]></title><description><![CDATA[I do a lot of web scraping in my spare time, and have been chasing down different formats and code snippets to make a large amount of network requests locally, with controls for rate limiting and error handling.]]></description><link>https://jonluca.substack.com/p/async-python-http</link><guid isPermaLink="false">https://jonluca.substack.com/p/async-python-http</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Tue, 15 Jun 2021 03:02:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_B79!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I do a lot of web scraping in my spare time, and have been chasing down different formats and code snippets to make a large amount of network requests locally, with controls for rate limiting and error handling.</p><p>I&#8217;ve gone through a few generations - I&#8217;ll use this post to catalogue where I started and what I&#8217;m doing now. If you want to skip the post and just see the final code, <a href="https://gist.github.com/jonluca/14fe99be6204f34cbd61c950b0faf3b1">it can be found here.</a></p><h2>Gen 1</h2><p>Generation one was trusty old <code>requests</code>. Need to make 10 requests? Wrap it in a for loop and make them iteratively.</p><pre><code>import requests

results = {}
for i in range(10):
    resp = requests.get('https://jsonplaceholder.typicode.com/todos/1')
    results[i] = resp.json()
</code></pre><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_B79!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_B79!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 424w, https://substackcdn.com/image/fetch/$s_!_B79!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 848w, https://substackcdn.com/image/fetch/$s_!_B79!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 1272w, https://substackcdn.com/image/fetch/$s_!_B79!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_B79!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Gen 1&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Gen 1" title="Python Gen 1" srcset="https://substackcdn.com/image/fetch/$s_!_B79!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 424w, https://substackcdn.com/image/fetch/$s_!_B79!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 848w, https://substackcdn.com/image/fetch/$s_!_B79!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 1272w, https://substackcdn.com/image/fetch/$s_!_B79!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7a8f839e-e9f5-4455-bc8e-e7db6283b654_800x544.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><p>This isn&#8217;t bad - 40 requests in 2.8s, or 1 req/70ms. Not an issue at all. This is fine when you need to bruteforce a 3 digit passcode - you can get 1000 requests done in 70s. Not great, but fast enough, and no need for external libraries or any research.</p><p>As soon as you get to something in the 4 character range, though, this becomes unwieldy.</p><h2>Gen 2</h2><p>The next step here was to find ways to make these requests using Threads. Spin off a native thread for each request, and let them run behind the scenes.</p><p>Set up a queue and pool to pull URLs from, and we&#8217;re good. The queue and Worker threads are defined pretty simply below:</p><pre><code>from queue import Queue

from threading import Thread


class Worker(Thread):
  """ Thread executing tasks from a given tasks queue """

  def __init__(self, tasks):
    Thread.__init__(self)
    self.tasks = tasks
    self.daemon = True
    self.start()

  def run(self):
    while True:
      func, args, kargs = self.tasks.get()
      try:
        func(*args, **kargs)
      except Exception as e:
        # An exception happened in this thread
        print(e)
      finally:
        # Mark this task as done, whether an exception happened or not
        self.tasks.task_done()


class ThreadPool:
  """ Pool of threads consuming tasks from a queue """

  def __init__(self, num_threads):
    self.tasks = Queue(num_threads)
    for _ in range(num_threads):
      Worker(self.tasks)

  def add_task(self, func, *args, **kargs):
    """ Add a task to the queue """
    self.tasks.put((func, args, kargs))

  def map(self, func, args_list):
    """ Add a list of tasks to the queue """
    for args in args_list:
      self.add_task(func, args)

  def wait_completion(self):
    """ Wait for completion of all the tasks in the queue """
    self.tasks.join()
</code></pre><p>And the actual query code is fairly straightforward - just define a function that&#8217;ll populate a global variable using some unique ID, and have it make the request off in its own thread.</p><pre><code>urls = [f"https://jsonplaceholder.typicode.com/todos/{i}" for i in range(40)]
pool = ThreadPool(40)

r = requests.session()


def get(url):
    resp = r.get(url)
    results[i] = resp.json()


pool.map(get, urls)
pool.wait_completion()
</code></pre><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6MRW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6MRW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 424w, https://substackcdn.com/image/fetch/$s_!6MRW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 848w, https://substackcdn.com/image/fetch/$s_!6MRW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 1272w, https://substackcdn.com/image/fetch/$s_!6MRW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6MRW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Gen 2&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Gen 2" title="Python Gen 2" srcset="https://substackcdn.com/image/fetch/$s_!6MRW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 424w, https://substackcdn.com/image/fetch/$s_!6MRW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 848w, https://substackcdn.com/image/fetch/$s_!6MRW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 1272w, https://substackcdn.com/image/fetch/$s_!6MRW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F8fe4ae44-9e11-425f-95cc-ee9fd832470f_800x833.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Now we&#8217;re getting somewhere - 40 requests in 365ms, or 9.125ms per request. The same 1000 requests that would&#8217;ve taken 1m10s earlier now finishes in a little over nine seconds, or just about a 7x speed up. Not bad for a pretty naive implementation of threading. Can we get it even faster, though?</p><h2>Gen 3</h2><p>A few years back I was introduced to the library <code>aiohttp</code> - which is <a href="https://docs.aiohttp.org/en/stable/">Asynchronous HTTP Client/Server for asyncio and Python.</a> This leverages the new (at the time) async capabilities of python and lose the actual overhead of <code>Thread</code>s.</p><p>There is a bit of monkey patching I&#8217;ve had to do to make it work with all the various request types - specifically around its conformance to the cookie spec and to get it to work properly in jupyter notebook, where I like to play around with a lot of network requests.</p><p>Once we start looking at a pool of thousands of requests, we also want to be able to throttle ourselves - our laptops can only open so many TCP connections at once, and fire off so many bits in a given second. I defined a helper called <code>gather_with_concurrency</code> - it&#8217;s a way of using <code>asyncio</code>s gather with a semaphore, to limit the amount of tasks we work on in any given second.</p><p>This slows us down a bit due to the semaphore overhead, but if you&#8217;re doing anything in the alphanumeric 4 digit space you&#8217;re looking at 36^4, or 1.7m requests, which should probably be pooled and throttled a bit, and the overhead from the semaphore is worth it.</p><pre><code>async def gather_with_concurrency(n, *tasks):
    semaphore = asyncio.Semaphore(n)

    async def sem_task(task):
        async with semaphore:
            return await task

    return await asyncio.gather(*(sem_task(task) for task in tasks))
</code></pre><p>Next we set up the connector and the custom session</p><pre><code>conn = aiohttp.TCPConnector(limit=None, ttl_dns_cache=300)
session = aiohttp.ClientSession(connector=conn)
</code></pre><p>Here we limit each host to none - it won&#8217;t throttle itself internally, since we want to control that externally. We also bump up the dns cache TTL. For the purposes of this blog post this won&#8217;t matter, but by default it&#8217;s 10s, which saves us from the occasional DNS query.</p><p>Finally we define our actual <code>async</code> function, which should look pretty familiar if you&#8217;re already used to <code>requests</code>. We also disable SSL verification for that slight speed boost as well.</p><pre><code>async def get(url):
    async with session.get(url, ssl=False) as response:
        obj = await response.read()
        all_offers[url] = obj
</code></pre><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jpxb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jpxb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 424w, https://substackcdn.com/image/fetch/$s_!jpxb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 848w, https://substackcdn.com/image/fetch/$s_!jpxb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 1272w, https://substackcdn.com/image/fetch/$s_!jpxb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jpxb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Gen 3&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Gen 3" title="Python Gen 3" srcset="https://substackcdn.com/image/fetch/$s_!jpxb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 424w, https://substackcdn.com/image/fetch/$s_!jpxb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 848w, https://substackcdn.com/image/fetch/$s_!jpxb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 1272w, https://substackcdn.com/image/fetch/$s_!jpxb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F275590ba-7a97-4cd4-86eb-1ce04312d4a0_800x437.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Now we&#8217;re really going! 40 requests in 100ms, or 4ms per requests. We can do about 250 requests per second - however, at this speed, the overhead of the initial function set up and jupyter notebook is actually a significant portion of the overall cost.</p><p>If we bump it up to 4000 requests, we see that we actually get closer to a 1.574s execution time, or about 56% of the time it took us to make 10 requests iteratively.</p><p>We can make one request every 0.393ms, or 393 microseconds. We can blast through the entire alphanumeric space for a 4 character permutation in about 660 million microseconds, or 11 minutes, all from my MacBook pro.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cYww!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cYww!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 424w, https://substackcdn.com/image/fetch/$s_!cYww!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 848w, https://substackcdn.com/image/fetch/$s_!cYww!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 1272w, https://substackcdn.com/image/fetch/$s_!cYww!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cYww!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/ff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Gen 3 w/ 4000 requests&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Gen 3 w/ 4000 requests" title="Python Gen 3 w/ 4000 requests" srcset="https://substackcdn.com/image/fetch/$s_!cYww!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 424w, https://substackcdn.com/image/fetch/$s_!cYww!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 848w, https://substackcdn.com/image/fetch/$s_!cYww!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 1272w, https://substackcdn.com/image/fetch/$s_!cYww!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fff1d76df-8c81-41fa-9359-61a071e8bd74_800x659.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Our Threading implementation also benefits from the increase in pool - giving it a 100 threads (the same as the semaphore in the asyncio version) gives us a time to completion of 8s for 4000 urls, or a little over 2ms per URL. Nowhere near our aiohttp implementation but not terrible either.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!coF1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!coF1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 424w, https://substackcdn.com/image/fetch/$s_!coF1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 848w, https://substackcdn.com/image/fetch/$s_!coF1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 1272w, https://substackcdn.com/image/fetch/$s_!coF1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!coF1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/fd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;ThreadPool at 4000 requests w/ 100 threads&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="ThreadPool at 4000 requests w/ 100 threads" title="ThreadPool at 4000 requests w/ 100 threads" srcset="https://substackcdn.com/image/fetch/$s_!coF1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 424w, https://substackcdn.com/image/fetch/$s_!coF1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 848w, https://substackcdn.com/image/fetch/$s_!coF1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 1272w, https://substackcdn.com/image/fetch/$s_!coF1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd2b5719-cdfa-4edf-9cb0-fbf396700160_800x323.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>The same 36^4 requests using the ThreadPool would take 48 minutes, though.</p><p>We can clean up the code and optimize it slightly as well:</p><pre><code>import sys
import os
import json
import asyncio
import aiohttp


# Initialize connection pool
conn = aiohttp.TCPConnector(limit_per_host=100, limit=0, ttl_dns_cache=300)
PARALLEL_REQUESTS = 100
results = []
urls = ['https://jsonplaceholder.typicode.com/todos/1' for i in range(4000)] #array of urls

async def gather_with_concurrency(n):
    semaphore = asyncio.Semaphore(n)
    session = aiohttp.ClientSession(connector=conn)

    # heres the logic for the generator
    async def get(url):
        async with semaphore:
            async with session.get(url, ssl=False) as response:
                obj = json.loads(await response.read())
                results.append(obj)
    await asyncio.gather(*(get(url) for url in urls))
    await session.close()

loop = asyncio.get_event_loop()
loop.run_until_complete(gather_with_concurrency(PARALLEL_REQUESTS))
conn.close()

print(f"Completed {len(urls)} requests with {len(results)} results")
</code></pre><h2>Optimal semaphore size?</h2><p>If we bump our concurrent requests to 4k we see a drastic loss in performance.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hZ5k!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hZ5k!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 424w, https://substackcdn.com/image/fetch/$s_!hZ5k!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 848w, https://substackcdn.com/image/fetch/$s_!hZ5k!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 1272w, https://substackcdn.com/image/fetch/$s_!hZ5k!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hZ5k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Python Gen 3 semaphore w/ 4000 concurrent requests&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Python Gen 3 semaphore w/ 4000 concurrent requests" title="Python Gen 3 semaphore w/ 4000 concurrent requests" srcset="https://substackcdn.com/image/fetch/$s_!hZ5k!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 424w, https://substackcdn.com/image/fetch/$s_!hZ5k!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 848w, https://substackcdn.com/image/fetch/$s_!hZ5k!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 1272w, https://substackcdn.com/image/fetch/$s_!hZ5k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5e38c94a-63e8-4e4e-bd64-e6c1cdea0eb9_800x118.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>This is nearly a 3x slow down due to resource contention issues locally.</p><p>The optimal number will depend on your host - beefier set ups will have higher concurrency limits, and if you&#8217;re running this on a remote host on something like digital ocean you can crank this number up quite a bit.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SdNW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SdNW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 424w, https://substackcdn.com/image/fetch/$s_!SdNW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 848w, https://substackcdn.com/image/fetch/$s_!SdNW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 1272w, https://substackcdn.com/image/fetch/$s_!SdNW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SdNW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/ad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Finding the optimal semaphore value&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Finding the optimal semaphore value" title="Finding the optimal semaphore value" srcset="https://substackcdn.com/image/fetch/$s_!SdNW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 424w, https://substackcdn.com/image/fetch/$s_!SdNW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 848w, https://substackcdn.com/image/fetch/$s_!SdNW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 1272w, https://substackcdn.com/image/fetch/$s_!SdNW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8fec1f-ed62-4250-ad34-b12d91a522eb_800x312.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Interestingly enough the optimal semaphore value was right around 60.</p><p>I mostly do this locally at home, though, for my side projects - introducing parallelization and multiple hosts can get you numbers that are an order of magnituded better than this, but the purpose of this excercise is seeing what we can hit on a local machine with jupyter notebook.</p><h2>Gen 4</h2><p>Know of a faster implementation of an HTTP library that is stateful and works locally? <a href="https://twitter.com/jonluca">Let me know!</a></p><h2>HTTPX</h2><p><strong>Update - 6/17/21</strong></p><p>A <a href="https://lobste.rs/s/fxpne4/writing_fast_async_http_requests_python#c_pr0lfr">poster on lobste.rs</a> said that I should try out httpx. HTTPX is a modern implementation of a python web client.</p><p>Unfortunately, in my testing, it was strictly slower than aiohttp. I used their async library with the same sempahore restricting the number of processes ran, but it was still slower.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!q-A0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!q-A0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 424w, https://substackcdn.com/image/fetch/$s_!q-A0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 848w, https://substackcdn.com/image/fetch/$s_!q-A0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 1272w, https://substackcdn.com/image/fetch/$s_!q-A0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!q-A0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/6635483b-1738-486a-9f24-af01ba33a328_800x394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;HTTPX speeds&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="HTTPX speeds" title="HTTPX speeds" srcset="https://substackcdn.com/image/fetch/$s_!q-A0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 424w, https://substackcdn.com/image/fetch/$s_!q-A0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 848w, https://substackcdn.com/image/fetch/$s_!q-A0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 1272w, https://substackcdn.com/image/fetch/$s_!q-A0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6635483b-1738-486a-9f24-af01ba33a328_800x394.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>I also tried a native gather, with punting the concurrency down to the library - this did not help either.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LqCY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LqCY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 424w, https://substackcdn.com/image/fetch/$s_!LqCY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 848w, https://substackcdn.com/image/fetch/$s_!LqCY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 1272w, https://substackcdn.com/image/fetch/$s_!LqCY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LqCY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;HTTPX without a semaphore&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="HTTPX without a semaphore" title="HTTPX without a semaphore" srcset="https://substackcdn.com/image/fetch/$s_!LqCY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 424w, https://substackcdn.com/image/fetch/$s_!LqCY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 848w, https://substackcdn.com/image/fetch/$s_!LqCY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 1272w, https://substackcdn.com/image/fetch/$s_!LqCY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5824290d-7fca-40a9-8421-3c91c7313a96_800x152.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><h2>PyCurl</h2><p>Someone <a href="https://lobste.rs/s/fxpne4/writing_fast_async_http_requests_python">on that same lobste.er&#8217;s thread</a> suggested <a href="http://pycurl.io/">pycurl</a>.</p><p>PyCurl is different in that it feels like a pretty raw wrapper to <code>curl</code>. Writing the code felt more like dispatching actions and opening sockets than dealing with a nice http library.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xy8C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xy8C!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 424w, https://substackcdn.com/image/fetch/$s_!Xy8C!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 848w, https://substackcdn.com/image/fetch/$s_!Xy8C!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 1272w, https://substackcdn.com/image/fetch/$s_!Xy8C!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xy8C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/a09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;PyCurl implementation&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="PyCurl implementation" title="PyCurl implementation" srcset="https://substackcdn.com/image/fetch/$s_!Xy8C!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 424w, https://substackcdn.com/image/fetch/$s_!Xy8C!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 848w, https://substackcdn.com/image/fetch/$s_!Xy8C!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 1272w, https://substackcdn.com/image/fetch/$s_!Xy8C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa09850f9-b94b-4ca0-9329-e1b3786d1fcf_800x959.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>The results were impressive, but the aiohttp library was <em>still faster</em>. This was my first time writing a pycurl implementation, though, based on <a href="https://www.programmersought.com/article/26191793705/">this template</a> - introducing native threading might be able to speed it up, but I still haven&#8217;t seen anything faster than the 393 microseconds approach.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lSKy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lSKy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 424w, https://substackcdn.com/image/fetch/$s_!lSKy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 848w, https://substackcdn.com/image/fetch/$s_!lSKy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 1272w, https://substackcdn.com/image/fetch/$s_!lSKy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lSKy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/ee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;PyCurl results&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="PyCurl results" title="PyCurl results" srcset="https://substackcdn.com/image/fetch/$s_!lSKy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 424w, https://substackcdn.com/image/fetch/$s_!lSKy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 848w, https://substackcdn.com/image/fetch/$s_!lSKy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 1272w, https://substackcdn.com/image/fetch/$s_!lSKy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fee642f1f-81d0-4f6b-a592-1323d08160d6_800x220.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>If you know how to set up HTTPX or PyCurl in a way that&#8217;s faster let me know!</p><h2>UVLoop</h2><p>Addendum: 8/27/21 - I received an email from Steve telling me about uvloop, a faster, drop in replacement for asyncio&#8217;s event loop.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9d4J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9d4J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 424w, https://substackcdn.com/image/fetch/$s_!9d4J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 848w, https://substackcdn.com/image/fetch/$s_!9d4J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 1272w, https://substackcdn.com/image/fetch/$s_!9d4J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9d4J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;uvloop email&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="uvloop email" title="uvloop email" srcset="https://substackcdn.com/image/fetch/$s_!9d4J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 424w, https://substackcdn.com/image/fetch/$s_!9d4J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 848w, https://substackcdn.com/image/fetch/$s_!9d4J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 1272w, https://substackcdn.com/image/fetch/$s_!9d4J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7c0338a6-1a7a-489f-989f-e0e8f177841f_800x337.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>It doesn&#8217;t seem to have impacted the performance all that much - it did have lower variance, though. Across multiple runs of a regular asyncio event loop, I would get as high as 3s for the same 4000 requests; with uvloop, it never broke 2.1s.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_UTt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_UTt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 424w, https://substackcdn.com/image/fetch/$s_!_UTt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 848w, https://substackcdn.com/image/fetch/$s_!_UTt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 1272w, https://substackcdn.com/image/fetch/$s_!_UTt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_UTt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/b243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;speed results for uvloop&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="speed results for uvloop" title="speed results for uvloop" srcset="https://substackcdn.com/image/fetch/$s_!_UTt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 424w, https://substackcdn.com/image/fetch/$s_!_UTt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 848w, https://substackcdn.com/image/fetch/$s_!_UTt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 1272w, https://substackcdn.com/image/fetch/$s_!_UTt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb243790e-68ad-479d-b1f5-0b614b8a07bc_800x384.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>For a drop in replacement it seems pretty great - I don&#8217;t think it&#8217;ll help much at this stage, though, because most of the timing is due to the actual network call at this point. The threading and event loop implementation isn&#8217;t adding that much overhead, I&#8217;m guessing.</p>]]></content:encoded></item><item><title><![CDATA[A boilerplate for SSR’d Vite, React 17, and TypeScript 4.3]]></title><description><![CDATA[Introducing a barebones, slightly-opinionated boilerplate for working with a modern web stack written for 2021.]]></description><link>https://jonluca.substack.com/p/vite</link><guid isPermaLink="false">https://jonluca.substack.com/p/vite</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sun, 16 May 2021 16:49:29 GMT</pubDate><content:encoded><![CDATA[<p>Introducing a barebones, slightly-opinionated boilerplate for working with a modern web stack written for 2021. This takes the additional jump of allowing you to run your own server, for applications that are more complex or need more flexibility than Netlify or Nextjs can provide.</p><p><strong><a href="https://github.com/jonluca/vite-typescript-ssr-react">Repo</a></strong></p><ul><li><p><a href="https://reactjs.org/blog/2020/10/20/react-v17.html">React 17</a></p></li><li><p><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-3-rc/">Typescript 4.3</a></p></li><li><p><a href="https://vitejs.dev/guide/ssr.html">Vite with Vite SSR</a></p></li><li><p><a href="https://github.com/features/actions">GitHub Actions</a></p></li><li><p><a href="https://tailwindui.com/">Tailwind CSS</a></p></li><li><p><a href="https://prettier.io/">Prettier</a> &amp; <a href="https://eslint.org/">ESLint</a></p></li></ul><p>The SSR is accomplished through an Express server.</p><p> Your browser does not support the video tag.</p><p>Vite HMR</p><h2>Vite</h2><p>Vite HMR is baked into the server, so we get the blazingly fast code changes reflected on the client seen in the video above. The server rendering logic currently does not have any type of prop injection or initial state.</p><h2>Opinions</h2><p>This boilerplate is slightly opinionated, specifically around linting and code format. The TypeScript config is set up such as to allow for the greatest flexibility</p><p>When running in development mode, the application uses <code>ts-node</code> to natively run the Express server and load in the Vite HMR. In development, the rendering is still done client side. In production, all requests are server rendered. Read more on SSR on <a href="https://vitejs.dev/guide/ssr.html">the Vite docs</a>.</p><p>There is no selected front end state management library or back end database. There is a simple React Context for basic state.</p><h2>Hosting</h2><p>This assumes a traditional hosting infrastructure. The goal of the project is to provide as little lock-in as possible. You&#8217;re required to set up the deployment infrastructure, as well as containerizing the application.</p><p><a href="https://github.com/jonluca/vite-typescript-ssr-react">You can get this boilerplate here.</a></p>]]></content:encoded></item><item><title><![CDATA[How to redeem $2000 of HNS for being a FOSS developer]]></title><description><![CDATA[If you were an active FOSS developer in 2019, you were gifted ~4,246 HNS, which as of May 2021 is worth 0.0359087 BTC, or $2k USD.]]></description><link>https://jonluca.substack.com/p/hns-airdrop</link><guid isPermaLink="false">https://jonluca.substack.com/p/hns-airdrop</guid><dc:creator><![CDATA[jonluca]]></dc:creator><pubDate>Sun, 09 May 2021 21:22:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pzWo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you were an active FOSS developer in 2019, you were gifted ~4,246 HNS, which as of May 2021 is worth 0.0359087 BTC, or $2k USD. <a href="https://handshake.org/claim/">Handshake wanted to reward FOSS developers by gifting handshake tokens (HNS).</a> You can redeem these pretty easily, and either use them to support open source projects, or redeem them as bitcoin and cash them out.</p><h2>Prerequisites</h2><p>Roughly 250,000 GitHub users qualified. You must have had at least <strong>15 followers</strong> during the week of <strong>2019-02-04</strong>. If you didn&#8217;t have a GitHub account at the time, or didn&#8217;t have 15 followers, or didn&#8217;t have a valid public SSH key uploaded, you were not eligible for the airdrop.</p><h2>First steps</h2><p>Download hs-airdrop and register for an account on https://namebase.io. Namebase will create a free HNS wallet for you, which you can easily transfer to BTC.</p><pre><code>git clone https://github.com/handshake-org/hs-airdrop.git &amp;&amp; cd hs-airdrop &amp;&amp; npm install
</code></pre><p>Next go to https://www.namebase.io/ and find your wallets address.</p><p>To redeem your HNS you&#8217;ll need:</p><ul><li><p>The <code>hs-airdrop</code> binary</p></li><li><p>The path to your private key</p></li><li><p>Your wallet address</p></li></ul><p>Namebase also has similar instructions on redeeming these coins at https://www.namebase.io/airdrop.</p><h2>SSH</h2><p>If you want to use SSH, first confirm that the key you want to use is registered on GitHub, and was associated with your account on February 4th, 2019.</p><p>List all your keys with</p><pre><code>$ ls ~/.ssh/
</code></pre><p>Most likely it will be named <code>id_rsa</code>. Then run the hs-airdrop binary with the path to your private key.</p><pre><code>./bin/hs-airdrop --bare ~/.ssh/id_rsa hs1...&lt;your address from namebase&gt; 0.1
</code></pre><p>It will ask you to decrypt your private key. Note that the private key is <em>never sent anywhere</em> - you can verify this yourself from the source code of hs-airdrop, here: https://github.com/handshake-org/hs-airdrop. This will then proceed to submit your confirmation and check if it is in the airdrop tree. If it is, you&#8217;ll get a base64 confirmation, which you&#8217;ll submit below.</p><h2>PGP</h2><p>PGP is a little more tricky - the documentation on the repo is a bit lacking.</p><p>On MacOS, using PGP Suite, you can do the following.</p><p>First, get all your available keys:</p><pre><code>$ gpg --list-keys
/Users/jonlucadecaro/.gnupg/pubring.kbx
---------------------------------------
pub   rsa4096 2017-08-24 [SC] [expires: 2033-08-20]
      849E61D17094A964866AE510E6DC4811DD593AC7
uid           [ultimate] JonLuca De Caro &lt;jonlucadecaro96@gmail.com&gt;
sub   rsa4096 2017-08-24 [E] [expires: 2033-08-20]
</code></pre><p>The key ID is the value below the <code>pub</code> key (in the case of the above, <code>849E61D17094A964866AE510E6DC4811DD593AC7</code>). You now want to export the private key, replacing my public key ID with yours.</p><pre><code>$ gpg --armor --export-secret-keys 849E61D17094A964866AE510E6DC4811DD593AC7 &gt; ~/Desktop/sec.asc
</code></pre><p>It will ask you for the keys password.</p><p>to check if your key is in the airdrop tree, you next run, again replacing my key with yours:</p><pre><code>./bin/hs-airdrop --bare ~/Desktop/sec.asc 849E61D17094A964866AE510E6DC4811DD593AC7 hs1...&lt;your address&gt; 0.1
</code></pre><p>This will tell you if your key is in the airdrop tree, and if it is, it will print out a base64 string with your airdrop redemption.</p><h2>Submitting your confirmation</h2><p>If your SSH or public key was in the airdrop tree, it will print a base64 string. Go to https://www.namebase.io/airdrop and paste it into the 5th box.</p><p>If it was valid, you&#8217;ll receive your HNS in roughly 16 hours.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pzWo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pzWo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 424w, https://substackcdn.com/image/fetch/$s_!pzWo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 848w, https://substackcdn.com/image/fetch/$s_!pzWo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 1272w, https://substackcdn.com/image/fetch/$s_!pzWo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pzWo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Namebase Balance&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Namebase Balance" title="Namebase Balance" srcset="https://substackcdn.com/image/fetch/$s_!pzWo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 424w, https://substackcdn.com/image/fetch/$s_!pzWo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 848w, https://substackcdn.com/image/fetch/$s_!pzWo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 1272w, https://substackcdn.com/image/fetch/$s_!pzWo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F99a286b7-4a36-426d-b074-67a4403eb9c0_800x453.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>You can then proceed to sell it on namebase.</p><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JlHS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JlHS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 424w, https://substackcdn.com/image/fetch/$s_!JlHS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 848w, https://substackcdn.com/image/fetch/$s_!JlHS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 1272w, https://substackcdn.com/image/fetch/$s_!JlHS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JlHS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Namebase Sell&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Namebase Sell" title="Namebase Sell" srcset="https://substackcdn.com/image/fetch/$s_!JlHS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 424w, https://substackcdn.com/image/fetch/$s_!JlHS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 848w, https://substackcdn.com/image/fetch/$s_!JlHS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 1272w, https://substackcdn.com/image/fetch/$s_!JlHS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7b38f41a-dea9-48d2-8467-4cbd218c6b84_800x299.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><p>Just paste in whatever BTC balance you want to send it to, and it&#8217;ll show up in a few minutes to hours. You can also use the coins to bid on names in namespace, or to support additional FOSS projects!</p><h3>Multiple keys</h3><p>Note that if you have multiple SSH keys, it will only allow you to redeem for one. It will create valid nonces for each key, but once you redeem one in the chain, it invalidates the rest. If you submit the base64 for it it will look like it was accepted, but it will be rejected by the miners</p><h2>Other Airdrops</h2><ul><li><p>Keybase did an XLM drop worth about $1000 USD as of May 2021 - check your https://keybase.io account</p></li><li><p>Uniswap did a 400 ETH drop worth about $16,000 USD as of May 2021 - https://airdrops.io/uniswap/</p></li></ul>]]></content:encoded></item></channel></rss>