Nischal Tiwari

From the scorer’s tap to a live Facebook post: building an over-summary automation

How I wired WordPress, n8n and the Facebook Graph API so one button publishes a cricket over to social — and the page-token gotcha that ate an afternoon.

During a live cricket match, the most valuable thing we can publish is an over that just changed the game — a tight final over, a wicket maiden, a chase that flipped. By the time someone published the score into Facebook by hand, the moment was already gone. I wanted the person scoring the match to be able to publish that over with a single tap, and have a properly written caption land on our Facebook page on its own.

This is the story of how I built that — the plan, the dead end I inherited, the rebuild, and the one authentication detail that cost me an afternoon.

The problem I started with

cricnepal runs a custom live-scoring engine inside WordPress (a mu-plugin we call the cn-engine). The scorer enters every ball; the public match centre reads from it. So the data was already there. What was missing was the bridge to social.

There was an old bridge, technically. WordPress had a settings field — “Live Match Over Summaries” — and a webhook URL sitting in it. I assumed it worked. It did not. When I traced the code, the option was registered and rendered in the admin, but nothing ever fired it. It was dead wiring: a switch screwed to the wall with no cable behind it. The only webhooks that actually POSTed were the on-publish share and a manual alert — neither of which triggered on an over ending.

So I was not fixing a broken feature. I was building one, and inheriting a misleading settings field that made it look solved.

Lesson I keep relearning: “there’s already a field for it” is not the same as “it works.” Grep the trigger, not the setting.

How I planned it

I wrote down three decisions before touching any code.

  1. Operator-gated, not automatic. My first instinct was to fire on every over end. I killed that idea fast — six posts an over-pair, every over, is spam, and it strips away editorial judgement. Instead: the scorer gets a “Post this over” button. It defaults to the last completed over. The human decides which over is worth it.
  2. PHP owns the caption. The old make.com scenario I inherited composed the caption inside the automation tool, using nested formula logic like if(length(batters[2].name) > 0; ...). That is brittle, untestable, and invisible to version control. I decided the WordPress side would compose a ready-to-post caption string and send it. The automation tool would do nothing clever — just take {{caption}} and post it. Belt and suspenders: I’d also send the structured over data, so a future channel could reformat if needed.
  3. The receiver is swappable. WordPress would POST to a configurable webhook URL. Whatever sat on the other end — make.com, n8n, anything — was a config flip, not a code change.

Choosing the receiver: why n8n, not make.com

I prototyped against make.com first because the inherited scenario lived there. Then I hit the wall everyone hits on the free tier: two active scenarios, maximum. I already wanted room for Twitter/X as a second route, plus other automations down the line. Two slots was a non-starter.

I looked at the alternatives and landed on n8n, self-hosted:

  • It’s free for internal use under its Sustainable Use License.
  • I could run it on a box we already pay for — no per-operation credits ticking away.
  • It speaks plain webhooks and has native nodes for the Graph API.

The trade was that I’d own the hosting. Given we already run a Dedicated box behind RunCloud, that was a trade I was happy to make.

The architecture, end to end

Scorer taps "Post this over"
        │
        ▼
WordPress REST  POST /match/{id}/cn-engine/over-summary-share
        │   (builds caption in PHP, reuses the over-summary builder)
        ▼
Outbound webhook  →  https://automate.cricnepal.com/webhook/...
        │   (non-blocking, fail-soft — a social hiccup never stalls scoring)
        ▼
n8n  Webhook trigger  →  Facebook Graph node  →  POST /{page-id}/feed
        │
        ▼
Live post on the cricnepal Facebook page

The two things I care about most in that diagram: the WordPress call is non-blocking and fail-soft (if the webhook is down, the scorer’s match never notices), and the caption is built before it leaves WordPress.

Step 1 — The WordPress side

I added one new file to the cn-engine, over-summary-webhook.php, that does three jobs:

  1. Build the payload. It reuses the existing over-summary builder, so the runs, the ball sequence, the bowler’s figures, and the batters at the crease all come from the same source the match centre already trusts.
  2. Compose the caption in PHP. This is the part I deliberately pulled out of the automation tool. The wording lives in git, where I can read it, test it, and change it in a pull request.
  3. Send it, safely. A non-blocking POST to the configured hook, logging on failure and never throwing into the scoring flow.

Here’s the shape of the caption the function emits:

🏏 Australia vs India
End of Over 10 • AUS 30/5
4 runs • 1 0 0 W 2 1
Zaheer Khan 12/2 (4.0 ov)
Ricky Ponting 18* (22) · Adam Gilchrist 4 (8)
#AUSvIND #LiveCricket

To prove it before anything went near a live page, I ran the builder against a real local match with wp eval and inspected the caption and the structured payload. Composing the text in PHP made this trivial — it’s just a function returning a string, so I could test it without ever calling Facebook.

On the scorer’s screen I added a small post-over-button.tsx component — “Post this over” — that reads the match store, defaults to the last completed over, calls the REST endpoint, and shows the result inline. Nothing fancy. The whole point was that it’s one tap.

