Tallyard Request demo access

Vendor & purchase-order desk · for small manufacturers

The purchasing desk that keeps the tally.

Tallyard is where a plant's buyer raises purchase orders, the stores clerk posts goods receipts, and the vendor scorecard writes itself from what actually arrived. Built for an Indian packaged-foods plant: GSTIN on every vendor, HSN on every line, FSSAI on the checklist, rupees in words on the PO.

Leave your email once and we send one access PIN and one temporary login that open every OC Apps demo, this one included. The demo runs on synthetic data and reseeds itself; nothing you enter is kept.

Open commitment
₹27.47 L
Awaiting ack.
2
Overdue receipts
7
Spend, 90 days
₹1.41 Cr
PO-2026-0039Deccan SpicesAcknowledged93,775
PO-2026-0038Kirloskar ServiceIssued14,19,440
PO-2026-0035Deccan SpicesReceived4,19,500
PO-2026-0023Kaveri OilsPartially received6,47,400

The problem

A plant with a dozen suppliers runs purchasing on a spreadsheet, a WhatsApp group and one person's memory. Three things go wrong every month.

Nobody knows what is still owed

Purchase orders live in a folder; receipts live in the stores register. Open commitment is a number someone recomputes by hand before the bank call.

Vendor performance is a feeling

"They're usually late" is not a fact you can renegotiate on. On-time rate, fill rate and lead time exist in the delivery challans, uncounted.

Onboarding skips the documents

A supplier starts delivering before the GST certificate, bank proof or FSSAI licence is on file, and the audit finds out first.

The product

Six screens and one drawer. Everything is server-rendered, fast, and exportable as CSV.

Vendors

A searchable directory; GSTIN and PAN validated on entry; an onboarding checklist that gates activation; a profile with terms, open commitment, the five-metric scorecard and a timeline.

Purchase orders

A line-by-line composer with HSN codes and GST rates; totals recomputed on the server; sequential numbering; a lifecycle that only moves forward; a printable PO with both GSTINs and the amount in words.

Receiving

Goods receipts per line, partials allowed, over-receipts refused. The order's state is derived from its receipts, never typed in. A queue of what is overdue, due this week, and later.

Scorecards

On-time rate, fill rate, mean lead time, price stability and acknowledgement lag, computed at read time from receipts and pinned by a fixture test.

Dashboard & spend

Open commitment, orders awaiting acknowledgement, overdue receipts, top vendors, twelve weeks of spend, risk flags that link to the vendor they name, and breakdowns by category, vendor and month.

Accounts, roles & vendor confirmation

Admin, buyer and stores sign in with their own accounts; every change carries a name. Orders are emailed to the vendor with a one-click confirm link, and the vendor's click sets "Vendor confirmed" without anyone at the desk typing it.

Assistant

Paste a supplier's email to get a vendor draft. Type "order 500 kg maida from Balaji Flour Mills, deliver in 10 days" to get a priced draft PO. Ask for a one-paragraph vendor brief. Everything works without AI; when a model key is set it helps too, and the badge says which is in use.

Receipts are the truth.Order status, on-time rates and open commitment are computed from goods receipts at read time, never cached in a column that can drift.
The assistant works without AI.Every AI feature has a deterministic offline path and a badge naming the provider. No feature exists only when a key is present.
Everything exports.Orders and vendors as CSV; a token-gated JSON API for machines; a printable PO document for the vendor.

Architecture

One FastAPI process, one SQLite file, server-rendered pages with a little JavaScript for the composer and the assistant drawer. The same code serves the local service and the hosted twin.

Browserserver-rendered pages · drawer · composer PIN gate (hosted twin)signed cookie · betadoc at / JSON API v1feed token · vendors · orders FASTAPI APP — app/main.py domain.pyvalidators · lifecycletotals · wordsscorecard · flags copilot.pyAnthropic → OpenRouter→ heuristicschema-validated JSON templates + staticJinja2 · design tokensledger tables · chipsprint stylesheet seed.py14 vendors · 40 POsreceipts · eventsdeterministic SQLite — db.pyvendors · purchase_orders · po_lines · receipts · eventslocal: one file on disk · twin: /tmp, reseeds on cold start Model providers (optional)ANTHROPIC_API_KEY → official SDK, structured outputOPENROUTER_API_KEY → JSON mode · none → rules engine

