npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@localpay/verification-engine

v0.1.3

Published

Bank receipt verification engine for Ethiopian and East African banks

Readme

@localpay/verification-engine

Bank receipt verification engine for Ethiopian banks

npm version npm downloads License: MIT DOCS Node.js CI

Installation · Quick Start · Supported Banks · Verification Methods · Proxy Support · Adding a Bank · NestJS


Features

  • 🏦 4 Ethiopian banks — CBE, Telebirr, Bank of Abyssinia, E-Birr
  • 🔗 5 verification methods — link, SMS, transaction reference, OCR, screenshot
  • 🌍 Proxy support — per-country routing for banks that block foreign IPs
  • Amount matching — configurable tolerance, strips currency symbols automatically
  • 🔌 Zero framework lock-in — plain TypeScript, no NestJS, no Prisma
  • 🧩 Extensible — add any bank by implementing one interface
  • 📦 Tree-shakeablesideEffects: false

Installation

npm install @localpay/verification-engine

Optional: Install puppeteer if you verify Telebirr or Bank of Abyssinia receipts. These banks serve JavaScript-rendered pages that require a headless browser.

npm install puppeteer

Quick Start

import { VerificationEngine } from "@localpay/verification-engine";

const engine = new VerificationEngine();

const result = await engine.verify({
  bank: "CBE",
  amount: 500,
  verMethod: "LINK",
  rawProof: "https://apps.cbe.com.et:100/?id=FT26093JCD3218872366",
});

if (result.status === "SUCCESS") {
  console.log(result.receipt.receipt.transactionNumber); // "FT26093JCD32"
  console.log(result.receipt.receipt.amount);            // "500"
  console.log(result.receipt.receipt.receiverAccount);   // account number
  console.log(result.receipt.receipt.receiverName);      // account holder name
}

if (result.status === "FAIL") {
  console.error(result.reason); // human-readable failure reason
}

Supported Banks

| Bank | parserKey | Methods | |------|------------|---------| | Commercial Bank of Ethiopia | CBE | LINK · SMS · TRANSACTION_REF · OCR | | Telebirr | TELEBIRR | LINK · SMS · TRANSACTION_REF · OCR | | Bank of Abyssinia | ABYSSINIA | LINK · SMS · TRANSACTION_REF · OCR | | E-Birr | EBIRR | LINK · SMS · TRANSACTION_REF · OCR |


Verification Methods

| Method | verMethod | Pass as rawProof | |--------|------------|-------------------| | Receipt URL | LINK | Full URL string | | SMS body | SMS | Raw SMS text string | | Transaction reference | TRANSACTION_REF | Transaction / reference number | | Image (OCR) | OCR | File path string or Buffer | | Screenshot | SCREENSHOT | Alias of OCR |

By link

const result = await engine.verify({
  bank: "CBE",
  amount: 500,
  verMethod: "LINK",
  rawProof: "https://apps.cbe.com.et:100/?id=FT26093JCD3218872366",
});

By SMS

const result = await engine.verify({
  bank: "TELEBIRR",
  amount: 250,
  verMethod: "SMS",
  rawProof:
    "Dear Ephrem, You have transferred ETB 250.00. Your transaction number is DEV6HKJX7K.",
});

By transaction reference

const result = await engine.verify({
  bank: "TELEBIRR",
  amount: 250,
  verMethod: "TRANSACTION_REF",
  rawProof: "DEV6HKJX7K",
});

CBE note: When passing a 12-character base reference (e.g. FT26093JCD32), also provide accountNumber so the engine can build the full receipt URL.

const result = await engine.verify({
  bank: "CBE",
  amount: 500,
  verMethod: "TRANSACTION_REF",
  rawProof: "FT26093JCD32",
  accountNumber: "1000000018872366",
});

By screenshot / OCR

const result = await engine.verify({
  bank: "CBE",
  amount: 500,
  verMethod: "OCR",
  rawProof: "/tmp/receipt-screenshot.png", // or a Buffer
});

OCR uses Tesseract.js internally by default. Supply a custom ocrReader for higher accuracy (e.g. Google Cloud Vision):

const engine = new VerificationEngine({
  ocrReader: async (input) => {
    // input is a string path or Buffer
    const text = await myVisionApi.recognize(input);
    return text;
  },
});

Amount tolerance

The engine compares payload.amount with the parsed receipt amount. Use amountTolerance when small rounding differences are expected.

await engine.verify({
  bank: "CBE",
  amount: 500,
  amountTolerance: 0.05, // default is 0.01
  verMethod: "LINK",
  rawProof: "https://...",
});

Proxy Support

Some banks (notably Telebirr) block requests from foreign IP addresses. Implement the ProxyResolver interface to provide per-country proxy config from your own data store. The engine resolves it transparently — parsers never deal with proxy config directly.

import {
  VerificationEngine,
  ProxyResolver,
  ProxyConfig,
  ProxyType,
} from "@localpay/verification-engine";

class MyProxyResolver implements ProxyResolver {
  async resolve(countryCode: string): Promise<ProxyConfig | null> {
    const row = await db.countryProxy.findUnique({ where: { countryCode } });
    if (!row?.proxyEnabled) return null;
    return {
      enabled: true,
      url: row.proxyUrl,           // "http://user:[email protected]:8080"
      type: row.proxyType as ProxyType, // HTTP_CONNECT or SOCKS5
    };
  }
}

const engine = new VerificationEngine({
  proxyResolver: new MyProxyResolver(),
});