Step 2 — Standing up n8n on the box

This is where most of the unglamorous work lived. n8n needed a public HTTPS endpoint, a process manager, and a reverse proxy. On RunCloud, the sequence that finally worked:

  • Created a web app automate.cricnepal.com with Let’s Encrypt SSL.
  • Switched the stack to “Native NGINX + Custom config.” This was the first trap. The default hybrid stack auto-generates an nginx→apache proxy config whose directives collide with the reverse-proxy I needed — nginx refused to reload with a duplicate error. Native NGINX gave me a clean slate.
  • Added a reverse-proxy config pointing nginx at 127.0.0.1:5678, with the websocket upgrade headers n8n needs for its editor.
  • Ran n8n under Supervisor as the runcloud user, with its environment (host, port, timezone, and the all-important N8N_ENCRYPTION_KEY) set in the supervisor config.

Back up the encryption key. Losing it bricks every stored credential. I wrote it down somewhere safe before I trusted anything to it.

Once the public URL returned n8n’s UI over HTTPS, the infrastructure was done. The rest was inside n8n: a Webhook trigger node, and a Facebook Graph node to post to the page feed.

Step 3 — The authentication detail that cost me an afternoon

Everything was built. The button worked. The webhook fired. n8n received it. And Facebook returned:

(#200) This endpoint requires the 'pages_read_engagement' permission
or the 'Page Public Content Access' feature... requires a page access token.

Here is the trap, stated plainly, because I want to save the next person the afternoon I lost:

Posting to a Page’s /feed requires a Page access token — not a user token, and not a system-user token. They look identical. They’re both long opaque strings. n8n’s credential test will even say “Connection tested successfully” with the wrong one, because the test only does a basic GET, and a basic GET passes for almost any valid token. Only a real Page token can actually post.

There’s a second subtlety on top of it: a Page token inherits the expiry of whatever it was derived from. Derive it from a token that expires in 60 days, and your automation silently dies in 60 days.

The fix, and the reason this is now durable:

  1. I used a system-user token, which is non-expiring.
  2. I derived the Page token from it with a single Graph call:GET /{page-id}?fields=access_token&access_token={system_user_token}The access_token that comes back is a non-expiring Page token — because its source never expires.
  3. I pasted that token into the n8n Facebook credential.

A few myths I had to clear along the way:

  • You do not need App Review to post to a Page you own. Business-owned pages skip it. I lost time chasing a review I didn’t need.
  • The Graph Explorer’s “Get Page Access Token” dropdown won’t always surface business-owned pages — a UI quirk. The ?fields=access_token query works regardless.
  • The credential connecting cleanly is not proof it can post. Test the real action, not the handshake.

After that, the scorer tapped the button and End of Over 10 • BNMC 30/5 appeared on the page. End to end. From a tap inside WordPress to a live post, no human in the middle.

What I’d tell myself before starting

  • Trace the trigger, not the setting. A configured field told me a feature existed. The feature did not exist.
  • Put the words in the codebase. Moving caption composition from the automation tool into PHP made it testable, reviewable, and reproducible. Brittle in-tool formula logic is a liability.
  • Make the integration fail-soft. Scoring a live match must never wait on, or break because of, a social post.
  • For Facebook: page token, derived from a non-expiring source. Frame it on the wall.
  • Gate it with a human. Automation removed the labour, not the judgement. The scorer still decides which over deserves the spotlight.

The feature is small — one button, one post. But it closed the gap between the moment a match turns and the moment our audience sees it. That gap was the whole point.

Built for cricnepal. Stack: WordPress (cn-engine mu-plugin), n8n on RunCloud/Hetzner, Facebook Graph API.

Case Study: Migrating a Live WordPress Cricket Platform to Headless React — With Zero Downtime

The brief: take CricNepal — a high-traffic, live-scoring cricket site running as a single WordPress theme — and turn it into a modern, headless platform. The constraints: it’s indexed, it’s live during matches, and it can never go dark.

We treated it as an architecture problem — event-sourcing, a real API, the strangler pattern — not a rewrite.

Starting point: the monolith

One WordPress install, one theme (rhinox2) doing everything — rendering, business logic, data access, presentation. Fine for a blog; wrong for a live sports product where scores change ball-by-ball and entities carry deep statistical histories.

1. Event-sourced live scoring — CN-Engine

Scoring was the first thing to leave the theme. We built CN-Engine, which records every delivery as a structured ball_event and derives all state from that event stream — score, on-strike batter, over rotations, fall-of-wickets, scorecard, commentary. Editing a mid-innings ball re-walks the entire chain deterministically: runs re-credited, strike rotations re-synced, no drift. State is computed, never a mutable field.

2. A domain REST layer

We exposed a proper API (a WordPress mu-plugin “domain API”): match bundles, player/team/tournament profiles, standings, archives, rankings, search. This seam reframed WordPress as CMS + engine behind an API, not “the website.”

3. CN-Engine v2 — operator-grade scoring

Live scoring is a high-pressure operator tool. v2 reworked the IA (grouped lifecycle nav, unified cockpit), added single-tap extras, and added loud guardrails: impossible states (e.g. a dismissed batter still at the crease) are detected and the operator is told exactly how to repair them — never silently corrupted. We also hardened the engine: canonical team-id resolution across all scoring paths, historical scorecard backfills, and a fix where a repair tool reported success while the replay layer silently rejected the input.

cricnepal brand

4. Design system v2026 + a two-layer token architecture

A unified identity from the Nepal kit (navy #2B2F4B, red #EC3339, white). Under it, a two-layer token system: semantic tokens (--surface, --text-heading) mapped to a Tailwind --color-* layer. That separation enabled a genuine dark mode — every surface/border/text token flips. We ran a full audit to eliminate hardcoded light values (the insidious kind, like a card hover that only broke in dark mode) and fixed the token bridge so bg-surface-* utilities flip correctly.

5. Figma as source of truth

Tokens, chips, and components live in Figma as the canonical system — design and code share one vocabulary, so a format pill in Figma is the same in production down to the accent border.

6. Account layer on Auth0

A fan account layer (follow topics, saved stories, quizzes, reading history) on Auth0, with its cn_session cleanly separated from WordPress’s editor login — two independent session layers, never conflated.

7. SEO + Schema.org, code-owned

SEO was first-class and server-generated: titles, meta, canonicals, OG, and rich Schema.org graphs (SportsEvent / Person+Athlete / SportsTeam). Sitemaps, robots, and feeds proxy through the same domain so nothing drops from the index during the migration.

8. The decouple — making “headless” actually true

The unglamorous core of the project: we moved everything the frontend depends on out of the theme and into the domain API — auth/account, quiz, follow-topics, nav menus, entity cards, photo galleries, the tournament header, team/player structured data. The theme becomes optional; the API becomes the contract.

9. SSR headless frontend

The experience layer is now a server-rendered Next.js app (App Router, ISR), hydrated into rich islands. Server renders real data, not skeletons — correct first paint for users and crawlers. Players, teams, tournaments, match centre, archives, search, rankings, homepage — all structured React fed by the API.

10. Structured data for entities

Player/team pages — profiles, career stats, rankings, debut records, squad memberships, recent matches, galleries, tagged news — converted from theme-injected HTML into typed endpoints + structured components, so the same data renders identically on the site, in islands, and in any future client.

11. Zero-downtime cutover (strangler pattern)

We ran the headless app alongside WordPress and migrated one route family at a time at the edge (nginx): /players as a canary → verify → teams → tournaments → matches → homepage. WordPress serves everything not yet migrated. Rollback is a single config change.

The production release was 148 commits promoted cleanly from staging, deployed with the live scorer sidecar rebuilt and Auth0 login verified healthy before the first route flipped. A real-world gotcha we solved: the origin sits behind Cloudflare with a Cloudflare Origin CA certificate, which Node’s fetch couldn’t verify — so server-side rendering 404’d until we added the CF Origin CA root to the trust store (no TLS weakening). Fans never saw a maintenance page.

Stack

WordPress (CMS + editorial) · mu-plugin domain REST API · CN-Engine event store · Next.js 16 SSR/ISR · Auth0 · Tailwind CSS + two-layer design tokens · Figma design system · RunCloud + Node supervisor sidecars · Cloudflare + nginx edge routing.

Takeaway

We turned a live WordPress monolith into a headless platform without a single minute of downtime — by treating the migration as an architecture problem rather than a rewrite. The same API now powers the website and can power mobile apps, partner widgets, and data products. WordPress does what it’s great at; the experience layer evolves on its own.

cricnepal.com got a complete rebuild — Here’s what changed for you

If the site feels faster, smoother, and more alive lately, that’s not your imagination. We rebuilt CricNepal from the ground up — and did it without ever taking the site offline.

Same site. Brand-new engine. Zero downtime.

Live cricket that’s actually live

Every ball is now recorded as it happens through our own match engine, CN-Engine. Scores, scorecards, fall-of-wickets, and commentary all update from a single live feed — so the number you see is the number that just happened.

A look built for Nepali cricket

We gave CricNepal a proper identity, drawn straight from the national kit — navy, red, and white. New player and team pages, cleaner cards, a sharper match centre, and a consistent feel across the whole site.

Dark mode 🌙

The whole site now has a true dark mode — easy on the eyes for those late-night World Cup League games, with the same Nepal-navy identity carried through.

Follow your favourites

Create an account and follow players, teams, and tournaments. Save stories, play quizzes, and pick up where you left off — on any device.

Faster pages, better on Google

Pages now render instantly and are built to be found — so the match report, the player profile, and the live scorecard all show up when you search for them.

Same site, brand-new engine

All of this happened with zero downtime. We migrated the site piece by piece behind the scenes — players, teams, tournaments, the match centre, the homepage — while the live site kept running for everyone.

This is just the start. The new foundation means we can move faster on everything next: more stats, more live features, and one day, a mobile app powered by the very same engine.

Thanks for following along — and for being part of Nepali cricket. 🇳🇵🏏