Feature matrix

Every capability we set out to build, with its status and the test that proved it. This table is generated from the internal build tracker, so it cannot claim more than the tracker shows.

41 of 41 tracked capabilities built and verified · 0 partial · 0 on the roadmap. Generated from the build tracker; each row names the test that proved it.

AreaCapabilityStatusProof (acceptance test)
FoundationsSchema: vendors, purchase_orders, po_lines, receipts, eventsBuiltdb.init() on an empty file creates exactly those 5 tables plus schema_version; running it twice is a no-op (row counts unchanged)
FoundationsSynthetic seed: an Indian packaged-foods plant's supplier baseBuiltseeded DB holds >=12 vendors with valid-format GSTINs, >=30 POs covering every lifecycle status, >=60 lines with HSN codes, and receipts against issued POs; reseed on an empty DB is deterministic (same counts)
FoundationsApp shell: FastAPI boot, /healthz, base layout with design tokensBuiltcurl /healthz returns 200 JSON with ok=true, db path, vendor count and ai provider; GET / returns 200 HTML with the nav and no inline placeholder text
FoundationsTest harnessBuiltpytest -q runs >=1 test against a temp DB via TestClient and passes
VendorsVendor directory with search, category and status filtersBuiltGET /vendors?q=<seeded name fragment> lists only matching vendors; ?status=on_hold lists only on-hold; headless: table renders one row per seeded vendor with 0 console errors
VendorsVendor list filters combineBuiltGET /vendors?q=<frag>&category=<cat>&status=<st> returns only vendors matching all three; a filter that excludes everything renders the 'no vendors match' state
VendorsVendor create/edit with GSTIN + PAN + net-days validationBuiltPOST /vendors with a 14-char GSTIN returns 422 with the field message; a valid form creates the vendor, writes a 'vendor.created' event and redirects to its profile
VendorsVendor profile: terms, open commitment, scorecard, checklist, timelineBuiltGET /vendors/{id} for a seeded vendor shows open-commitment ₹ equal to the SQL sum of its unclosed POs, the 5 scorecard metrics, the onboarding checklist and the last 8 events
VendorsOnboarding checklist gates activation; status transitions guardedBuiltPOST /vendors/{id}/status active on a vendor missing a required document returns 409; after ticking the documents the same request returns 303 and the status is active; retired->active is refused
Purchase ordersPO register with status chips, totals and read-time overdue flagBuiltGET /orders lists every seeded PO; ?status=issued filters; a PO whose expected date is past and not received carries the overdue class; totals column equals the stored grand total
Purchase ordersPO composer with multi-line editor; server-side totals and sequential numberingBuiltPOST /orders with 3 lines creates PO-<year>-NNNN one higher than the last; subtotal, GST and grand total are recomputed server-side and match the line math to the paisa; a client-tampered total is ignored
Purchase ordersLifecycle state machine: draft→issued→acknowledged→(partial)→received→closed; cancel guardedBuilttests/test_lifecycle.py covers every legal edge and asserts 409 on issued->closed, received->cancelled and closed->anything
Purchase ordersPrintable PO document (buyer/vendor blocks, GSTIN, HSN lines, terms) with print stylesheetBuiltGET /orders/{id}/document returns 200 containing both parties' GSTINs, every line's HSN, the words-in-rupees total, and a @media print rule that hides the nav
Purchase ordersJSON API v1 with feed tokenBuiltGET /api/v1/orders without token returns 401 JSON; with ?token=<FEED_TOKEN> returns 200 with count equal to the PO table; /api/v1/vendors likewise
ReceivingGoods receipt per line; partial allowed; over-receipt refused; PO status derives from receiptsBuiltPOST /orders/{id}/receive with qty > outstanding returns 422; receiving part of one line flips the PO to partially_received; receiving the remainder flips it to received; each posts a receipt row
ReceivingReceiving queue: due this week, overdue, by vendorBuiltGET /receiving lists issued/acknowledged/partial POs grouped overdue / this week / later with counts that equal the SQL groups for today's date
ReceivingEvent timeline on every mutationBuiltcreating a PO, issuing it and receiving a line write >=3 events with actor, kind, subject and timestamp; /activity renders them newest first
Scorecards & dashboardVendor scorecard metrics computed at read time from receiptsBuilttests/test_scorecard.py builds a 4-PO fixture and asserts on-time rate, fill rate, mean lead days, price stability and acknowledgement lag to 2 decimals against hand-computed values
Scorecards & dashboardDashboard: open commitment, awaiting acknowledgement, overdue receipts, top vendors, 12-week spendBuilta test asserts each dashboard tile equals its SQL definition on the seeded DB; headless: 12 spend bars render with 0 console errors
Scorecards & dashboardSpend analytics by category / vendor / month + CSV exportsBuiltGET /export/orders.csv has PO-count + 1 rows and a GST column; /spend renders three breakdowns whose grand totals agree with each other
Scorecards & dashboardRisk flags: spend concentration, expiring documents, poor on-time vendorsBuiltthe seeded DB raises >=2 flags of >=2 kinds; each flag on the dashboard links to the vendor it names
CopilotProvider layer: Anthropic → OpenRouter → heuristic; never raises; badge in /healthzBuiltwith no keys set /healthz ai=heuristic; a test monkeypatches a failing provider and the copilot still returns a heuristic answer with provider='heuristic'
CopilotVendor intake: paste an email/brochure → structured vendor draft prefilled into the formBuilta sample supplier email yields legal name, GSTIN, PAN, category, contact and net-days via the heuristic extractor; LLM output is validated against the same schema and rejected on mismatch
CopilotPO from a sentence: 'order 500 kg maida from <vendor>, deliver in 10 days' → draft POBuiltthe heuristic parser resolves vendor, item, qty, unit and due date from that sentence, prices the line from the vendor's last price, and creates a draft PO; unknown vendor returns a clear 'no such vendor' message
CopilotVendor brief: one paragraph on the scorecard with a recommended actionBuiltthe brief for a seeded vendor mentions each of the 5 metric values shown on the profile; offline it is a template, online it is LLM text checked for those values
CopilotCopilot drawer UI with an honest provider badgeBuiltheadless: the drawer opens from any page, the badge text equals /healthz ai, and submitting the intake sample fills the vendor form fields with 0 console errors
ShipLocal service under pm2 on :8341 with an ecosystem fileBuiltpm2 list shows tallyard online; curl http://127.0.0.1:8341/healthz returns 200 after pm2 restart tallyard
ShipHosted twin on Vercel: ephemeral /tmp DB that reseeds, stateless PIN gateBuiltcurl https://<twin>/healthz returns 200 with ai provider; GET /app without the PIN cookie redirects to /demo; with the PIN the dashboard renders
ShipBetadoc site served at / of the twin: story, product, architecture, honest feature matrix, roadmapBuiltGET / on the twin returns the site; its feature matrix marks built / partial / roadmap exactly as the task DB reads at publish time; documentation page links resolve
ShipCustom subdomain tallyard.ajaykongala.onlineBuiltcurl -sI https://tallyard.ajaykongala.online/ returns 200 over TLS and /healthz matches the vercel.app twin
ShipHeadless walk of every page: 0 console errors, forms submitBuilttools/walk.py visits dashboard, vendors, a profile, orders, composer, a document, receiving, spend, activity and the copilot drawer on :8341 and reports 0 console errors and 0 failed requests
ShipRegistry, HOME.md, cockpit :6796 under pm2, memory updatedBuilttools/check_registry.py passes with 8341 and 6796 present; the cockpit's five tabs render with 0 console errors and the architecture boxes reflect task state
ShipPlain-language delivery statuses with legend; Copilot renamed Assistant with simpler copy (user feedback 2026-09-18)BuiltPO_LABEL uses delivery words (Not sent yet / Sent to vendor / Vendor confirmed / Partly delivered / Delivered / Completed / Cancelled); /orders renders the legend stating payment is separate; order page action buttons use ACTION_LABEL; drawer titled Assistant with badge 'Works without AI' or 'Using AI'; pytest 41 green and tools/walk.py PASS; twin redeployed with the same wording
Users & emailUsers table, PBKDF2 passwords, stateless signed sessionsBuilttests: create_user + authenticate succeed; wrong password and inactive user fail; a tampered session cookie reads as no session
Users & emailSign-up / sign-in / sign-out pages; first sign-up becomes admin; later sign-ups need an invite codeBuiltPOST /signup on an empty users table creates an admin and signs in (303 + cookie); a second POST /signup without a valid invite returns 403; POST /login wrong password 401; /logout clears the cookie; every app page redirects to /login when signed out
Users & emailRoles gate writes: admin all, buyer vendors+orders+sending, stores receiving only; events carry the usernameBuilta stores user POST /orders → 403 and POST /vendors → 403 but POST /orders/{id}/receive → 303; a buyer POST /users/invite → 403; the event written by a receipt has actor == that user's username
Users & emailAdmin users page: list, invite by email (Resend or on-screen link), change role, deactivateBuiltadmin GET /users lists users; POST /users/invite creates an invite and (screen mode) shows the join link; a deactivated user's POST /login returns 401; role change is reflected on the next sign-in
Users & emailMail service: Resend when RESEND_API_KEY is set, on-screen preview otherwise; /healthz reports email modeBuiltwith no key /healthz email=screen and mail.send returns mode=screen with a preview; a test monkeypatching urlopen sees the Resend request body carry from/to/subject; tallyard.ajaykongala.online verified as a Resend sending domain
Users & emailEmail the PO to the vendor with a signed confirm link; the vendor's click sets 'Vendor confirmed'; resend buttonBuiltPOST /orders/{id}/send on a sent order emails (or previews) the PO with /confirm/<token>; GET /confirm/<token> flips issued→acknowledged without a login and writes an event 'confirmed by vendor via email link'; a second click says already confirmed; a bad token 404s; the twin passes /confirm/ through the PIN gate
Users & emailShip: seeded demo users, tests + walk green, twin redeployed with RESEND env, docs updatedBuiltseed creates admin/buyer/stores demo users shown on the gate and in the docs; pytest and tools/walk.py pass with sign-in; twin /healthz shows email=resend; a real PO email lands via Resend from hello@tallyard.ajaykongala.online
Users & emailEmail-first demo access: PIN hidden and rotated, request form issues PIN + temporary login by email, stateless guest credentialsBuilttwin /demo shows no PIN and links to /demo/request; POST /demo/request with a valid email answers 'Check your inbox' and a Resend email with PIN + guest login lands; the guest login signs in on any instance (stateless HMAC credentials, 2-day expiry); login page on the twin lists no seeded credentials; docs no longer publish credentials

Roadmap

What comes after the demo, in the order a plant would ask for it.

Deployment

Two lanes, one codebase, no code changes between them. The twin is flipped with environment variables alone.

On-premises

Runs on a plant PC or a small Linux box under a process manager. One SQLite file is the whole database; back it up by copying it. Model keys are optional and stay on that machine.

uv venv .venv && uv pip install -r requirements.txt
pm2 start ecosystem.config.js   # :8341
# optional: ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env

Cloud twin (this site)

The same app on a serverless host behind a PIN gate, with an ephemeral database that reseeds itself on every cold start. Nothing to install, up around the clock, and safe to hand to a prospect because nothing persists.

TALLYARD_DB=/tmp/tallyard.db  TALLYARD_SEED=1
BETADOC_PIN=…  FEED_TOKEN=…
vercel --prod   # betadoc at /, app behind /demo

Open the desk.

Synthetic data, real workflows. Raise a PO, receive it short, and watch the scorecard move.