EVS Meter Tools

io.github.nicholasohjjPublished by GitHub account nicholasohjjv2.0.1Updated Nov 5, 2026

Check NUS and SUTD hostel EVS electricity meters and get top-up links. Unofficial.

VerifiedStreamable HTTPWeb executableLocation & LifestyleFinance

Overview

AI-generated overview

Read-only MCP server for NUS and SUTD hostel EVS prepaid electricity meters: check balance, usage, top-ups, and get a card top-up link.

What you can do
  • Check a meter's current prepaid credit balance in SGD for NUS and SUTD meters
  • Review up to 7 days of usage with spending analysis, month-to-date spend and estimated remaining days (NUS only)
  • List up to 10 recent top-ups, with guidance on confirming whether a payment went through
  • Create a link to the secure top-up page so the user can pay by card themselves
When to use it
Useful for students in NUS or SUTD hostels who want an assistant to watch their aircon credit, explain recent spending, and hand over a top-up link when the balance runs low. It is an unofficial service, so treat it as a convenience rather than an official billing source.
Requirements
Remote streamable HTTP endpoint; no account, API key or header is needed. The user must supply an 8-digit meter ID and say whether it is an NUS or SUTD meter. Network access to the hosted service is required, and calls are rate limited per IP address.
Before you install
Unofficial and not affiliated with NUS, SUTD or EVS. The server cannot take payments and never receives card details; payment happens on the linked page, where the user enters card data themselves. Meter lookups are sent to the hosted service and on to the user's AI provider, though room addresses are omitted from responses. Top-up links are created without charge but the user pays real money on the page.

Installation

In SourceWeft

  1. Open EVS Meter Tools in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.

Use with an agent

Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.

Read https://sourceweft.com/mcp/io.github.nicholasohjj%2Fnusaircon/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.

Read the install guide the agent follows

Other MCP clients

Add this to your client's mcpServers config.

{
  "mcpServers": {
    "evs-meter-tools": {
      "type": "http",
      "url": "https://nusaircon-production.up.railway.app/mcp"
    }
  }
}

README

EVS Meter Tools

A Telegram bot and web app for supported EVS electricity meters. Supported NUS and SUTD meters can be topped up by credit card, and supported lookup flows show balance, usage, and top-up history where available.

Supported systems

Hostel groupEVS system
NUS hostels, including PGPR, Houses @ PGP, Residential Colleges, NUS College, UTown Residences, RVRC, Valour Housecp2nus.evs.com.sg
SUTDSUTDMain / EVSSUTDWebPOS

The legacy NUS CP2 fallback (cp2.evs.com.sg) is hidden from the web app and bot. Its backend routes and services are kept but are not linked from any UI; see CP2 — legacy NUS fallback (hidden).

Features

  • Check meter balance, 7-day usage history, and recent top-ups from within Telegram
  • Top up supported NUS electricity meters via credit card (SGD $6-$50)
  • Top up supported SUTD electricity meters via credit card (SGD $10-$50)
  • RSA-encrypted card entry — card details never leave the browser in plaintext
  • Works as a Telegram Mini App and as a standalone website
  • Cross-system guard: cp2nus bootstrap rejects meters that belong to the legacy cp2 system before initiating payment
  • SUTD balance, top-up history, and top-up support
  • Analytics tracking and error capture throughout the flow
  • Opt-in low-balance alerts on Telegram, with a button to start a top-up
  • Read-only MCP server, so people can connect their own Claude and set their own instructions

Architecture

