A command-line tool for batch validation of Nigerian bank account numbers (NUBAN). Reads accounts from a CSV file, queries the NUBAN API with bounded concurrency and per-request rate limiting, and writes the results to an Excel workbook with per-row status and a summary sheet.
- Node.js 18 or newer.
- A NUBAN API key. Sign up and generate one at https://app.nuban.com.ng.
git clone <repo-url> nuban-cli
cd nuban-cli
npm install
cp .env.example .env
# Open .env and set NUBAN_API_KEY to your key.Basic run (uses defaults from .env):
node src/cli.js --input accounts.csvCustom output path:
node src/cli.js -i accounts.csv -o reports/may-2026.xlsxOverride concurrency for this run only:
node src/cli.js -i accounts.csv -c 10Show stack traces on error (for debugging):
DEBUG=true node src/cli.js -i accounts.csvRetry only the transient failures from a previous run, updating that file in place:
node src/cli.js --retry-from output.xlsxThis reads a previous run's output workbook and re-processes only the rows whose
Status is API_ERROR with a transient Error reason (TIMEOUT, NETWORK_ERROR,
RATE_LIMITED, API_BUSY). Rows that failed for a deterministic reason (NOT_FOUND,
INVALID_ACCOUNT, UNKNOWN_ERROR) or that never reached the API (VALIDATION_ERROR)
are skipped — retrying them would just reproduce the same result while burning through
your rate-limit budget. Every other row (successes, deterministic failures) is carried
over unchanged. --retry-from and -i/--input are mutually exclusive.
By default this overwrites output.xlsx in place — the retried rows are spliced
back into their original positions and the file is rewritten whole. Pass -o to write
the merged result somewhere else instead and leave the source file untouched:
node src/cli.js --retry-from output.xlsx -o retry.xlsxThe first row must be headers. Header names are case-insensitive; the following variants are accepted:
| Canonical field | Accepted header variants |
|---|---|
account_number |
account_number, account_no, acc_no, accountnumber |
bank_code |
bank_code, bank_id, bankcode |
provided_name |
provided_name, name, account_name, beneficiary_name (optional) |
Per-row validation:
account_number— exactly 10 digits.bank_code— exactly 3 or 6 digits.
Rows that fail validation are not aborted; they appear in the output with status = VALIDATION_ERROR.
If a provided_name column is present, each successfully resolved row is scored against the
API's account_name using the same fuzzy name-matching logic as bankvalidatorinternal
(src/name-matching.js) — see Name matching below. Rows without a
provided_name value are not scored (Match Score = N/A); this is not treated as a mismatch.
Example:
account_number,bank_code,provided_name
0123456789,057,John Doe
1234567890,058,Jane Smith
2345678901,057,Ade BelloOne row per input record, in original CSV order. Failed rows have a light-red background; the header is frozen.
| Column | Contents |
|---|---|
| Account Number | from input |
| Bank Code | from input |
| Provided Name | from input (empty if no provided_name column) |
| Bank Name | from API on success; empty on failure |
| Account Name | from API on success; error reason on failure |
| Match Score | 100/70/50/0 (as NN%) on success; N/A if no provided_name or on failure |
| Similarity Status | reason text for the match score (e.g. Verified Match); N/A on failure |
| Status | SUCCESS, API_ERROR, or VALIDATION_ERROR |
| Error | error reason on failure; empty on success |
API error reasons (appear in Error, and in Account Name for failed rows):
| Reason | Trigger |
|---|---|
NOT_FOUND |
API returned no matching account. |
INVALID_ACCOUNT |
API rejected the input as malformed. |
TIMEOUT |
Request exceeded REQUEST_TIMEOUT_MS. |
NETWORK_ERROR |
DNS / connection failure. |
RATE_LIMITED |
API returned HTTP 429. |
API_BUSY |
API returned a "try again" error body — its own transient failure, not the account/bank data. |
UNKNOWN_ERROR |
Anything else (5xx, malformed response, etc.). |
TIMEOUT, NETWORK_ERROR, and API_BUSY are retried automatically within a single run
(RETRY_ATTEMPTS); API_BUSY and RATE_LIMITED are also what --retry-from looks for
when re-running a previous output (see Usage).
VALIDATION_ERROR rows carry a human-readable validation message in Error (e.g. account_number must be exactly 10 digits).
A two-column key/value table:
| Metric | Value |
|---|---|
| Total rows | count |
| Succeeded | count |
| Failed (API) | count |
| Failed (Validation) | count |
When a row has both a provided_name and a successful API account_name, the two are
scored with the same fuzzy matcher bankvalidatorinternal uses (src/name-matching.js,
ported from backend/utils/nameMatching.js so scoring stays identical between the two
tools). It normalizes accents/case, strips filler words (ltd, mr, bank, …), and
matches tokens by exact, initial, and typo-tolerant (Levenshtein ≥ 85%) comparison, then
buckets the result:
| Score | Reason | Meaning |
|---|---|---|
| 100 | Verified Match |
Every provided name token matched, same token count. |
| 70 | Partial Match (Verify) |
2+ tokens matched, but not a perfect set. |
| 50 | High Risk (Incomplete) |
Exactly 1 token matched. |
| 0 | Mismatch (Rejected) |
No tokens matched. |
| 0 | N/A |
No provided_name was supplied for this row. |
Set these in .env. Only NUBAN_API_KEY is required.
| Variable | Default | Purpose |
|---|---|---|
NUBAN_API_KEY |
(required) | Your NUBAN API key. The CLI exits with code 1 if missing. |
CONCURRENCY_LIMIT |
5 |
Max simultaneous in-flight API requests. Override with -c. |
REQUESTS_PER_SECOND |
0.25 |
Token-bucket refill rate. The default 0.25 matches the NUBAN free-tier limit of 15 req/min. Fractional values allowed. |
REQUEST_TIMEOUT_MS |
5000 |
Per-request timeout in milliseconds. |
RETRY_ATTEMPTS |
2 |
Retries per request after the first failure (only TIMEOUT / NETWORK_ERROR). |
OUTPUT_FILE |
output.xlsx |
Default output path. Override with -o. |
CONCURRENCY_LIMIT and REQUESTS_PER_SECOND are independent: concurrency caps how many requests can be in flight simultaneously, while the rate limiter caps long-run throughput. With the default 0.25 rps, throughput is the binding constraint regardless of concurrency — at startup the CLI prints an estimate so you know what to expect (e.g. Estimated time: ~67 min for 1000 rows at 15 req/min).
Error: NUBAN_API_KEY is not set. Copy .env.example to .env and fill in your key.
You haven't created .env, or the key is blank. Run cp .env.example .env and populate NUBAN_API_KEY.
High failure rate (many TIMEOUT or RATE_LIMITED rows).
The API is being asked for more than it can serve. Lower CONCURRENCY_LIMIT in .env, or pass -c 2 and raise gradually until failures stay under your tolerance.
Most or all requests time out.
First, confirm the host is reachable: curl -I https://app.nuban.com.ng. If your network is reachable but slow, raise REQUEST_TIMEOUT_MS (e.g. to 15000). If you suspect a different bug, run with DEBUG=true to see the underlying stack trace.
.env is .gitignored because it holds your NUBAN API key — that key authenticates billable API calls and grants access to account-lookup data.
- Never commit
.env, the raw key, or any log/screenshot containing the key. If a key leaks, rotate it immediately at https://app.nuban.com.ng. - The CLI loads the key into a non-enumerable property of the config object, so
console.log(config)andJSON.stringify(config)cannot expose it. - The logger redacts any metadata field whose name contains
key,token,secret,password, orauthorization. - The HTTP client rewrites the API key out of any error URL or message before it reaches the logger.
These defenses reduce accidental leakage but do not substitute for keeping .env private.