[{"data":1,"prerenderedAt":188},["ShallowReactive",2],{"navigation":3,"/blog/gospendl-builder-story":46,"/blog/gospendl-builder-story-surround":183},[4],{"title":5,"path":6,"stem":7,"children":8,"page":45},"Blog","/blog","blog",[9,13,17,21,25,29,33,37,41],{"title":10,"path":11,"stem":12},"ESLint Without Prettier: One Tool for Formatting and Linting in TypeScript","/blog/eslint-best-practice","blog/eslint-best-practice",{"title":14,"path":15,"stem":16},"Building GoSpendl: A Bitcoin Product Search Engine, End to End","/blog/gospendl-builder-story","blog/gospendl-builder-story",{"title":18,"path":19,"stem":20},"Why Your Startup Should Go Headless CMS from Day One","/blog/headless-cms-startups","blog/headless-cms-startups",{"title":22,"path":23,"stem":24},"How I Built a Token-Efficient Crawler for 900+ Shops with Crawl4AI","/blog/how-i-built-a-token-efficient-crawler","blog/how-i-built-a-token-efficient-crawler",{"title":26,"path":27,"stem":28},"Migrating a Live B2B Platform to AWS Without Stopping the Business","/blog/migrating-live-b2b-platform-aws","blog/migrating-live-b2b-platform-aws",{"title":30,"path":31,"stem":32},"Nuxt 4 WebSockets Are Elegant. Here's Why They Break on Vercel.","/blog/nuxt-websockets-vs-vercel","blog/nuxt-websockets-vs-vercel",{"title":34,"path":35,"stem":36},"Host Your Docker Container on Railway in 5 Minutes (8GB RAM for $5)","/blog/railway-smart-docker-host","blog/railway-smart-docker-host",{"title":38,"path":39,"stem":40},"Supabase vs Custom Auth: What I Recommend for Early-Stage SaaS","/blog/supabase-vs-custom-auth","blog/supabase-vs-custom-auth",{"title":42,"path":43,"stem":44},"How to Migrate WordPress to Strapi Headless CMS (With 1,000+ Posts)","/blog/wordpress-to-strapi-migration","blog/wordpress-to-strapi-migration",false,{"id":47,"title":14,"author":48,"body":52,"date":173,"description":174,"extension":175,"image":176,"meta":177,"minRead":178,"navigation":179,"path":15,"seo":180,"sitemap":181,"stem":16,"__hash__":182},"blog/blog/gospendl-builder-story.md",{"name":49,"avatar":50},"Niklas Grieger",{"src":51,"alt":49},"/profile.jpg",{"type":53,"value":54,"toc":162},"minimark",[55,60,64,75,78,82,85,88,91,95,98,105,108,112,115,122,126,133,137,140,143,146,150,153],[56,57,59],"h2",{"id":58},"where-the-idea-came-from","Where the idea came from",[61,62,63],"p",{},"A friend of mine uses Satsback regularly - a rewards platform that pays out a bit of Bitcoin (sats) back on purchases made through its partner shops. His complaint was always the same: you have to already know which partner shop you want, go there first, and search from inside it. There's no way to just search \"iPhone\" and see it across every participating shop at once, let alone compare prices between them.",[61,65,66,67,74],{},"That's the whole idea behind ",[68,69,73],"a",{"href":70,"rel":71},"https://gospendl.com",[72],"nofollow","GoSpendl",": search once, see which shops have it, what the satsback rate is at each one, and compare prices - instead of checking shops one at a time.",[61,76,77],{},"I built it solo, end to end - frontend, crawling infrastructure, search, hosting. This post is the build story: what I built, what broke, and what I'd do differently.",[56,79,81],{"id":80},"the-hard-part-isnt-the-search-box","The hard part isn't the search box",[61,83,84],{},"The obvious version of this product is easy: a Nuxt frontend, an Algolia index, done. The actual hard part is upstream of that - getting accurate, current product data out of hundreds of shops that all look completely different, without it costing a fortune or falling over the moment a shop redesigns its page.",[61,86,87],{},"That single problem shaped almost every technical decision in this project.",[89,90],"gospendl-architecture-overview",{},[56,92,94],{"id":93},"crawling-without-burning-the-token-budget","Crawling without burning the token budget",[61,96,97],{},"My first instinct was to point an LLM at each shop's page and let it figure out the product data. That works fine for one or two shops. It does not work for hundreds, triggered live on every user search - it turns into a token cost problem fast.",[61,99,100,101,104],{},"What I landed on was a two-phase approach: use an LLM once per shop, offline, to generate a reusable CSS extraction schema. Every real crawl after that is pure CSS extraction against the stored schema - zero LLM calls, zero token cost, per request. ",[68,102,103],{"href":23},"I wrote up the full details separately",", including the failure modes that cost me the most time - feeding an LLM cleaned HTML instead of raw HTML silently breaks pattern recognition, and an LLM without an explicit \"give up if you can't find a pattern\" instruction will confidently hallucinate CSS selectors that match nothing.",[61,106,107],{},"On a sample of 30 shops, about a third produced a working schema on the first attempt, and the plausibility checks I built in caught most of what would otherwise have been bad data reaching search.",[56,109,111],{"id":110},"realtime-and-the-adapter-that-didnt-exist-yet","Realtime, and the adapter that didn't exist yet",[61,113,114],{},"The other piece I wanted was realtime: when a crawl job finds new results for something you're searching, you should see it without refreshing. Nuxt 4's native WebSocket API looked perfect for this - minimal setup, worked flawlessly locally.",[61,116,117,118,121],{},"Deployed to Vercel, it just didn't connect. Turned out this wasn't a Vercel limitation at all - ",[68,119,120],{"href":31},"the gap was one layer down",", in Nitro's Vercel preset, which doesn't yet implement the WebSocket upgrade handshake that Nuxt 4 depends on. I ended up rebuilding that layer on Server-Sent Events over Redis pub/sub instead, which turned out to solve a second problem for free - it doesn't care which serverless instance is handling which request, unlike the in-memory WebSocket approach I'd started with.",[56,123,125],{"id":124},"self-hosting-the-crawler-for-5-a-month","Self-hosting the crawler for $5 a month",[61,127,128,129,132],{},"Crawl4AI, the crawler I use, needs a real Chromium browser under the hood - at least 4GB of RAM just to run. My existing VPS had exactly 4GB, total, for everything. ",[68,130,131],{"href":35},"Railway ended up being the answer",": point it at a Docker image, no server config, no SSH, and the Hobby plan gives 8GB RAM for $5/month - a fraction of what a comparable VPS upgrade or a DigitalOcean droplet would have cost, with none of the ongoing maintenance.",[56,134,136],{"id":135},"login-without-a-login-form","Login without a login form",[61,138,139],{},"The satsback rewards themselves come through a partnership with Satsback, who gave GoSpendl API access to their partner-shop network. Their side of a purchase is straightforward: go to a partner shop through their browser extension or a redirect link, buy something, and a percentage comes back in sats to your Satsback account.",[61,141,142],{},"The interesting part was authentication. There's no email/password, no OAuth - it's entirely Nostr-based. If you have a Nostr browser extension (nos2x, Alby), GoSpendl asks it to sign a small event, sends that signed event to Satsback, and gets back a token for your session. No account creation, nothing to remember - if you already have a Nostr identity, that's your login.",[61,144,145],{},"It only fires the first time you actually click a satsback-enabled product, not on every page load, so most people browsing and comparing prices never trigger it at all.",[56,147,149],{"id":148},"where-it-stands","Where it stands",[61,151,152],{},"The crawler works. The realtime layer works. The search works. Technically, this is the part of the project I'm proudest of - it's a genuinely solid piece of infrastructure, and I learned more building the crawling and realtime layers than on most client projects I've billed for.",[61,154,155,156,161],{},"GoSpendl itself is on pause right now while I figure out what's next for it. The code is live and open source if you want to look under the hood: ",[68,157,160],{"href":158,"rel":159},"https://github.com/devonik/go-spendl",[72],"github.com/devonik/go-spendl",".",{"title":163,"searchDepth":164,"depth":164,"links":165},"",2,[166,167,168,169,170,171,172],{"id":58,"depth":164,"text":59},{"id":80,"depth":164,"text":81},{"id":93,"depth":164,"text":94},{"id":110,"depth":164,"text":111},{"id":124,"depth":164,"text":125},{"id":135,"depth":164,"text":136},{"id":148,"depth":164,"text":149},"2026-08-06T00:00:00.000Z","A product search platform for the Bitcoin community, built solo - the crawler that scales without burning LLM tokens, the realtime layer that broke on Vercel, the $5/month host that runs it all. Here's the whole build, and where it stands now.","md","/blog/gospendl-builder-story-hero.png",{},7,true,{"title":14,"description":174},{"loc":15},"AFptR6pjBlNxNfDRpbVl1sHr_Hq9Q2QuFwNLve1KHWY",[184,186],{"title":10,"path":11,"stem":12,"description":185,"children":-1},"Skip the Prettier setup. This lightweight ESLint config handles both formatting and linting for TypeScript projects — ready to install in 5 minutes.",{"title":18,"path":19,"stem":20,"description":187,"children":-1},"Most startups pick WordPress or a monolithic CMS because it's fast. 18 months later, they're paying to undo that decision. Here's why headless is the smarter choice from the start.",1785343074209]