Invoice on Stellar. Get paid. Keep the proof.
Quittance helps freelancers create an invoice, accept payment via link or QR on Stellar, verify it on Horizon (memo + amount + destination), then download or email payment proof. Settlement stays on-chain. Quittance does not expose other people’s wallet identity or history.
Sharp initial user: freelancer invoicing a client in XLM/USDC on Stellar.
| Capability | Status |
|---|---|
| Freighter wallet as identity (create + pay) | Done — no Google login gate |
| Create invoice + payment URL / QR | Done |
Optional client email + Send invoice / Email proof (mailto:) |
Done |
| Horizon-backed payment verify | Done (memo, amount, destination, asset) |
| Dashboard scoped to connected wallet | Done |
| Primary Download Proof CTA after paid | Done (PDF print flow in browser) |
| Simulate-payment UI | Removed from demo UI (ALLOW_SIMULATE=true only on API) |
| Public hosted demo + testnet evidence pack | Phase D (not yet) |
| Postgres persistence (wallet-scoped invoices) | Optional — see Postgres persistence |
| SMTP / Gmail API | After demo (Phase E) |
Ship plan: PLAN.md.
- Connect Freighter and create an invoice (optional client name/email)
- Share the payment URL or QR — or Send invoice if email is set
- Client pays on Stellar (Freighter, QR, or manual transfer with the memo)
POST /api/invoices/:id/verifychecks the tx on Horizon- Download Proof (primary) or Email Proof (if client email exists)
Identity is the wallet. Email is an optional delivery channel, not a login gate.
Freighter is the only wallet gate for create and pay. The frontend stores one
session snapshot in frontend/lib/store.ts: extension availability, public key,
active Freighter network, network passphrase, balance, and connection state.
WalletSessionSync refreshes that snapshot on every route and watches Freighter
for account or network changes without reloading the page.
Landing, dashboard, create, pay, and invoice-detail all use the same gate matrix:
install Freighter when the extension is missing, connect Freighter when no public
key is available, switch networks when Freighter is not on
NEXT_PUBLIC_STELLAR_NETWORK, and continue only when the wallet is connected on
the expected network. The network check is passphrase-strict: when Freighter
reports a network passphrase it must equal the resolved network's passphrase
exactly — a custom network may call itself TESTNET but cannot forge
Test SDF Network ; September 2015. Wallets that report no passphrase fall
back to the network-name comparison. shared/network.ts is the single
resolver both deployments use: it maps the configured network to the
passphrase, the default Horizon URL, and the explorer segment, and proof
exports always claim the server's resolved network rather than a caller hint
(a mismatched ?network= query fails closed with 400).
The create form is only rendered while that gate is satisfied, so locking or
disconnecting Freighter mid-form unmounts it. The fields a person typed —
amount, asset, description, seller and client name/email, and the payment window
— are kept in sessionStorage by frontend/lib/invoice-draft.js: one key per
tab, dropped when the draft is empty and cleared once the invoice is created.
Nothing about the wallet is stored (no public key, signature, balance or invoice
id), so the draft holds only what was typed into the form.
Reconnecting on the expected network restores those fields and re-enables create in the same page load; reconnecting on the wrong network keeps create unavailable and shows the existing mismatch prompt instead of submitting an invoice the verifier would reject.
An invoice becomes PAID only when all four checks pass against Horizon:
memo, destination, amount, and asset (code and issuer for
non-native assets). A fifth guard rejects a transaction observed on a different
Stellar network.
One module owns these rules: backend/src/services/payment-verification.ts.
It is pure — the caller fetches the transaction and operations from Horizon and
passes them in. Every verify path routes through it, so the MVP and Postgres
handlers reject the same cases with the same wording:
| Path | Entry point |
|---|---|
| MVP (in-memory) | POST /api/invoices/:id/verify — backend/src/server-mvp.ts |
| Postgres | POST /api/invoices/:id/verify — backend/src/routes/invoice.handlers.ts |
| Standalone check | POST /api/stellar/verify-payment — backend/src/services/stellar.service.ts |
| Pay flow (client) | frontend/lib/verification.js — mirrors codes and messages |
Checks run in a fixed order, so every caller reports the same first failure:
tx hash → network → payment operation → memo → destination → amount → asset
Rejections return a stable code alongside the human-readable error:
| Code | Message | HTTP |
|---|---|---|
MISSING_TX_HASH |
Transaction hash is required | 400 |
INVALID_TX_HASH |
Transaction hash must be 64 hexadecimal characters | 400 |
INVALID_PAYER_NAME |
Payer name must be text | 400 |
INVALID_PAYER_EMAIL |
Payer email is invalid | 400 |
PAYER_INFO_TOO_LONG |
Payer information is too long | 400 |
INVOICE_ALREADY_PAID |
Invoice has already been paid | 400 |
INVOICE_EXPIRED |
Invoice has expired and can no longer accept payment | 400 |
INVOICE_NOT_PENDING |
Invoice is not pending | 400 |
TRANSACTION_NOT_FOUND |
Transaction not found on Stellar | 404 |
NO_PAYMENT_OPERATION |
No payment operation found in transaction | 400 |
MEMO_MISMATCH |
Memo mismatch | 400 |
DESTINATION_MISMATCH |
Payment destination mismatch | 400 |
AMOUNT_MISMATCH |
Amount mismatch | 400 |
AMOUNT_TOO_LOW |
Payment is less than the invoice amount | 400 |
AMOUNT_TOO_HIGH |
Payment is more than the invoice amount | 400 |
ASSET_MISMATCH |
Asset mismatch | 400 |
NETWORK_MISMATCH |
Transaction is on a different Stellar network | 400 |
The client mirror lets the pay page reject malformed input before a round trip and show the exact message the server would return. A test asserts the two tables stay identical — if you add a code, add it in both files.
Those codes are also how the app keeps every page on one copy of the wording.
frontend/lib/verification.js exposes messageForCode(code); the API layer
(api-runtime.js / apiErrorMessage) and the pay page (describeVerifyError)
resolve a stable code to that message before falling back to server text, so
the pay page, invoice detail, dashboard, and monitoring banners all read
identically for the same rejection. VERIFICATION_CODES and messageForCode
are exported from backend/src/services/payment-verification.ts as well, so a
test pins the two layers together.
Amounts compare at Stellar's 7-decimal (stroop) precision, so 100 and
100.0000000 match while a partial payment does not.
Run the checks: cd backend && npm test — cd frontend && npm test.
Sellers choose a payment window of 1–30 days (7 days by default). Both
storage backends persist expiresAt and lazily transition elapsed PENDING
invoices to EXPIRED before get, list, stats, verify, cancel, or monitor work.
Expired invoices remain visible in seller history, but they are excluded from
pending/actionable counts and cannot expose QR, pay, verify, or payment-proof
controls. The client also projects stale pending data through expiresAt so a
cached invoice stops offering payment after its deadline.
The policy for exact payments that land across an expiry or cancellation
boundary is documented in docs/LATE_PAYMENT_POLICY.md.
Quittance supports multi-asset invoicing across native XLM and credit assets such as USDC on Stellar:
- Native XLM: No issuer required, verified directly with native payment operations.
- Credit Assets (e.g. USDC): Verified with
asset_type,asset_code, and pinnedasset_issuer. - Trustline UX: The pay flow inspects trustline status and provides actionable guidance (
op_no_trusthandling) if the buyer wallet needs to add a trustline.
Sellers manage their invoices from the dashboard and detail views:
- Cancel Pending Invoices: Sellers can cancel any pending invoice before payment or expiration. Cancellation uses one proof path (issue #517): the request body carries
sellerPublicKeyplus a Freighter signature over the canonical messagecancel:<invoiceId>— the UI signs viasignBlobbefore callingPOST /api/invoices/:id/cancel. Query params andx-seller-public-keyheaders are not accepted as transports, and a value that disagrees with the body returns400. A foreign signer returns403; a missing or invalid signature returns401; onlyPENDINGinvoices can cancel. Signatures are mandatory in production (NODE_ENV=productionorREQUIRE_CANCEL_SIGNATURE=true); local dev/tests keep the signature optional bypass. - Copy Pay & Share Links: Direct quick-copy actions with toast feedback for pay URLs and invoice IDs across dashboard cards and detail pages.
- Proof & Receipt Navigation: One-click jump to verified PDF payment proof and transaction details for all
PAIDinvoices.
backend/src/server-dual.ts is the unified server entrypoint that selects
the storage backend via an environment variable. Both the in-memory MVP and
PostgreSQL back the same handler code through the InvoiceStorage interface,
so a bug fix or new feature in a handler applies to both backends at once.
INVOICE_STORAGE |
DATABASE_URL present |
Backend used |
|---|---|---|
memory (explicit) |
any | In-memory (no DB needed) |
postgres (explicit) |
✓ | PostgreSQL |
postgres (explicit) |
✗ | Boot error — fails immediately with a clear message |
| (unset) | ✓ | PostgreSQL (implicit) |
| (unset) | ✗ | In-memory (implicit default) |
Memory is the safe default. Setting INVOICE_STORAGE=postgres without
providing DATABASE_URL is always an error — the server never silently falls
back to memory when Postgres was explicitly requested.
cd backend
# In-memory (no database):
npm run dev:dual
# PostgreSQL (database required):
INVOICE_STORAGE=postgres DATABASE_URL=postgresql://... npm run dev:dual
# Or via .env:
echo "INVOICE_STORAGE=postgres" >> .env
echo "DATABASE_URL=postgresql://user:pass@localhost:5432/quittance" >> .env
npm run dev:dualcd backend
npm run db:migrate # applies db/schema.sql (idempotent, safe to re-run)
npm run db:seed # optional: two demo sellers for wallet-scoping visibilitycd backend
npm run test:dual # invoice-dual-backend, postgres-restart-persistence, cross-seller-isolation
npm test # full suite: all of the above + existing testsSet INVOICE_STORAGE=memory in backend/.env and restart, or switch to the
hardcoded MVP entrypoint (npm run dev:mvp). The PostgreSQL database is not
modified; rows persist and reappear when you re-enable Postgres.
See Postgres persistence (optional) for migration commands, test flags, and the full cutover procedure.
| Layer | Tech |
|---|---|
| Frontend | Next.js 14, TypeScript, Tailwind, Freighter |
| Backend (local / demo) | Express, TypeScript, in-memory MVP (server-mvp.ts) |
| Chain | Stellar testnet / public via Horizon |
| Later | PostgreSQL full server (server.ts, not required for v0.1) |
Both entrypoints share one invoice route layer. Only the storage adapter differs:
server-mvp.ts ─┐ ┌─ memory-invoice-storage.ts (in-memory)
├─ routes/invoice.routes.ts ─ routes/invoice.handlers.ts ─ InvoiceStorage
server.ts ─────┘ └─ postgres-invoice-storage.ts (PostgreSQL)
src/routes/invoice.handlers.ts— the only implementation of create / get / list / verify / cancel / stats / payment-info / simulate.src/storage/invoice-storage.ts— theInvoiceStorageinterface both backends implement. Seller keys are always wallet-scoped: they come from the invoice payload or thesellerPublicKeyquery parameter, never from a static env key.src/types/api.ts— the sharedApiResponseenvelope ({ success, data, message?, pagination? }or{ success: false, error }) used by every route on both servers.
A bug fix in a handler applies to both servers at once.
The event taxonomy, correlation rules, redacted JSON examples, and PaaS metric
queries for this request path are defined in
docs/STRUCTURED_LOGGING.md.
- Node.js 18+
- Freighter for wallet flows — see Freighter docs
- Stellar testnet account for real payments (Laboratory)
PostgreSQL and Redis are not required for the MVP path below.
- Install the Freighter browser extension and create or import a wallet.
- Open Freighter's network menu and select Testnet. The official Connect to the Testnet guide shows the same flow.
- Copy your public account address (it starts with
G). Use Freighter's Fund with Friendbot prompt, or open Stellar Lab's Fund Account page, paste the address, and select Get testnet XLM.
Only use your public G... address with Friendbot. Never paste a secret key or recovery phrase into a funding form. Testnet XLM has no real-world value and may disappear when Stellar resets Testnet.
Follow these steps from a fresh clone. Use two terminals so the backend and frontend can run at the same time.
git clone https://github.com/Kappa16/Quittance0.git
cd Quittance0cd backend
npm i
cp env.mvp.example .env
npm run dev:mvpKeep this terminal running.
- API:
http://localhost:3001/api - Health check:
http://localhost:3001/api/health - MVP server entrypoint:
backend/src/server-mvp.ts
Optional: set ALLOW_SIMULATE=true in backend/.env only for local fake payments (not for demos).
Open a second terminal from the repo root:
cd frontend
npm i
cp env.mvp.local .env.local
npm run devOpen the app at http://localhost:3000.
- The MVP backend is intentionally in-memory: invoices and payment state clear every time the backend process restarts.
- PostgreSQL and Redis are not needed for the local MVP path.
FRONTEND_URLinbackend/.envmust match the frontend origin for CORS; the provided MVP env useshttp://localhost:3000.NEXT_PUBLIC_API_URLinfrontend/.env.localmust include/api; the provided MVP env useshttp://localhost:3001/api.
- Backend MVP template:
backend/env.mvp.example→ copy tobackend/.env - Frontend MVP template:
frontend/env.mvp.local→ copy tofrontend/.env.local - Full frontend template:
frontend/env.example.txt
Use this path when invoices must survive a backend restart. Identity is still the
connected Freighter wallet: every invoice is stored under its seller_public_key,
and list/stats endpoints only return the requesting wallet's invoices.
For a live move from the MVP without changing existing /pay/[id] links or
payment proofs, follow the in-memory to Postgres cutover plan.
In backend/.env (template: backend/env.example.txt):
DATABASE_URL=postgresql://user:password@localhost:5432/quittance
SELLER_PUBLIC_KEY / SELLER_SECRET_KEY are optional. They are only used by
the single-account Horizon payment monitor; without them the server starts in
wallet-scoped mode and the monitor stays off.
The monitor polls from a durable Horizon paging token and resumes after
restarts. Its recovery model, failure matrix, operator endpoint, and Testnet
restart evidence are documented in docs/PAYMENT_MONITOR.md.
cd backend
npm run db:migrate # applies db/schema.sql (idempotent, safe to re-run)
npm run db:seed # optional: sample invoices for two demo walletsdb/schema.sql— invoices, transactions, payment_events,invoice_statsview. Re-running it also drops the legacyuserstable andinvoices.user_idcolumn from older databases.db/seed.sql— invoices for two demo seller wallets so wallet scoping is visible locally. Swap a seedseller_public_keyfor your own Freighter address to see the rows in your dashboard.- Runners:
backend/src/db/migrate.ts,backend/src/db/seed.ts.
cd backend
npm run dev # src/server.ts (Postgres) instead of dev:mvp (in-memory)The dashboard sends the connected wallet on every call:
GET /api/invoices?sellerPublicKey=G... and GET /api/invoices/stats?sellerPublicKey=G...
both return 400 when the seller key is missing.
POST /api/invoices is idempotent (issue #514): send an Idempotency-Key
header (or idempotencyKey body field) and a replay returns the original
invoice — same id, memo, and pay link — instead of minting a second one.
Keyless retries are still safe: identical create intent from the same seller
inside a 2-minute window collapses onto the original row.
GET /api/invoices/:id serves two shapes from one record (issue #503): anonymous
callers — including the /pay/:id checkout page — receive the public pay DTO
(amount, asset, memo, destination, status, expiry, payment fields only), while
GET /api/invoices/:id?sellerPublicKey=<the invoice's own seller key> returns
the full workspace record with client contact and payer identity. The same
public shape is embedded in GET /api/invoices/:id/payment-info and returned
by POST /api/invoices/:id/verify.
cd backend
npm run typecheck # TypeScript compile check (no emit)
npm test # unit + scoping + parity tests
npm run test:isolated # standalone regression tests in tests/isolated
DATABASE_URL=postgresql://user:password@localhost:5432/quittance_test npm test # adds the Postgres integration testThe integration test (backend/tests/invoice-postgres.integration.test.ts) is
skipped unless DATABASE_URL is set. Point it at a disposable database — it
applies the schema, runs the seed twice to check idempotency, and writes rows
covering create, list, status filter, pagination, verify (markAsPaid with all
payer fields), expiry-time guard, cancel once-only, markExpiredInvoices lazy
transition, and the PostgresInvoiceStorage adapter end to end.
npm test also exercises the shared invoice handlers against both the in-memory
and PostgreSQL storage adapters, so create/verify/cancel/list/expiry/stats
regressions surface without a live database. Every backend path uses the same
StoredInvoice fields (seller metadata, asset issuer, expiry, payer info,
metadata) — the handler suite is parameterised over both adapters, so field
parity between memory and Postgres stays pinned by the same assertions.
- Create/import the project in Vercel and set Root Directory to
frontend. - Keep the Next.js preset;
frontend/vercel.jsonusesnpm ci, builds.next, and applies the public security headers. - Add these variables to Production (and Preview when preview deploys should call the API):
| Variable | Example |
|---|---|
NEXT_PUBLIC_API_URL |
https://YOUR-API-HOST/api |
NEXT_PUBLIC_STELLAR_NETWORK |
TESTNET |
NEXT_PUBLIC_HORIZON_URL |
https://horizon-testnet.stellar.org |
NEXT_PUBLIC_APP_URL |
https://YOUR-APP.vercel.app |
- Run
npm run deploy:checklocally with the same variables before deploying. - Deploy. A missing/invalid production API URL fails closed in the UI with an explicit configuration warning; it never falls back to a visitor's localhost.
Templates: frontend/env.example.txt, frontend/env.mvp.local.
Recommended host for server-mvp.ts (in-memory). backend/vercel.json now has
an optional serverless MVP entrypoint, but Render is the documented demo path
because it exposes normal liveness/readiness checks and predictable logs.
For complete details across Render, Fly.io, Railway, and Kubernetes, see docs/DEPLOYMENT.md.
- Liveness (
/api/healthor/health): Monitors process status. Probed by platforms to decide container restarts. Responds immediately (200 OK) without touching external APIs or databases, preventing cold-start restart loops. - Readiness (
/api/readyor/ready): Monitors deployment configuration and service readiness. Probed by platforms to gate traffic routing. Fails fast (503 Service Unavailable) if critical variables (FRONTEND_URL,STELLAR_NETWORK,STELLAR_HORIZON_URL,ALLOW_SIMULATE) are missing or misconfigured. In-memory demo deploy is never blocked on PostgreSQL. Optional Horizon connectivity ping can be enabled viaHEALTH_HORIZON_PING=true.
- Create a Web Service on Render from this repo.
- Root Directory:
backend - Build:
npm ci && npm run build - Start:
npm run start:mvp:prod - Health check path:
/api/ready(Render uses this to gate traffic and deploy transitions;/api/healthremains liveness) - Environment variables:
| Variable | Value |
|---|---|
NODE_ENV |
production |
STELLAR_NETWORK |
TESTNET |
STELLAR_HORIZON_URL |
https://horizon-testnet.stellar.org |
FRONTEND_URL |
https://YOUR-APP.vercel.app (exact frontend origin) |
FRONTEND_URLS |
Optional comma-separated preview/custom origins |
ALLOW_SIMULATE |
false |
HEALTH_HORIZON_PING |
false (optional, default false) |
PORT is set by Render automatically.
backend/render.yaml is a complete Blueprint for this path. Set the unsynced
FRONTEND_URL value in Render; origins are exact and wildcards are rejected.
- Copy the public API URL (e.g.
https://quittance-api.onrender.com). - Set frontend
NEXT_PUBLIC_API_URLtohttps://…/apiand redeploy Vercel. - Confirm CORS: browser call from the Vercel origin to
/api/healthsucceeds. - Confirm
GET /api/readyreturns HTTP 200 withready: true. - Run the deployed create/read round-trip:
DEPLOY_API_URL=https://YOUR-API-HOST/api node scripts/deploy-smoke.mjsThe smoke command creates one tiny in-memory XLM invoice, reads it back, and checks health/readiness. It never simulates or submits a Stellar payment.
- Reserve/deploy the Vercel project URL.
- Configure that exact origin as Render
FRONTEND_URL, then deploy Render. - Put the Render URL plus
/apiinto VercelNEXT_PUBLIC_API_URLand redeploy. - Run the smoke command and the browser checklist in
EVIDENCE.md.
Note: Free-tier / in-memory means cold starts and process restarts clear all invoices. Fine for a short demo; document this for reviewers.
Env template: backend/env.mvp.example.
A Stellar asset is the pair (code, issuer), never the code alone — anyone can
issue a credit asset coded USDC, or even XLM. How invoices name assets and
how settlement compares them is documented in
docs/ASSETS.md and docs/VERIFY.md.
The current SEP-0007 field map, wallet test vectors, memo byte limit, and
Testnet recommendation are in docs/SEP_0007_QR.md.
Freighter mobile feasibility, device/browser compatibility matrix, non-custodial
fallback UX without Google login, and demonstration instructions are in
docs/MOBILE_PAY_FEASIBILITY.md and
docs/MOBILE_DEMO_SCRIPT.md.
Every pull request and every push to main runs the jobs defined in
.github/workflows/ci.yml: backend,
frontend, root-tests (shared contracts), and an optional
evidence-smoke. All of them are reproducible locally with the commands
below — CI runs nothing you cannot run yourself.
The root package.json is a thin, dependency-free
orchestration layer over shared/backend/frontend's own scripts — it
has no node_modules of its own to install. Each command below is the
exact local equivalent of one CI job or step; npm run ci runs the same
sequence CI does end to end (except evidence-smoke, which needs real
Testnet secrets evidence:smoke documents separately below).
# The full local mirror of CI (installs backend + frontend deps first)
npm run ci
# Individual pieces, once dependencies are installed:
npm run test:shared # Shared export/contract tests (repository root)
npm run test:backend # Backend: unit + integration tests
npm run test:frontend # Frontend: unit tests
npm run test:a11y # Frontend: focused axe, focus, live-region, contrast checks
npm run lint:frontend # Frontend: next lint
npm run typecheck # Backend + frontend: tsc --noEmit
npm test # test:shared + test:backend + test:frontend togetherThese call straight through to each package's own scripts (e.g.
npm --prefix backend test), so cd backend && npm test still works
exactly as before if you'd rather work inside one package at a time.
The focused accessibility suite renders the landing, dashboard, pay, and
invoice-detail routes in jsdom, audits them with axe, and directly checks the
focus and live-region behavior that a static axe scan cannot observe. Because
jsdom has no layout engine, WCAG contrast ratios are verified separately from
the color pairs declared in frontend/tailwind.config.js.
backend/tests/invoice-payment-loop.test.ts exercises the whole core loop —
create invoice → pay → verify → status PAID — against the real Express
app, the real validation and the real in-memory store.
Horizon is the only thing replaced. The test starts a small stub on a loopback
port and points STELLAR_HORIZON_URL at it, so no network call leaves the
machine and the suite is deterministic. Alongside the happy path it pins the
rejections that protect a seller: a memo belonging to another invoice, a wrong
destination, a wrong amount, a wrong asset, and a second verification of an
invoice that is already paid.
If verify ever stops setting PAID, or starts accepting a payment it should
refuse, this test fails.
The default backend, frontend, and root-tests jobs that run on every PR
require no secrets. The workflow sets only:
| Variable | Job | Why |
|---|---|---|
STELLAR_NETWORK=TESTNET |
backend | Never resolve mainnet configuration |
STELLAR_HORIZON_URL=http://127.0.0.1:1 |
backend | Fail closed; the integration test overrides it with its own stub |
NEXT_PUBLIC_API_URL |
frontend | Build-time default for the client |
NEXT_PUBLIC_STELLAR_NETWORK=TESTNET |
frontend | Keep the client on testnet |
A plaintext Horizon URL is accepted only for a loopback address
(backend/src/config/stellar.ts), so a real deployment can never be downgraded
to HTTP by configuration.
Unlike the three jobs above, evidence-smoke runs a real create → pay →
verify pass against Stellar Testnet (backend/scripts/evidence-smoke.mjs,
also runnable locally as cd backend && npm run evidence:smoke). That needs
a funded Testnet keypair, which is exactly the kind of thing a public repo
must not require an external contributor's PR to have:
- The job's final step is gated by
env.EVIDENCE_PAYER_SECRETandenv.EVIDENCE_SELLER_PUBLIC_KEYbeing non-empty. GitHub never exposes repository secrets to a fork'spull_requestcontext at all, so on an external contributor's PR these are always empty and the step is skipped cleanly — it is not possible for a fork PR to fail this job for lacking credentials it was never meant to have. - Where the repository's own
EVIDENCE_SELLER_PUBLIC_KEYandEVIDENCE_PAYER_SECRETsecrets (andEVIDENCE_API_URLrepository variable) are configured — this repository's own main-branch pushes, or a PR from a branch within it — the step runs for real and its failure does block the job, the same as any other test.
For a manual testnet pass with a real Freighter payment instead of this
scripted one, see EVIDENCE.md.
Reviewer pack: EVIDENCE.md (URLs, testnet tx hashes, recording, tech note).
After deployment, cd backend && npm run evidence:smoke -- --write-evidence
runs the real Testnet create → pay → verify path and fills the reviewer tables;
the required secret and public variables are listed in EVIDENCE.md.
| Item | Status |
|---|---|
| Public demo URL | Fill in EVIDENCE.md after deploy (D4) |
| Testnet tx hashes | Fill in after a real Freighter pay (D5) |
| Screen recording | Fill in after demo recording (D5) |
Until then, run locally: backend → npm run dev:mvp, frontend → npm run dev.
backend/ Express API — use server-mvp.ts for demo
src/routes/ shared invoice route layer (both servers)
src/storage/ InvoiceStorage: in-memory + PostgreSQL adapters
src/services/payment-verification.ts — canonical verify rules
frontend/ Next.js app
lib/verification.js — client mirror of the verify contract
db/ Postgres schema + seed SQL (runners: backend/src/db/)
PLAN.md Product & delivery plan
ROADMAP.md Short commit checklist
EVIDENCE.md Public demo URL + testnet evidence (reviewer one-pager)
MIT — see LICENSE.