EVS Meter Tools

io.github.nicholasohjj由 GitHub 帳號 nicholasohjj 發布v2.0.1更新於 Nov 5, 2026

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

已驗證Streamable HTTP可網頁執行位置與生活金融

概覽

AI 產生的概覽

針對 NUS 與 SUTD 宿舍 EVS 預付電表的唯讀 MCP 伺服器,可查餘額、用量、儲值紀錄並產生刷卡儲值連結。

可以做什麼
  • 查詢電表目前預付餘額(新幣),支援 NUS 與 SUTD 電表
  • 查看最多 7 天的用量,含消費分析、本月累計消費與餘額可用天數估算(僅 NUS)
  • 列出最近最多 10 筆儲值,並說明如何判斷某筆付款是否成功
  • 產生安全儲值頁面連結,由使用者自行開啟並以信用卡付款
適用情境
適合住在 NUS 或 SUTD 宿舍、希望讓助理留意冷氣電費、說明近期消費,並在餘額偏低時提供儲值連結的使用者。此服務為非官方專案,宜視為便利工具而非官方帳務依據。
執行需求
遠端 streamable HTTP 端點,不需帳號、API 金鑰或標頭。使用者需提供 8 位電表編號,並說明是 NUS 或 SUTD 電表。需要能連線到該託管服務,呼叫會依 IP 位址限流。
安裝前請注意
非官方專案,與 NUS、SUTD 及 EVS 無隸屬關係。伺服器無法付款,也不會收到信用卡資料;付款在連結頁面由使用者自行輸入卡號完成。電表查詢會送到該託管服務並轉給使用者的 AI 服務商,但回應中省略房間地址。產生儲值連結本身不收費,但使用者在頁面上支付的是真實款項。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 EVS Meter Tools,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。

交給代理程式安裝

把這段提示詞貼上到 Claude Code、Codex、Cursor 等代理程式,或 SourceWeft 對話中。代理程式會讀取該伺服器的安裝指南,先向你展示發布者、端點和登入方式,經你同意後再把它新增到你的客戶端。金鑰不會寫進任何命令。

閱讀 https://sourceweft.com/mcp/io.github.nicholasohjj%2Fnusaircon/install.md,並按其中說明把這個 MCP 伺服器接入我的客戶端。修改任何設定之前,先告訴我它的發布者、端點和登入方式,等我確認。不要把 API 金鑰寫進命令或我會提交的檔案;問我金鑰應從哪裡取得。

檢視代理程式所依據的安裝指南

其他 MCP 客戶端

把它新增到你客戶端的 mcpServers 設定中。

{
  "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.

來源:README.md,提交 0cbcdb9

工具

4

get_meter_balance

Get meter balance

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

輸入參數system必填meter_id必填

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.

輸入參數meter_id必填

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.

輸入參數system必填meter_id必填

版本歷史

1
  1. v2.0.1最新Nov 5, 2026

評分

已滿足適用檢查項的 100%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通必需25 分
  • 已宣告認證方式必需10 分
  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分