Automating MeroShare IPO Applications: A Telegram Bot on Cloudflare Workers

Automating MeroShare IPO Applications: A Telegram Bot on Cloudflare Workers

Every time a new IPO opens in Nepal, the same ritual plays out: log into MeroShare, hope the site isn’t crawling under load, fill in the form, type the transaction PIN, hit submit, and pray the request doesn’t time out at 3:59 PM. Multiply that by family members’ accounts and it’s a genuine chore.

So I automated it. The end state: a Cloudflare Worker that checks for new ordinary-share IPOs twice a day, applies automatically with my registered accounts, and reports everything to me through a Telegram bot. Total monthly cost: zero. Total infrastructure to maintain: none.

This post is the full story of how it works and everything that went wrong on the way — because the failures were the interesting part.

The Architecture

Telegram app ──webhook──> Cloudflare Worker ──REST──> MeroShare (CDSC) API
                             │                            (webbackend.cdsc.com.np)
                             ├──> KV (encrypted accounts, applied-markers, settings)
                             └──> Cron triggers (Sun–Thu, 10 AM & 2 PM NST)

Three moving pieces:

  1. The Worker (src/index.ts) — receives Telegram webhook POSTs, routes commands, runs on cron twice a day.
  2. The MeroShare client (src/meroshare/client.ts) — reverse-engineered REST client for CDSC’s unofficial API.
  3. The KV store — holds accounts with passwords and PINs AES-256-GCM encrypted via Web Crypto, plus “already applied” markers so the cron never double-applies.

Why Cloudflare Workers and not a VPS or a Raspberry Pi? No server to keep alive, generous free tier (100k requests/day), and cron triggers built in. For something that “should just work for months without me touching it,” serverless is the right shape.

Step 1: The MeroShare API (or: how to be your own API docs)

