Skip to content

Repository files navigation

paste-to-verify

npm CI license node

Verify Ethiopian CBE and Telebirr payment receipts from a pasted SMS — no bank API keys, no manual review.

When a customer pays, they get an SMS containing a receipt link. This library takes that raw SMS, extracts the link, fetches the authoritative receipt page, parses it into a normalized transaction, and runs deterministic checks.

import { parseReceipt, verifyTransaction } from 'paste-to-verify';

const transaction = await parseReceipt(pastedSms);
const result = verifyTransaction(transaction, {
  amount: 990,
  receiverName: 'Dawit',
  maxAgeMinutes: 30,
});

result.verified; // boolean
result.failures; // [{ field, expected, actual, reason }]

Why a receipt page and not the SMS?

The SMS is not trusted as the source of truth — it is easy to fabricate. It serves two purposes only: it supplies the receipt URL, and it provides a fallback for fields the receipt page doesn't expose. The receipt page is authoritative, so on any overlap the page value wins.

Providers

Provider Host Receipt page Fetch strategy
Telebirr transactioninfo.ethiotelecom.et Server-rendered HTML fetch + node-html-parser (edge-friendly)
CBE cbe.com.et JavaScript SPA (spinner-only HTML) Headless browser render, then scrape the DOM

The CBE receipt is a Nuxt SPA whose served HTML contains only a loading spinner, so it is rendered with Playwright (by default) and the loaded DOM is scraped. You can inject your own renderer for edge runtimes — see apps/docsProviders → CBE.

CBE therefore needs a headless browser. Telebirr alone does not.

Monorepo layout

packages/core/        # the published npm package: paste-to-verify
apps/docs/            # documentation site + landing page (Next.js + Fumadocs)
apps/examples/        # interactive demo UIs — paste an SMS (or use a built-in
  express/            #   sample), watch the parsed receipt + live verdict
  nextjs/             #   express :3001 · nextjs :3003
                      #   each also exposes POST /verify and POST /check

The layout follows a deliberate convention so it can grow without breaking changes:

  • packages/* holds publishable libraries (npm). New JS/TS packages (e.g. a framework adapter packages/nextjs) slot in beside core and are picked up by the pnpm workspace automatically.
  • apps/* holds deployables and consumers — the docs site and runnable examples. Nothing in apps/ is published.
  • Other language ports (e.g. a PHP/Composer package) get their own top-level directory (php/), since each ecosystem's tooling expects its own manifest at its package root. The JS workspace config (pnpm-workspace.yaml, turbo.json) never needs to know about them, and packages/core keeps its npm identity untouched.

Tooling: pnpm workspaces + Turborepo, TypeScript (strict), tsup (ESM + CJS + d.ts), Vitest, ESLint + Prettier.

Contributing — human or AI — starts with AGENTS.md: the commands, the correctness invariants that must not be broken, and the testing and release conventions.

Development

pnpm install          # also installs Playwright's package
pnpm build            # turbo: build core, docs, and examples
pnpm test             # vitest with coverage (>90% on packages/core/src)
pnpm lint
pnpm typecheck

Run a piece in isolation:

pnpm --filter paste-to-verify test          # core unit tests
pnpm --filter @paste-to-verify/docs dev     # docs at http://localhost:3000
pnpm --filter @paste-to-verify/example-express dev

To actually parse CBE receipts (locally or in the examples), install the browser:

pnpm --filter paste-to-verify exec playwright install chromium

Fixtures & live checks

Tests mock fetch/the renderer with saved fixtures in packages/core/tests/fixtures, so CI never hits the network. The committed fixtures and docs use synthetic, anonymized data — real receipts contain personal information (names, account numbers, a unique link) and are never committed.

To check against a real receipt, supply your own URL/SMS via a gitignored .env (see .env.example) and run the live scripts:

cp .env.example .env   # then fill in TELEBIRR_SMS / CBE_SMS (or *_RECEIPT_URL)

node --env-file=.env packages/core/scripts/smoke-live.mjs     # parse a live receipt end-to-end
node --env-file=.env packages/core/scripts/save-fixtures.mjs  # re-save fixtures from a live page

Regenerating fixtures overwrites them with the live page's (personal) data — don't commit that.

Public API

  • parseReceipt(sms, options?) — main entry point (async).
  • parseReceiptUrl(url, options?) / parseReceiptId(provider, id, options?) — verify from just the receipt link or a transaction id (no SMS needed).
  • verifyTransaction(tx, options?) — pure, synchronous, never throws.
  • extractUrl(sms), detectProvider(url), normalizeUrl(url), urlMatchesHost(url, host) — utilities.
  • createPlaywrightRenderer() — the default renderer (inject your own for edge).
  • Typed errors (UrlNotFoundError, ProviderNotRecognizedError, ReceiptFetchError, ReceiptParseError, BrowserUnavailableError), each with a stable code. parseReceipt never throws an untyped error.

Full reference and guides live in the docs site (apps/docs).

Technical decisions

Decision Choice
HTML parser node-html-parser (Telebirr)
SPA rendering Playwright (CBE), injectable + optional peer dependency
HTTP client native fetch
Test runner Vitest
Build tsup (ESM + CJS + .d.ts)
Package manager / tasks pnpm + Turborepo
Node minimum 18

License

MIT © Biruk Worku — see LICENSE.

About

Verify Ethiopian CBE & Telebirr payment receipts from a pasted SMS - no bank API keys, no manual review.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages