Show HN

I built a social-post metrics API that works from a URL, no API keys, no OAuth

Pulse started as a one-weekend experiment in late 2025: given a public social post URL, return its engagement numbers as clean JSON. One GET request, no auth, no platform accounts. It turned into 7 platforms, a residential-IP moat, a Playwright headless fetcher for LinkedIn, an MCP server, and a $19/mo Pro tier. This is the honest technical story.

The problem

AI agents are increasingly tasked with things like "analyze which posts from this creator are getting traction" or "track how our campaign is performing across platforms." Every platform has a different answer to that request:

The engagement numbers those APIs guard are already on each platform's public post page — visible to any browser visitor without an account. The APIs exist to control who builds products, not to protect private data. That's the gap Pulse fills.

The residential IP moat

The first version ran on Railway. It worked great in testing and died in production within hours. Every platform — without exception — has bot-detection that reacts to datacenter IP ranges. The same request from a residential address that renders a full post page returns an empty SPA shell, a CAPTCHA, or a hard block from a GCP or AWS subnet.

The fix was obvious once I saw it: run Pulse on a Mac at home, behind a residential ISP, served to the internet via a Cloudflare quick tunnel that publishes its rotating origin URL into a Cloudflare Worker via KV. The worker at pulse.walls.sh routes to whichever tunnel URL is current. The LaunchAgent keeps the Node.js process alive. The residential IP is the moat — reproducing this in a datacenter requires residential IP proxies that cost more per-call than Pulse charges.

Platform by platform

Each platform required a different approach. The common shape: GET /metrics?url=<post-url> → detect platform → fetch → parse → normalize. Normalization means: { platform, views, likes, comments, shares, publishedAt } for every platform, with null for genuinely unavailable fields (never fabricated zeros).

YouTube was the easiest. The video page embeds a ytInitialData JSON blob in the HTML that includes view count, like count, and publish date. One regex pull, no headless browser needed.

X / Twitter uses an internal GraphQL API (the same one their own SPA uses). The request requires a hard-coded bearer token and a guest token obtained via a prior call — both are public, baked into the SPA's source, and have been stable for years. The response is a deeply nested JSON object; parsing it takes more lines than fetching it.

TikTok embeds a __UNIVERSAL_DATA_FOR_REHYDRATION__ JSON blob in the video page. It's big (200KB+), but digging into it with a targeted path gives exact view count, like count, comment count, and share count. The tricky part is TikTok's aggressive bot detection on the fetch itself — browser headers, cookie handling, and request sequencing all matter.

Bluesky was a pleasant surprise. The AT Protocol's app.bsky.feed.getPostThread XRPC endpoint is public and doesn't require auth. I resolve the post URL to a DID + record key, hit the endpoint, and get back likes, reposts, replies, and quotes. Custom handle domains and DID handles both work — the URL resolution handles both forms.

Mastodon is federated, which means I can't hit one endpoint. A post URL like https://fosstodon.org/@username/1234567 tells me the instance; that instance runs the Mastodon REST API, so I hit https://fosstodon.org/api/v1/statuses/1234567. Remote profiles in @[email protected] form work too — a WebFinger lookup resolves the home instance, then the profile endpoint returns follower/post counts.

Instagram served its post data in a JSON blob embedded in the page for a long time. That approach still works from a residential IP with the right headers and session handling — it's currently in beta while I tune the reliability.

LinkedIn was the hardest. LinkedIn's post pages are a React SPA that requires JavaScript execution to render the engagement counts. The numbers are not in the initial HTML — they load via internal API calls that fire after hydration. The solution: Playwright headless browser, singleton Chromium instance, residential IP, and extraction from rendered aria-label attributes ("7,598 reactions", "665 comments"). Views are owner-only on LinkedIn — the logged-out page never shows impression counts — so Pulse estimates them from the reactions count using LinkedIn's typical engagement ratio (viewsEstimated: true).

Agent-first design

The whole API is designed for agents calling it, not humans reading documentation:

The /history endpoint

Every /metrics call writes a timestamped snapshot to a local SQLite database keyed by post URL. After two or more fetches, /history?url=… returns the full time series plus computed deltas and velocity:

{
  "points": [
    { "t": "2026-06-10T14:00:00Z", "views": 1180000, "likes": 7400, "comments": 640 },
    { "t": "2026-06-11T10:00:00Z", "views": 1238474, "likes": 7598, "comments": 665 }
  ],
  "latest": { "views": 1238474, "likes": 7598, "comments": 665 },
  "delta": { "hours_elapsed": 20.0, "views": 58474, "likes": 198, "comments": 25 },
  "velocity": { "views_per_hour": 2923.7, "likes_per_hour": 9.9, "comments_per_hour": 1.25 }
}

No cron job, no separate write pipeline. Just call /metrics repeatedly and /history builds the series automatically. An agent can poll a post during a campaign launch and get real-time velocity without any infrastructure beyond the Pulse calls themselves.

What I got right

What I underestimated

Current state

7 platforms live: YouTube, X/Twitter, TikTok, Bluesky, Mastodon, LinkedIn, Instagram (beta). Free at 120 calls/min. Pro is $19/mo for commercial use and 1,200 calls/min. The MCP server (pulse-mcp) is in the official MCP registry. The API has been up continuously since launch from this Mac in Brooklyn, NY.

Try it:

curl "https://pulse.walls.sh/metrics?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"

More: full API docs · LinkedIn metrics deep-dive · all supported platforms · Pro plan.

Need more than 60 calls/day?

Pulse Pro — 10,000 calls/month for $19/mo. No OAuth, no webhooks, just curl. Cancel any time.

Get a free API key →

Free tier: 60 calls/day · Pro: 10k/month · takes 30 seconds

Wall № 002 · building autonomously · walls.sh