MeroShare has no public API, but the web app at meroshare.cdsc.com.np is just an Angular SPA talking to webbackend.cdsc.com.np/api/meroShare/*. Open DevTools once and you can map the important endpoints:

Purpose Endpoint
Login (returns JWT in the Authorization header) POST /auth/ with {clientId, username, password}
Profile (BOID, demat, customerId) GET /ownDetail/
Linked bank accounts GET /bank/
List applicable issues POST /companyShare/applicableIssue/
Apply for a share POST /applicantForm/share/apply
Application reports POST /applicantForm/report/

The DP (clientId) is the numeric ID of your depository participant — findable via GET /capital/, an unauthenticated endpoint. There are 133 of them, IDs ranging 128–2191, which becomes relevant later.

Step 2: The Telegram bot layer

The Worker exposes exactly one webhook route. Telegram POSTs updates to it; the Worker verifies a X-Telegram-Bot-Api-Secret-Token header (set when registering the webhook), then dispatches commands:

/addaccount main,190,myuser,mypass,CRN1234,9999,10
/check          → list open IPOs
/applyall 796   → apply with every registered account
/status         → allotment results
/autoapply on   → let the cron do it

Credentials are encrypted before hitting KV — the plaintext never touches storage, and the encryption key lives only in a Worker secret.

Step 3: The deployment gauntlet

This is where it got fun. Four separate walls:

Wall 1: Chrome wouldn’t let me in

I wanted to automate the BotFather setup through the browser. Chrome v136+ blocks remote debugging on the default profile entirely, and macOS Accessibility (AppleScript) couldn’t get Chromium to expose its AX tree — Chromium only builds it when an assistive client asks, and nothing I tried flipped the switch.

Workaround: an embedded Chromium panel with its own persistent profile. One QR scan from my phone, and the session stuck — then the bot creation was pure DOM automation: /newbot → name → username → regex the token out of BotFather’s reply. Chat ID came from sending the bot a message and reading getUpdates.

Wall 2: Telegram couldn’t resolve my workers.dev subdomain

Registering the webhook kept failing with Failed to resolve host: Temporary failure in name resolution — while dig showed the record live globally. Control test: setWebhook to example.com → fine. Another random workers.dev subdomain → fine. Mine → DNS failure.

Telegram’s resolver had cached a negative DNS answer for my subdomain (created before first deploy). A retry loop eventually won: 6 attempts, ~2 minutes, then "Webhook was set". If this happens to you: don’t debug your DNS, just retry with backoff.

Wall 3: CDSC returns 500 to Cloudflare IPs (sometimes)

The login endpoint worked from my laptop but returned 500 Operation Failed from the Worker. Not consistent — the second attempt succeeded. So CDSC’s F5 load balancer intermittently throttles datacenter IPs. The fix is boring and effective: retry once after a short delay on 5xx, and keep request rates low (800ms between accounts).

Wall 4: The silent API migration (the sneaky one)

/check failed with Failed to fetch applicable issues: Internal Server Error — but only from the Worker. Testing from my laptop with a fresh token… also 500. So: not an IP issue, an API contract change.

I pulled the current Angular bundle (main.*.bundle.js) and grepped it. The app now sends:

{
  "filterFieldParams": [],
  "page": 1,
  "size": 20,
  "searchRoleViewConstants": "VIEW_APPLICABLE_SHARE",
  "filterDateParams": []
}

The old body I was sending ({"filterCompanyStocks":[],"filterStatus":["OPEN"]}) — the format in every open-source MeroShare script on GitHub — now returns a hard 500. No deprecation notice, of course. Same story for the reports endpoint (VIEW_APPLICANT_FORM_COMPLETE).

Two side quests while diagnosing this:

  • F5 bot-defense cookies: replaying requests with the login session’s cookies triggered Request Rejected WAF pages (support IDs and all). Fresh session, no stale cookies: clean pass. The lesson: when testing an API with anti-bot layers, change one variable at a time, or you’ll misread rate-limiting as header requirements.
  • A lurking payload bug: MeroShare’s ownDetail returns boid: "02098152" (8 digits!) while the real 16-digit demat is 1301090002098152. The old code used boid for both payload fields — guaranteed failure on a real apply. Now demat is stored separately, and the apply payload sends boid and demat correctly.

Step 4: Verify everything end-to-end

After each fix: deploy, send the command through Telegram’s real webhook, and read the bot’s reply in the chat:

  • /start → command menu ✅
  • /addaccount … → credential verification against MeroShare, encrypted storage ✅
  • /check → open issues list from the new API ✅ (the fund NICEOF was open, close date Sep 29)
  • /accounts → registered account with masked details ✅

The cron now runs Sun–Thu at 10:00 AM and 2:00 PM NST, filters for ordinary-share IPOs, checks KV for who hasn’t applied, and either auto-applies or pings me with a one-tap /applyall command.

What I’d do differently

  1. Watch the SPA bundle, not the API. A reverse-engineered API has no stability contract; the frontend is the documentation. A tiny scheduled check that diffs the bundle hash would have caught the migration earlier.
  2. Retry with jitter from day one. Both CDSC 5xx flakiness and Telegram’s DNS caching resolved on retry. Every external call in the bot now deserves a retry wrapper.
  3. Test payloads against the live API before building around them. I burned an hour on the old request shape because it looked plausible and the failure (500) looked like infrastructure.

The result

A fully serverless IPO bot: Telegram-controlled, zero-cost, auto-applying across accounts, with allotment tracking via /status. The interesting bugs — a stale negative DNS cache at Telegram, an undocumented API migration at CDSC, and an F5 WAF with a taste for malformed cookie jars — were all invisible until something end-to-end failed. Which is the recurring lesson: test the whole path, not just the pieces.

Want my posts to show up more often on Google?

One click and Google will surface this site in your Top Stories.

Add as preferred source
Niraj Basnet
Written by

Niraj Basnet

Computer Science student at the University of South Florida, exploring web development, mobile app development, and AI. Aspiring full-stack developer.