Primitive spec
Devers
The procurement and accounts-payable primitive. Devers is the buy side of the platform: vendors, purchase orders, receiving, vendor bills, three-way match, and AP aging — the mirror image of Hardin/Hober selling and Mallow recording AR. Devers buys; Mallow records AP.
What it owns
Devers owns the purchasing loop. A Vendor is a supplier with payment terms; a PurchaseOrder is what you asked them for; a GoodsReceipt is what actually arrived at the dock; a VendorBill is what they charged you; a BillPayment is how the bill gets settled. Three-way match reconciles ordered vs received vs billed at approval time, and every approval and payment posts double-entry legs into the Mallow ledger.
Devers does not own stock levels (that is Trantor — receiving calls Trantor replenishment in the same transaction), the ledger itself (that is Mallow — Devers posts journals with sourceType: devers_bill | devers_payment), or money movement rails (a driver seam, mirroring the Payments processor registry). It composes them into one buy-side flow.
Concepts
- Vendor & VendorItem
- A supplier (
vend_) with contact details,termsNetDays(default 30), a default expense account code, and an optional tokenized rail counterparty. AVendorItem(vitm_) is that vendor's catalog entry for something you stock:vendorSku,lastCostCents,minOrderQty,packSize(order in cases of 12, receive and count in units), and apreferredflag — one preferred vendor per resource type. - PurchaseOrder
- The procurement document (
dpo_): per-tenant human-readablepoNumber(PO-000123), statusdraft → submitted → partially_received → received → closed(orcancelled). EachPurchaseOrderLineis either a stock line (a Trantor resource type) or a non-stock line (an expense account code — services, freight), carryingqtyOrdered,unitCostCents, and receipt/bill counters maintained by the receiving and billing flows. - GoodsReceipt (receiving)
- What arrived (
dgr_), line by line with aconditionofok | damaged | rejected. Posting a receipt is one transaction: PO line counters bump, PO status transitions, and eachokstock line calls Trantor replenishment — stock quantity increments,lastReplenishedAtstamps, the low-stock latch clears. Damaged and rejected lines never replenish; over-receipt beyondqtyOrderedwarns and requires a reason. Stock truth commits or rolls back together — no soft-fail at the loading dock. - VendorBill & three-way match
- The vendor's invoice (
dbil_), PO-linked or standalone — rent and utilities are first-class non-PO bills. At approval, PO-linked lines runThreeWayMatchService: ordered qty/cost vs received qty vs billed qty/cost, withinApPolicytolerances ⇒matched; outside ⇒variance, which demands an approval reason recorded in the AuditEvent. Duplicate bills are rejected outright — resubmitting a vendor's bill number returns the existing bill instead of paying it twice. Bills are USD-only in v1. - AP ledger postings
- Expense-on-purchase (periodic inventory) in v1: bill approval posts
DRexpense per line /CR 2100 Accounts PayableviaDeversPostingService, with stock lines defaulting to5000 Cost of Goods Sold. Rail disbursement walks a dedicated in-transit liability: submitDR 2100 / CR 2110 AP Payments In Transit, paidDR 2110 / CR 1000 Cash, returns reverse. Reversals are idempotent — a replay can never double-post. - Payment rail seam
- A
BillPayment(dpay_) allocates across bills (partial payments and one-payment-many-bills both work) and executes throughApPaymentRailDriverInterface: counterparty tokenization (raw bank details discarded after tokenization — encrypted-at-rest fallback, masked reads), payment initiation, cancellation, and webhook handling. Webhook ingest is per-event transactional and deduplicated; unmatched or closed-fiscal-period events park visibly and can be replayed once resolved. TheMockRailDriverruns the full lifecycle today, including R01 returns. - Reorder automation
- A scheduled sweep (
ReorderSweepService, a Daneel step) finds Trantor inventory at or below its reorder level, groups by preferredVendorItemvendor, and builds draft POs respectingminOrderQtyandpackSize, notifying the purchasing role via Speaker. Draft-only by design — humans submit POs. - ApPolicy & AP aging
- The audited per-tenant policy row (
dapl_): match tolerances (qtyTolerance,costTolerancePermille) andrequireSecondApproverAboveCentsfor a second approver on large payments.ApAgingReportServicebuckets unpaid approved bills current/30/60/90+, mirroring the AR aging shape — console tile plus Pelorat stream.
API surface
Endpoints are versioned under /devers/v1/. Operator surfaces are permission-gated; the money-out transitions (bill approve/void, payment execute/cancel) require owner-level finance.approve. The rail webhook route is machine-facing and signature-verified per driver.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /devers/v1/vendors | Create or list Vendors. |
| POST | /devers/v1/vendors/{id}/counterparty | Tokenize vendor bank details into a rail counterparty ref. |
| POST / GET | /devers/v1/vendor-items | Vendor catalog entries — SKU, pack size, min order qty, preferred flag. |
| POST / GET | /devers/v1/purchase-orders | Draft or list PurchaseOrders. |
| POST | /devers/v1/purchase-orders/{id}/submit | Submit a draft PO (also /cancel). |
| POST | /devers/v1/receipts | Post a GoodsReceipt — PO counters + Trantor replenishment in one transaction. |
| POST / GET | /devers/v1/bills | Enter or list VendorBills, PO-linked or standalone. |
| POST | /devers/v1/bills/{id}/approve | Run three-way match and post the AP journal (also /void). |
| POST | /devers/v1/payments | Build a payment run: bill ids → allocations; then /{id}/execute or /{id}/cancel. |
| GET | /devers/v1/reports/ap-aging | AP aging buckets: current / 30 / 60 / 90+. |
| GET | /devers/v1/reorder-suggestions | Draft-PO suggestions from low stock, grouped by preferred vendor. |
| GET / PUT | /devers/v1/ap-policy | Match tolerances and second-approver threshold. |
| POST | /devers/v1/webhooks/{railKey} | Signature-verified rail webhook ingest, deduped per event. |
Example: PO → receive → bill → match
A salon restocks retail shampoo: draft and submit a PO, post the receipt when the cases land (stock updates in the same transaction), enter the vendor's bill, and approve — three-way match runs and the AP journal posts.
// 1. Draft the PO, then POST /devers/v1/purchase-orders/{id}/submit
POST /devers/v1/purchase-orders
{
"vendorId": "vend_01JZM4…",
"expectedAt": "2026-07-21",
"lines": [
{ "trantorResourceTypeId": "rt_01JX2K…",
"vendorItemId": "vitm_01JZM7…", // packSize 12 — ordered as 4 cases
"description": "Argan repair shampoo, 300ml",
"qtyOrdered": 48, "unitCostCents": 950 }
]
}
→ 201 { "id": "dpo_01JZN1…", "poNumber": "PO-000123",
"status": "draft", "totalCents": 45600 }
// 2. Cases arrive — one transaction: PO counters + Trantor replenishment
POST /devers/v1/receipts
{
"purchaseOrderId": "dpo_01JZN1…",
"lines": [ { "poLineId": "dpol_01JZN1…", "qtyReceived": 48, "condition": "ok" } ]
}
→ 201 { "id": "dgr_01JZP8…" } // PO status: received;
// stockQuantity +48, low-stock latch cleared
// 3. Enter the vendor's bill against the PO
POST /devers/v1/bills
{
"vendorId": "vend_01JZM4…",
"purchaseOrderId": "dpo_01JZN1…",
"billNumber": "INV-8841", // UNIQUE per vendor — duplicates 409
"billDate": "2026-07-21",
"lines": [ { "poLineId": "dpol_01JZN1…", "qty": 48, "unitCostCents": 950 } ]
}
→ 201 { "id": "dbil_01JZQ3…", "status": "draft", "totalCents": 45600 }
// 4. Approve — three-way match (ordered vs received vs billed), then post
POST /devers/v1/bills/dbil_01JZQ3…/approve // requires finance.approve
→ 200 { "id": "dbil_01JZQ3…", "status": "approved",
"matchStatus": "matched", // within ApPolicy tolerances
"dueAt": "2026-08-20", // termsNetDays: 30
"mallowJournalId": "jrnl_01JZQ4…" }
// Journal: DR 5000 Cost of Goods Sold 45600 / CR 2100 Accounts Payable 45600
How it fits with the rest
flowchart TB
Con[Demerzel purchasing console] --> Dv(Devers)
Dv -- receiving replenishes --> Tr[Trantor]
Tr -. low-stock signal .-> Dv
Dv -- AP journal legs --> Ml[Mallow ledger]
Dv -- payment rail seam --> Rail[ACH rail driver]
Rail -. signed webhooks .-> Dv
Trantor is on both ends of the loop: its low-stock signal feeds the reorder sweep, and posting a receipt replenishes its inventory in the same transaction. Mallow records every approval, payment, and reversal as double-entry journals that flow onward through the daily accounting summary. The rail driver seam mirrors the Payments processor registry — when a rail partner lands, the real driver slots in behind the same interface, webhooks, and replay machinery the Mock driver exercises today. The purchasing console lives in Demerzel at /dashboard/purchasing, and the money-out transitions — bill approve and void, payment execute and cancel — are deny-by-default behind owner-level finance.approve.