Guidance for AI coding agents working in this repository. Human contributors should read it too — everything here is a real constraint, not a preference.
paste-to-verify confirms that an Ethiopian CBE or Telebirr payment
actually happened, starting from the SMS the customer received. It extracts the
receipt link, fetches the provider's receipt page, normalizes it, and runs
deterministic checks.
People use this to decide whether money arrived. A wrong verified: true is the
worst possible bug in this codebase. Treat correctness as the priority over
features, performance, or elegance.
packages/core/ the published npm package (paste-to-verify)
src/ library source — the only code that ships
tests/ vitest suites + synthetic fixtures
scripts/ live smoke checks, run manually, never in CI
apps/docs/ landing page + docs site (Next.js + Fumadocs)
apps/examples/ runnable demo servers — express/ and nextjs/
packages/* is for publishable npm libraries; apps/* is for deployables
and consumers and is never published. A future PHP port belongs in a top-level
php/ directory with its own composer.json — do NOT put it under packages/,
which is claimed by the pnpm workspace.
Use pnpm. Never npm install, yarn, or bun — the lockfile is pnpm's.
pnpm install
pnpm build # turbo: core, docs, examples
pnpm test # vitest with coverage
pnpm typecheck
pnpm lint
pnpm format # prettier --write
pnpm format:check # what CI enforcesScoped runs while iterating:
pnpm --filter paste-to-verify test
pnpm --filter @paste-to-verify/docs dev # :3000
pnpm --filter @paste-to-verify/example-express dev # :3001Before claiming work is done, run pnpm build && pnpm test && pnpm typecheck && pnpm lint && pnpm format:check — that is exactly what CI runs.
The receipt page is the source of truth. The SMS is not. The SMS supplies the receipt URL and fills fields the page omits, nothing more. When both sources have a field, the page value must win. Any change that lets SMS text override a page value is a security bug, because an SMS is trivially forged.
amount is the principal credited to the receiver, excluding fees.
Fees, VAT, levies, totals and balances live under raw. The CBE SMS headline
figure (e.g. 990.61) is the total debited, not the principal — that is why
cbeProvider.parseSms deliberately never sets amount.
parseReceipt never throws an untyped error.
Every failure path must surface as a PasteToVerifyError subclass with a stable
code. Unexpected errors are wrapped in ReceiptParseError with the original
as cause. Never let a raw TypeError escape.
verifyTransaction is pure, synchronous, and never throws.
No network, no I/O, no clock reads except options.now. Failures are returned
in failures, never raised.
Provider URLs match by hostname, never by substring.
Use urlMatchesHost(url, host). url.includes('cbe.com.et') is a spoofing hole
— https://cbe.com.et.evil.example/x contains it. There is a regression test
for this; do not weaken it.
Timestamps are parsed by component and converted from EAT (UTC+3) to UTC.
Never new Date(someString) — that silently assumes the host's timezone.
Never commit real receipt data.
Committed fixtures and docs examples are synthetic and anonymized. Real receipts
carry names, account numbers, and a working link. scripts/save-fixtures.mjs
overwrites fixtures with live personal data — its output must never be
committed.
Never reformat files under packages/core/tests/fixtures/.
They are prettier-ignored on purpose: the parsers read text nodes and
whitespace, so reformatting silently changes what the tests assert.
- TypeScript, strict. No
anyin shipped code; prefer precise types and narrow with type guards. - Every exported symbol gets a JSDoc block explaining what it does and any
non-obvious contract. Match the density already in
src/— comments explain why, not what. - Prettier owns formatting (single quotes, semicolons, 100 columns, trailing
commas). Do not hand-format; run
pnpm format. - Keep
packages/coredependency-light. It has exactly one runtime dependency (node-html-parser) and one optional peer (playwright). Adding a runtime dependency needs a real justification. - Node.js 18+ is the floor; the package ships ESM + CJS. Do not use APIs newer
than Node 18 in
src/. - Anything that touches the network or a browser must be injectable through
RuntimeOptions(fetchImpl,render,timeoutMs) so it can be tested and run on edge runtimes.
A provider is one object implementing ProviderPlugin: id, urlPattern,
matchesUrl, parseSms, fetchAndParse, plus the optional buildReceiptUrl
that powers parseReceiptId.
fetchAndParseis authoritative — return only what the page actually shows.parseSmssupplies fallbacks only, and must not guess atamountwhen the SMS headline is a total rather than a principal.- Throw
ReceiptFetchErrorfor network/non-2xx andReceiptParseErrorwhen required fields are missing. - Register built-ins in
src/providers/registry.ts. Consumers instead push onto the exportedprovidersarray — the docs must never tell a package user to recreate that file.
CBE-specific: the receipt token in the URL path (v2-…) is NOT the FT…
transaction reference. buildReceiptUrl takes the token.
- Vitest. Tests must never hit the network or launch a browser — inject
stubs from
tests/helpers.ts(stubFetch,recordingFetch,failingFetch,stubRenderer) viaRuntimeOptions. - Coverage on
packages/core/srcstays above 90%. New branches need tests. - Every bug fix gets a regression test that fails before the fix.
- Test the contract, not the implementation: assert on the normalized
transaction and on
failures, not on internal helper calls. - The
scripts/*.mjslive checks are manual, need a gitignored.env, and must stay out of CI.
- Content is MDX under
content/docs/;meta.jsoncontrols sidebar order and section grouping. Frontmattericon:accepts any Lucide icon name. Callout,Cards,Steps,Tabs,TypeTableand code blocks are available in MDX without imports (seemdx-components.tsx).- Every factual claim must be verified against
packages/core/srcbefore it is written. The docs are the contract users read; drift is a bug. - Headings become anchor ids. When linking cross-page anchors, confirm the id exists rather than guessing the slug.
- Their
dev/startscripts rebuildpackages/corefirst, on purpose: they import the builtdist, and a stale build silently breaks newer exports. Keep that step. - Demo mode injects fixture-backed
fetchImpl/renderfor the sample receipts, so the demos work with no network. Keep them working offline. - Both servers accept
{ sms } | { url } | { provider, id }and must map typed error codes to HTTP status consistently with the docs.
Semver, currently 0.x — minor versions may carry breaking changes.
- Update
CHANGELOG.md. - Bump
versioninpackages/core/package.json. git tag vX.Y.Z && git push origin vX.Y.Z.
The Release workflow builds, tests, and publishes to npm via Trusted
Publishing (OIDC) with provenance. Do not publish by hand unless that workflow
is broken, and never commit an npm token.
- Write imperative subject lines that say what changed and why it matters. Explain reasoning in the body when the change is not self-evident.
- Do not add AI attribution, co-author trailers, or tool advertisements to commits, PRs, or code comments.
- Rebase onto the latest
mainbefore opening a PR;mainis squash-merged and linear. - Do not commit or push unless the user explicitly asks.