Both HTTP_CONNECT (default) and SOCKS5 proxy types are supported. Proxy config is resolved per request — toggling it in your database takes effect immediately with no redeploy.


Adding a New Bank

Implement the ParserAndExtractor interface:

import {
  ParserAndExtractor,
  ParserFetchContext,
  RawReceipt,
} from "@localpay/verification-engine";

export class MyBankParser implements ParserAndExtractor {
  /** Extract receipt URL from SMS body or OCR text */
  extract(text: string, accountNumber?: string): { link: string } {
    const match = text.match(/my-bank\.com\/receipt\/([A-Z0-9]+)/i);
    if (!match) return { link: "" };
    return { link: `https://my-bank.com/receipt/${match[1]}` };
  }

  /** Build receipt URL directly from a transaction reference */
  transactionRef(ref: string): { link: string } {
    return { link: `https://my-bank.com/receipt/${ref}` };
  }

  /** Download the receipt page — always use context.fetcher for proxy support */
  async fetch(link: string, context?: ParserFetchContext): Promise<{ page: any }> {
    const response = context
      ? await context.fetcher.fetch(link, context.countryCode)
      : await fetch(link).then(async (r) => ({ data: await r.text() }));
    return { page: response.data };
  }

  /** Parse the page into a structured receipt */
  async receiptParser(page: any): Promise<{ bank: string; receipt: RawReceipt }> {
    return {
      bank: "MY_BANK",
      receipt: {
        transactionNumber: "...",
        date: "...",
        amount: "...",
        receiverAccount: "...",
        receiverName: "...",
      },
    };
  }
}

Pass it to the engine alongside the built-in parsers:

import { VerificationEngine, PARSER_REGISTRY } from "@localpay/verification-engine";
import { MyBankParser } from "./my-bank.parser";

const engine = new VerificationEngine({
  parsers: {
    ...PARSER_REGISTRY,   // keep all built-in banks
    MY_BANK: new MyBankParser(),
  },
});

Check registered banks at runtime:

engine.getSupportedBanks();
// → ["CBE", "TELEBIRR", "ABYSSINIA", "EBIRR", "MY_BANK"]

NestJS Integration

The engine is a plain class — wrap it in a NestJS service:

import { Injectable } from "@nestjs/common";
import { VerificationEngine, ProxyResolver } from "@localpay/verification-engine";
import { PrismaService } from "./prisma.service";

@Injectable()
export class VerificationService {
  private readonly engine: VerificationEngine;

  constructor(private readonly prisma: PrismaService) {
    const proxyResolver: ProxyResolver = {
      resolve: async (countryCode) => {
        const row = await this.prisma.countryProxy.findUnique({
          where: { countryCode },
        });
        if (!row?.proxyEnabled) return null;
        return { enabled: true, url: row.proxyUrl, type: row.proxyType as any };
      },
    };

    this.engine = new VerificationEngine({ proxyResolver });
  }

  verify(payload: Parameters<VerificationEngine["verify"]>[0]) {
    return this.engine.verify(payload);
  }
}

API Reference

new VerificationEngine(options?)

| Option | Type | Description | |--------|------|-------------| | proxyResolver | ProxyResolver \| null | Per-country proxy config provider | | ocrReader | (input: RawProof) => Promise<string> | Custom OCR function | | parsers | ParserRegistry | Override or extend the parser registry |

engine.verify(payload)

| Field | Type | Required | Description | |-------|------|----------|-------------| | bank | string | ✅ | Parser key e.g. "CBE", "TELEBIRR" | | amount | number | ✅ | Expected transfer amount | | verMethod | VerificationMethod | ✅ | How to verify | | rawProof | string \| Buffer | ✅ | The proof to verify | | accountNumber | string | — | Sender account (CBE 12-char refs) | | countryCode | string | — | ISO country code, defaults to "ET" | | amountTolerance | number | — | Amount diff tolerance, defaults to 0.01 |

Result

type VerifyResult =
  | { status: "SUCCESS"; receipt: { bank: string; receipt: RawReceipt } }
  | { status: "FAIL";    reason: string };

RawReceipt

interface RawReceipt {
  transactionNumber: string;
  date:              string;
  amount:            string;
  receiverAccount:   string;
  receiverName:      string;
}

Utilities

import { safeParsDate, parseDate } from "@localpay/verification-engine";

// Returns null instead of throwing on bad input
safeParsDate("18-03-2026 21:46:09"); // → Date object
safeParsDate("not a date");          // → null

// Throws on unrecognised format
parseDate("18-03-2026 21:46:09");    // → Date object

Supported date formats:

| Bank | Format example | |------|---------------| | eBirr | 2026-02-11 20:07:02 +0300 EAT | | Telebirr | 18-03-2026 21:46:09 | | CBE PDF | 3/11/2026, 6:15:00 PM | | BOA | 23/01/26 14:04 · 23/01/2026 14:04 |


Contributing

# Clone and install
git clone https://github.com/oneshotEFA/verification-engine.git
cd verification-engine
npm install

# Build
npm run build

# Run tests
npm test

# Type-check only
npm run lint

To add a new bank:

  1. Create src/parsers/<country>/<bank>.parser.ts
  2. Implement ParserAndExtractor
  3. Export from src/parsers/<country>/index.ts
  4. Spread into src/parsers/index.ts
  5. Add test cases in tests/verification-engine.test.js

License

MIT · Built by LocalPay