Social proof is good when you have a new app. I wanted people to know that others were using the app. I literally asked Claude for a 90s style website counter. I used the example of the famous burger sign that says, "billions served."

Image source: The Flintstones Wiki on Fandom
side note- this movie was so magical when I was a little kid!
Turned out to be a good little rabbit hole, so I figured it was worth writing
up on its own — not just "here's a counter," but what actually broke along
the way and why.
🔗 See it in action: https://theplaidscientist.github.io/dailydoodle/
💻 Code: https://github.com/theplaidscientist/dailydoodle
Attempt 1: a free, no-signup counter API
Daily Doodle
is a static site on GitHub Pages — no backend, no server I control. So the
first move was a free public counter service
(countapi.mileshilliard.com) — no
account, no API key, just a GET request that increments a number tied to a
key I made up:
fetch(`https://countapi.mileshilliard.com/api/v1/hit/${COUNTER_KEY}`)
.then(r => r.json())
.then(data => { counterEl.textContent = data.value; });
Worked immediately on desktop. Yay! It's working! This is gonna be so cool.
Then it quietly stopped working on mobile
I switched to my phone before sending the link to my friend and realized it was still at triple ---. No matter what I did, I couldn't get the counter to update..
The counter would just... not move on mobile. No error the user would ever
see, because I'd deliberately built it to fail silently (dashes on screen
instead of a broken-looking blank) rather than break the actual app if the
counter service ever had a bad day.
First fix I tried: fire the request two ways at once — the normal fetch()
call, plus a fallback using an <img> tag pointed at the same endpoint,
since some ad blockers treat image requests differently than fetch/XHR calls:
const pixel = new Image();
pixel.src = `https://countapi.mileshilliard.com/api/v1/hit/${COUNTER_KEY}?_=${Date.now()}`;
Didn't help. Which was actually useful information — if both request types
fail identically, that's not a request-type problem, that's the whole
domain being blocked at the network level (an ad blocker, a mobile
carrier's filtering, a DNS-level blocklist like NextDNS/AdGuard). Generic
counter/analytics-sounding domains get swept up in filter lists a lot more
than people realize.
Attempt 2: Firebase instead
The fix wasn't cleverer code — it was picking a backend domain that's
essentially never blocklisted, because too much of the internet depends on
it. Firebase fit: firebaseio.com is Google infrastructure that a huge
number of mainstream apps rely on, so blocklists generally leave it alone.
Setup, for anyone who wants to do this on their own static site:
- Create a free project at Firebase Console (no credit card needed for the free Spark plan)
- Add a Realtime Database, start it in test mode (public read/write — fine for something as low-stakes as a number)
- Use Firebase's REST API directly, no SDK, no auth needed in test mode:
// Read the current count
fetch(`${DB_URL}/counters/dailyDoodle.json`)
.then(r => r.json())
.then(value => { counterEl.textContent = value || 0; });
// Increment it atomically (safe even if two people spin at once)
fetch(`${DB_URL}/counters/dailyDoodle.json`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ '.sv': { 'increment': 1 } })
})
.then(r => r.json())
.then(value => { counterEl.textContent = value; });
That .sv: { increment: 1 } bit is Firebase's server-side increment — the
math happens on their server, not in the browser, so there's no race
condition if two people hit spin at the same moment.
The honest tradeoff
Test mode means the database is publicly writable by anyone who finds the
URL — genuinely fine for a number nobody can really abuse in a meaningful
way, but worth knowing if you're reusing this pattern for anything with
actual sensitive data.
Sources / further reading
- countapi.mileshilliard.com — the free counter service I started with
- Firebase Realtime Database REST API docs
- Firebase Console
Have you had this problem? Did you solve it similarly or differently?
Top comments (2)
Haha, yip, mobile strikes again! Had a similar issue with a WPA app, worked fine directly in browser, but the moment it used a MAUI wrapper, it breaks... No errors, no weird glitches, nothing, turns out the issue was that the initialize would get blocked by the router, even though it's internal traffic and ports were opened. Weird how sometimes the issue isnt what you built, but what it's running on... Though by me it was a hello world page that broke the silence and got me the info I needed. The MAUI app's request for the PWA site, was hitting the http, instead of the https site (cuz it wasnt signed), which ended up causing the router to block it for some reason. Switched network, it worked, used wireguard it worked, it was that exact configuration of app over wifi, pc via ethernet to the same router that would fail. Never really dug into more exactly what caused the initialize method to bork it, but my suspicion is that it was fetching the whole PWA assembly, which maybe triggered some obscure safety feature that blocks large TCP payloads? Idk, all I know is that once it was signed and the packets could authenticate over https, it worked 😂 somedays it's better to take the small wins, than spend days figuring out why it happened in the first place...
Small wins all day! Hahaha