Telegram Bot (telegraf)          Website (React, /app/)    │                                │    ├── /topup                       └── HomePage    ├── /balance                          └── system selection    ├── /usage                                + meter ID + amount    └── /topups          │ webhook in production/Railway,       │          │ polling in development               │          ▼                                       ▼    Express (server.js)  ←────────────────────────────          │          ├── POST /telegram/webhook/* — Telegram webhook receivers          ├── GET  /webapp              — legacy CP2 (hidden from UI): fetches meter summary, redirects to React          ├── GET  /webapp/bootstrap    — runs full payment init, returns token          ├── GET  /webapp/session      — returns session data as JSON for React          ├── GET  /webapp/pay          — redirects to React card entry page          ├── POST /webapp/enets_pay    — proxies encrypted card data to eNETS          ├── POST /webapp/notify       — fallback Telegram payment notification          └── GET  /webapp/result       — redirects to React result page
    React Frontend (/app/)          ├── /                 — HomePage (system + meter ID + amount)          ├── /balance          — HomePage opened in balance-check mode          ├── /cp2nus           — HomePage preselected for primary NUS CP2NUS flow          ├── /cp2nus/loading   — cp2nus variant          ├── /cp2nus/pay       — cp2nus variant          ├── /cp2nus/result    — cp2nus variant          ├── /sutd/loading     — SUTD variant          ├── /sutd/pay         — SUTD variant          └── /sutd/result      — SUTD variant

Payment flows

CP2 — legacy NUS fallback (hidden)

Hidden. This flow is no longer offered in the web app or bot. The /app/cp2, /app/loading, /app/pay, and /app/result React routes are commented out in frontend/src/App.jsx, /app/cp2 is excluded from the sitemap and robots.txt, and the bot no longer shows a hostel picker. The backend routes and services below are kept for reference and possible reinstatement.

Scrapes the legacy EVS WebPOS portal to create a transaction, then proxies through eNETS. It was used only for NUS meters that had not migrated to CP2NUS.

  1. /webapp — fetches meter address and balance from ORE, redirects to React loading page with address/balance in query params
  2. Bootstrap (/webapp/bootstrap) — runs runPurchaseFlow and getMeterSummary in parallel:
    • GET /EVSWebPOS/ → login → POST /loginServlet (meter validation)
    • POST /selectOfferServlet (amount selection)
    • GET /paymentServlet → extract merchant_txn_ref
    • POST creditpayment.jsp → extract eNETS message
    • POST /enets2/PaymentListener.do → extract RSA public key (n, e), netsMid, netsTxnRef
    • Creates a payment session (10-min TTL), redirects to React card page
  3. Card page (/app/pay) — React component; fetches session via /webapp/session; encrypts cardNo + cvv with eNETS RSA scripts client-side
  4. Payment proxy (/webapp/enets_pay):
    • POST https://www.enets.sg/GW2/uCredit/pay
    • Preferred path: extracts EVS callback form → POST /EVSWebPOS/transSumServlet → parseEvsTransactionSummary
    • Fallback: parseEnetsResult scrapes the eNETS receipt HTML directly
    • Writes outcome (status, merchantTxnRef, reason) back to the server-side session
  5. Result page (/app/result) — React component; reads outcome from server session via /webapp/session

CP2NUS — primary NUS hostel flow

Uses the EVS JSON API and the eNETS Payment Page (enetspp) host directly. This is the primary NUS hostel flow for migrated meters.

  1. /webapp — same as cp2; fetches meter info, redirects to /app/cp2nus/loading
  2. Bootstrap (/cp2nus/webapp/bootstrap) — runBootstrap runs sequentially:
    • Meter system check — isCp2Meter() guard: rejects cp2 meters with a WRONG_SYSTEM error; on network failure the check is skipped and flow proceeds
    • init_pay — POST /enets/init_pay → { txn_identifier, req, sign }
    • meter_info — getMeterSummary → buildPayDisplayAddress
    • enetspp_pay — buildEnetsPayUrl → GET enetspp/pay?p=… → extract txnReq, keyId, hmac
    • TxnReqListener — POST /GW2/TxnReqListener → RSA key, netsTxnRef, netsMid, paymtNetsMid, txnRand, keyId, hmac
  3. Card page (/app/cp2nus/pay) — same RSA encryption; paymtNetsMid (acquiring MID) used in panSubmitForm, not top-level netsMid
  4. Payment proxy (/cp2nus/webapp/enets_pay):
    • GET /GW2/pluginpages/env.jsp → seed JSESSIONID
    • POST /GW2/credit/init;jsessionid=…
    • POST /GW2/credit/panSubmitForm
    • Preferred path: netsTxnStatus in response → preParsed result (no b2s call)
    • Fallback: POST /enets/b2s → 303 redirect → parsePayResult base64-decodes params
  5. Result page (/app/cp2nus/result) — reads outcome from server session

SUTD

Uses the SUTD EVS WebPOS pages and the SUTD credit payment bridge.

  1. /sutd/webapp — fetches SUTD meter credit, redirects to /app/sutd/loading
  2. Bootstrap (/sutd/webapp/bootstrap) — runs the SUTD WebPOS flow:
    • POST /EVSSUTDWebPOS/loginServlet with the meter ID
    • POST /EVSSUTDWebPOS/selectOfferServlet with the selected amount
    • GET /EVSSUTDWebPOS/paymentServlet → extract merchant_txn_ref
    • POST http://120.50.44.233/payment_sutd_credit/creditpayment.jsp
    • POST /enets2/PaymentListener.do → extract RSA public key, netsMid, netsTxnRef
  3. Card page (/app/sutd/pay) — same RSA-encrypted card entry as NUS flows
  4. Payment proxy (/sutd/webapp/enets_pay) — posts encrypted card data to eNETS, then posts the callback to /EVSSUTDWebPOS/transSumServlet
  5. Result page (/app/sutd/result) — reads outcome from server session

Setup

Prerequisites

  • Node.js 20.19+ (22.12+ on Node 22)
  • A Telegram bot token (from @BotFather)
  • A publicly accessible HTTPS server (required for Telegram WebApp buttons)

Installation

bash
# Install backend dependenciesnpm install
# Install and build the frontendnpm run build:frontend

Environment variables

Create a .env file:

env
TELEGRAM_BOT_TOKEN=your_bot_token_here      # legacy single-bot tokenNUS_TELEGRAM_BOT_TOKEN=your_nus_bot_token   # optional, for concurrent nusairconbotSUTD_TELEGRAM_BOT_TOKEN=your_sutd_bot_token # optional, for concurrent sutdairconbotPAYMENT_SESSION_SECRET=replace_with_openssl_rand_hex_32 # stable secret for signed payment/result tokensSERVER_URL=https://your-public-server.example.comOWNER_CHAT_ID=your_telegram_chat_id   # receives feedback notificationsMAINTENANCE_MODE=false                # set to "true" to put the app and bot in global maintenance modeMAINTENANCE_MESSAGE=                  # optional custom global maintenance messageOWNER_STATUS_TIMEOUT_MS=4000          # optional timeout for owner /status EVS/eNETS checksTOPUP_DISABLED=false                  # set to "true" to show maintenance messageDB_DIR=.                              # local SQLite dir; use /data on Railway with a mounted volumeTELEGRAM_BOT_MODE=                    # production/Railway defaults to webhook; dev defaults to pollingTELEGRAM_BOT_AUDIENCE=nus             # legacy single-bot mode: "nus" or "sutd"LOW_BALANCE_CRON_SECRET=              # optional, enables the low-balance check endpointMCP_ALLOWED_ORIGINS=                  # optional, comma-separated extra browser origins allowed to call /mcpGOOGLE_SITE_VERIFICATION_FILE=        # optional Search Console HTML file name, e.g. googleabc123.htmlGOOGLE_SITE_VERIFICATION_CONTENT=     # optional exact file body if Google provides non-default content

SERVER_URL must be HTTPS for the Telegram WebApp payment button to work. If it is HTTP, the bot falls back to a plain browser link instead. On Railway, set it to https://${{RAILWAY_PUBLIC_DOMAIN}} or leave it unset and the app will derive it from RAILWAY_PUBLIC_DOMAIN.

PAYMENT_SESSION_SECRET should be a stable random value, for example from openssl rand -hex 32. If it is omitted, the app falls back to the configured bot token, which still survives Railway sleeps as long as the bot token does not change.

For Google Search Console URL-prefix verification, use the HTML file method and set GOOGLE_SITE_VERIFICATION_FILE to the downloaded file name. The default response body is google-site-verification: <file name>. If Google's downloaded file contains different text, set GOOGLE_SITE_VERIFICATION_CONTENT to the exact file body.

In production, or when RAILWAY_PUBLIC_DOMAIN is present, each configured bot uses a Telegram webhook by default so Railway Serverless can sleep and wake from inbound Telegram requests. Set TELEGRAM_BOT_MODE=polling only for an always-on deployment. Optional webhook variables are TELEGRAM_WEBHOOK_PATH, TELEGRAM_WEBHOOK_SECRET, and TELEGRAM_DROP_PENDING_UPDATES for single-bot mode. For concurrent bots, use NUS_TELEGRAM_WEBHOOK_PATH, NUS_TELEGRAM_WEBHOOK_SECRET, SUTD_TELEGRAM_WEBHOOK_PATH, and SUTD_TELEGRAM_WEBHOOK_SECRET only if you need custom paths; otherwise unique paths are generated automatically.

For concurrent Telegram bots in one deployment, set both NUS_TELEGRAM_BOT_TOKEN and SUTD_TELEGRAM_BOT_TOKEN. The same service starts both nusairconbot and sutdairconbot, with separate command menus, webhook paths, in-memory sessions, and payment notifications. Single-bot deployments can still use TELEGRAM_BOT_TOKEN plus TELEGRAM_BOT_AUDIENCE.

Running

bash
# Development (two terminals)npm run dev            # Express backend on :3000npm run dev:frontend   # Vite dev server on :5173
# Productionnpm run build:frontendnpm start

The frontend is served at /app/ by Express in production. In development, Vite proxies /webapp, /cp2nus, and /sutd to the backend.

For local development, keep DB_DIR=. or omit it. On Railway, set DB_DIR=/data only after attaching a volume mounted at /data.

Railway Serverless

The repo includes railway.json with the production build, start command, and /health check. Railway Serverless itself is enabled in the dashboard:

  1. Add a public domain for the service.
  2. Set either TELEGRAM_BOT_TOKEN for one bot, or NUS_TELEGRAM_BOT_TOKEN and SUTD_TELEGRAM_BOT_TOKEN for concurrent bots. Also set PAYMENT_SESSION_SECRET, SERVER_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}, and any optional variables such as OWNER_CHAT_ID.
  3. Attach a volume mounted at /data and set DB_DIR=/data so saved users survive restarts.
  4. Go to service settings > Deploy > Serverless and enable Serverless.
  5. Deploy. Startup will register the Telegram webhook automatically.

Cold starts can make the first Telegram or web request slower, and Railway may return a first-request 502 while waking the service. Bot sessions and owner reply threads are in-memory and reset by restarts or sleeps; payment/result tokens are sealed with PAYMENT_SESSION_SECRET and can still be read after a restart until their TTL expires. Saved meter IDs remain in SQLite.

Testing

bash
# Backend testsnpm test
# Frontend testscd frontend && npm test
# Lint backend and frontendnpm run lint

Connect your own Claude (MCP)

The service exposes a public, read-only MCP server at POST /mcp, so people can connect their own AI assistant and decide how it behaves. No account or API key is needed.

ToolWhat it does
get_meter_balanceCurrent balance (NUS and SUTD)
get_meter_usageLast 7 days of usage and analysis (NUS only)
get_recent_topupsUp to 10 recent top-ups (NUS and SUTD)
get_topup_linkLink to the top-up page, where the user pays by card themselves

The tools cannot make payments and never receive card details. Responses leave out the room address, because they are passed on to the user's AI provider.

To connect, add https://<your-domain>/mcp as a custom connector in Claude's settings. In Claude Code, run:

bash
claude mcp add --transport http evs-meter https://<your-domain>/mcp

Each person writes their own instructions in their assistant, for example in a Claude Project:

text
My NUS meter ID is 12345678. When I ask about aircon credit, check the balanceand work out how many days it will last from my recent usage. If it will lastunder 3 days, suggest a top-up amount and give me the top-up link.

The server is stateless, so it works with Railway Serverless. Tool calls are limited to 120 per 5 minutes per IP address, and can't be sent as a batch. Hosted clients share IP addresses, so that limit is higher than the website's lookup limit.

Tool errors are JSON, so an agent can decide what to do next:

json
{ "code": "METER_NOT_FOUND", "message": "EVS has no NUS meter with this ID. ...", "retryable": false }
CodeRetryableMeaning
METER_NOT_FOUNDNoEVS doesn't recognise the meter on that system
INVALID_AMOUNTNoThe amount is outside the system's top-up limits
TOPUPS_PAUSEDNoTop-ups are turned off for now
EVS_UNAVAILABLEYesEVS could not be reached or returned no details
RATE_LIMITEDYesToo many calls from this IP address; includes retryAfterSeconds

get_topup_link checks the meter first. If EVS can't be reached, it still returns a link with meterVerified: false.

Every tool declares an output schema and returns structuredContent, with the same JSON as text for clients that don't read it.

To have an agent check your balance daily and send you a payment link when it runs low, see scheduled balance check. To run your own copy, or build your own automation on top of it, see the self-hosting guide.

Bot commands

CommandDescription
/startShow the main menu
/topupStart an electricity top-up
/balanceCheck meter balance
/usageShow last 7 days of usage
/topupsShow recent top-ups
/alertsShow or change low-balance alerts
/feedbackShare feedback or report an issue
/cancelCancel the current flow
/helpShow help and supported systems
/statusOwner-only live service health
/statsOwner-only runtime snapshot

Low-balance alerts

People opt in with /alerts on, which covers the saved meters on that bot. Meters are saved when someone starts a top-up in the bot. Once a day, each opted-in meter is checked and its chat gets a message if the balance is below SGD 5. The message has a button that starts the bot's normal top-up flow, so the person still chooses the amount and pays by card.

Each meter gets one alert per low-balance episode, and can alert again once a check sees the balance back at SGD 5 or more. A failed lookup is skipped and never counts as a zero balance. Alerts stop if someone blocks the bot, and /forget removes them along with the meter.

To turn on the daily check:

  1. Generate a secret with openssl rand -hex 32.
  2. Set it as LOW_BALANCE_CRON_SECRET on Railway.
  3. Add the same value as a repository secret with the same name on GitHub.

.github/workflows/low-balance-check.yml then calls POST /internal/low-balance-check at 09:00 Singapore time, which also wakes the service if Railway Serverless has put it to sleep. To test it, run the workflow manually from the GitHub Actions tab. GitHub can delay scheduled runs, and it disables them after 60 days without repository activity.

Bot session flow

Sessions are stored in-memory with a 15-minute TTL. All messages for a given chat are serialised through a per-chat lock to prevent race conditions. On serverless deployments, sessions reset when Railway stops and later restarts the service. The top-up flow stages are:

idle  → awaiting_meter_id          (8-digit ID; prefetches balance + 7-day usage;                                NUS always uses cp2nus, no hostel picker)  → awaiting_amount            ($6–$50 SGD)  → awaiting_payment           (WebApp Pay button; re-prompts on text)  → idle                       (reset after WebApp closes)
  → awaiting_meter_id_balance  (/balance with no saved meter)  → awaiting_meter_id_usage    (/usage with no saved meter)  → awaiting_meter_id_topups   (/topups with no saved meter)
  → awaiting_feedback_rating   (/feedback — star rating keyboard)  → awaiting_feedback_text     (free-text or ⏭ Skip)  → idle

/balance, /usage, and /topups use single-step stages that return to idle after one response.

Payment Session

Payment sessions (created by /webapp/bootstrap) are sealed encrypted tokens with a 10-minute TTL, separate from bot sessions. The token holds the meter ID, amount, address, balance, and eNETS gateway fields needed for payment. This avoids losing an active payment session when Railway Free wakes the service on a new process.

Once /webapp/enets_pay has a final outcome, the server sends the Telegram payment notification immediately and returns a sealed result token with a 24-hour TTL. Payment and result tokens carry an internal tokenKind; payment routes reject result tokens, and result routes reject payment tokens. The React result page reads outcome data from GET /webapp/session?token=; query params are never trusted for payment results. CP2NUS receipt PDFs are still held in a volatile in-memory cache, so the result page only shows the receipt link when that cache entry is available.

User store

Saved meter IDs and hostels are persisted in a SQLite database (evs_users.db) using better-sqlite3. The database is written to DB_DIR (Railway Volume at /data if present, otherwise the project root). Unlike bot sessions and payment sessions, the user store survives restarts.

Owner reply threading

When a user submits feedback, the bot forwards a notification to OWNER_CHAT_ID. The owner can reply directly to that Telegram notification and the bot forwards the reply back to the user. Reply threads are tracked via an in-memory pendingReplies map with a 7-day TTL.

Note: threading only follows the original notification message. If the owner replies to their own reply, that message is not automatically routed back to the user — only replies to the original forwarded notification are intercepted.

Project structure

├── server.js                        # Express entry point; serves React at /app/├── routes/│   ├── cp2.js                    # WebApp + API routes for legacy cp2 (hidden from UI)│   ├── cp2nus.js                 # WebApp + API routes for cp2nus│   ├── sutd.js                   # WebApp + API routes for SUTD│   ├── mcp.js                    # Read-only MCP server for AI assistants│   ├── alerts.js                 # Secret-protected trigger for the low-balance check│   └── website.js                # Standalone website lookup + feedback API├── services/│   ├── cp2Service.js             # Legacy cp2 purchase flow: EVS WebPOS scraping + eNETS proxy│   ├── cp2nusService.js          # Purchase flow: EVS JSON API + eNETS PP + NETS API│   ├── sutdService.js            # SUTD balance, history, and WebPOS purchase flow│   ├── ore.js                    # ORE API: meter summary, usage history, top-up history│   ├── meterLookup.js            # Balance, usage, and top-up lookups shared by the website and MCP│   ├── paymentSession.js         # Sealed payment/result tokens and receipt cache│   ├── paymentNotification.js    # Telegram payment result notification helper│   ├── paymentSubmitLock.js      # Short-lived duplicate payment submit guard│   ├── utils.js                  # HTML parsing, result normalisation, XSS escaping│   ├── validators.js             # Meter ID validation and strict amount parsing│   ├── config.js                 # Base URLs and shared HTTP headers│   └── analytics.js              # Event tracking and exception capture├── bot/│   ├── index.js                  # Telegraf handlers + webhook/polling runtime│   ├── handlers/                 # Command and text message handlers│   ├── services/                 # Bot session, user store, lookup helpers│   └── constants.js              # Stage names, keyboards, shared messages├── views/│   └── errorPage.js              # Shared HTML error page for Express error responses└── frontend/                     # React + Vite frontend    ├── src/    │   ├── App.jsx               # React Router routes    │   ├── pages/    │   │   ├── HomePage.jsx      # System selection + meter ID + amount entry    │   │   ├── LoadingPage.jsx   # Spinner; calls /webapp/bootstrap    │   │   ├── CardPaymentPage.jsx  # RSA card form; calls /webapp/enets_pay    │   │   ├── ResultPage.jsx    # Payment outcome    │   │   └── TermsPage.jsx     # Terms of Use    │   ├── components/           # Card, DetailRow, Logo, ErrorCard    │   └── lib/                  # rsa.js, cardBrand.js, validation.js    └── __tests__/                # Vitest + Testing Library tests

Notes

  • Bot sessions are in-memory and expire after 15 minutes. Payment tokens are sealed and restart-safe if PAYMENT_SESSION_SECRET is stable; pending payment tokens expire after 10 minutes, completed result tokens after 24 hours. The user store (saved meter IDs) is SQLite-backed and persists across restarts.
  • /webapp/enets_pay uses a process-local submit lock keyed by merchant transaction reference to reject duplicate in-flight payment submits with HTTP 409.
  • Card details are RSA-encrypted in the browser before being sent to the server. The server never sees plaintext card numbers or CVVs.
  • The cp2nus flow distinguishes between the top-level netsMid (UMID_xxx) and paymtNetsMid (acquiring MID from paymtSvcInfoList[0]). Using the wrong MID will cause the payment to fail silently.
  • ORE (ore.evs.com.sg) and the eNETS init endpoint (p-1.evs.com.sg) are currently called with an empty Authorization: Bearer header and succeed — no token is required. The EVS2 portal also runs a user-auth service (evs2u.evs.com.sg/login) that issues short-lived (~1h) JWTs, so EVS could start enforcing a real token on these endpoints in future. If lookups or top-ups begin returning 401/403, this is the likely cause and the flow would need to obtain and attach a JWT.
  • Accepted top-up amount input is strict: plain numbers plus common currency prefixes such as $10 or S$ 10.00; malformed mixed strings are rejected. NUS minimum top-up is $6.00 SGD, SUTD minimum top-up is $10.00 SGD, and all systems currently cap top-ups at $50.00 SGD.
  • The website entry point (/app/) and the Telegram Mini App use the same Express routes and React pages — no separate codepaths.
  • Global maintenance mode can be enabled at runtime with /maintenanceon (owner command) or at startup with MAINTENANCE_MODE=true. Users see the global maintenance message across the app and bot; /health and Telegram webhooks remain available so the service stays reachable and the owner can run /maintenanceoff. Set MAINTENANCE_MESSAGE to customize the copy. GLOBAL_MAINTENANCE=true is accepted as a legacy alias.
  • Top-ups can be disabled at runtime with /topupoff (owner command) or at startup with TOPUP_DISABLED=true. Bot users in an active top-up session when the flag is set will have their session reset and see the maintenance message. Website payment start, loading, card, bootstrap, and card-submit routes also return the maintenance response while top-ups are disabled; lookup, terms, result, and feedback routes remain available.

Licence

MIT. This is an unofficial tool, not affiliated with NUS, SUTD, EVS or eNETS.

Source: README.md at commit 0cbcdb9

Tools

4

get_meter_balance

Get meter balance

Get the current prepaid credit balance of a meter, in SGD.

Parameterssystemrequiredmeter_idrequired

get_meter_usage

Get meter usage

Get up to 7 days of usage for an NUS meter, with spending analysis, month-to-date spending, and how long the balance may last. Amounts are in SGD. Dates are as EVS reports them, with no time zone, and recent days may be missing. Not available for SUTD meters.

Parametersmeter_idrequired

get_recent_topups

Get recent top-ups

Get up to 10 recent top-ups for a meter. To confirm a payment, look for a top-up with the same amount dated after the createdAt of its link, and with no status showing it failed. Earlier top-ups can have the same amount, so always check the date. Dates have no time zone and appear to be Singapore time (UTC+8), while createdAt is ISO 8601. If nothing matches, the payment may not be recorded yet or may have failed: check again later, and have the user check their card statement before paying again.

Parameterssystemrequiredmeter_idrequired

Version history

1
  1. v2.0.1LatestNov 5, 2026

Score

100% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 daysRequired25 pts
  • Auth declaredRequired10 pts
  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts