Skip to content

UnityNodes/ledgerfactor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

93 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LedgerFactor

Confidential invoice financing on Canton. A supplier discounts a buyer-approved invoice to a financier, the financier's margin is hidden from the buyer by the protocol, and the ledger structurally guarantees one invoice can never be financed twice. An AI agent underwrites the receivable over only the data the financier is entitled to see.

Built for HackCanton S2, track Real-World Asset (RWA) & Business Workflows: the invoice is the asset, and the demo runs its full lifecycle (issuance, state changes, transfer, fulfillment, audit) as a workflow an organization could actually run. Business brief: docs/business-brief.md.

Live: https://ledgerfactor.unitynodes.com - an interactive guided demo: create an invoice and step it through issue → confirm → list → AI-underwrite → offer → settle, watching the four party views update live. Persistent systemd services behind Caddy + Cloudflare; see docs/deploy.md.

The offer step - the 1.97% margin appears only on the Supplier and Financier views; Buyer and Auditor get a redaction bar

Why this runs only on Canton

Three guarantees are enforced by the ledger, not by the UI:

  1. Selective disclosure (margin privacy). The discount rate lives on FinancingProposal / FinancingOffer, whose only stakeholders are the financier and supplier. The buyer is never a stakeholder, so the margin never reaches the buyer's party view.
  2. Single-financing uniqueness (anti-double-pledge). Confirming an invoice makes the buyer co-sign a BuyerAttestation, keyed on (supplier, invoiceNumber) and carrying the buyer and the amount. The attestation is unforgeable (the buyer signs it), and the financing path (AcceptProposal) checks that a matching attestation exists and that its buyer and amount equal the invoice's, so a supplier cannot get an honest financier to fund an invoice the buyer never signed, nor inflate one the buyer did. The settled receivable is keyed (supplier, invoiceNumber) too, so one receivable can never be financed twice, on any path.
  3. Atomic DvP. Assigning the receivable and paying the supplier the discounted cash settle in a single transaction.

Architecture

 web/ (React + Vite)  ──/api──▶  server/ (Node + Express)  ──JSON API──▶  Canton sandbox
 four role screens               party-scoped views                        (Daml contracts)
                                 + AI credit-scoring agent
  • daml/ - the model and the Daml Script proofs. This is the core.
  • server/ - Node gateway. Holds per-party JWTs, exposes party-scoped /api/view/:role, and runs the credit-scoring agent (server/src/scoring/).
  • web/ - the "same invoice, four screens" dashboard. Each column is a live query to the ledger as that party; the margin card appears only where the ledger discloses it.

The two money-shots (proven, not asserted)

Daml Script tests in daml/Tests.daml, each querying the ledger as its own party:

  • testSelectiveDisclosure - the buyer queries for FinancingOffer / FinancingProposal and gets nothing; financier and supplier see the margin.
  • testNoDoubleFinancing - two offers on one invoice; the first AcceptFinancing consumes it, the second is rejected by the ledger.
  • testNoDoublePledgeAcrossContracts - the same invoice number re-issued as a second contract; the buyer cannot attest it again, so it can never be financed twice.
  • testForgedConfirmationCannotFinance - a supplier-forged Confirmed invoice with no buyer attestation is rejected; financing needs the buyer's signed acknowledgement.
  • testForgedAmountCannotFinance - the buyer signs a small invoice, the supplier reuses its number on an inflated one; financing is rejected on the amount mismatch.
  • testForgedBuyerCannotFinance - the supplier attests with a throwaway party, then names the real buyer on the invoice; financing is rejected on the buyer mismatch.
  • testHappyPathSettlement - atomic DvP: supplier paid 97,000 on a 100,000 invoice at 3%, original receivable gone, auditor sees face value only.
daml/Tests.daml:setupParties: ok, 0 active contracts, 0 transactions.
daml/Tests.daml:testSelectiveDisclosure: ok, 3 active contracts, 4 transactions.
daml/Tests.daml:testNoDoubleFinancing: ok, 6 active contracts, 11 transactions.
daml/Tests.daml:testNoDoublePledgeAcrossContracts: ok, 5 active contracts, 9 transactions.
daml/Tests.daml:testForgedConfirmationCannotFinance: ok, 2 active contracts, 3 transactions.
daml/Tests.daml:testForgedAmountCannotFinance: ok, 4 active contracts, 5 transactions.
daml/Tests.daml:testForgedBuyerCannotFinance: ok, 4 active contracts, 5 transactions.
daml/Tests.daml:testFaceAmountMustMatchInvoice: ok, 6 active contracts, 11 transactions.
daml/Tests.daml:testHappyPathSettlement: ok, 4 active contracts, 7 transactions.
daml/Tests.daml:testSealedBidAuction: ok, 6 active contracts, 10 transactions.

The same disclosure is verifiable live: seed one demo session, then query the running gateway as each party. Only supplier + financier see the discount rate.

S=demo-$RANDOM
curl -s -X POST localhost:8080/api/actions/sample -H "x-lf-session: $S" >/dev/null
for r in supplier buyer financier auditor; do
  rate=$(curl -s "localhost:8080/api/view/$r" -H "x-lf-session: $S" | grep -o '"discountRate":"[0-9.]*"' | head -1)
  echo "$r: ${rate:-no discount rate disclosed}"
done
# supplier + financier print the rate; buyer + auditor print "no discount rate disclosed"

The AI credit-scoring agent

server/src/scoring/ is a deterministic underwriting engine - not a chatbot. It scores buyer payment reliability, portfolio concentration, dilution risk, and invoice size into a 0-100 credit score and risk band, and recommends a discount rate. The financier reviews and signs the resulting on-ledger offer. An LLM turns the structured rationale into a short memo - via Groq (GROQ_API_KEY) or Anthropic (ANTHROPIC_API_KEY), using only the numbers the engine produced; with no key it falls back to a deterministic memo, so the demo never depends on a network call. 10/10 unit tests: npm --prefix server test.

Run it

Prerequisites: Daml SDK 2.10.4 and a JDK (17 works), Node 20+.

# 1. prove the core (no services needed)
daml test

# 2. run the whole stack (sandbox + JSON API + server + web)
./scripts/dev.sh
# → UI on http://localhost:5173

Veild - sealed-bid auction (second mode)

Toggle Sealed auction · Veild on the live site for a multi-financier blind auction. A supplier auctions one invoice to three financiers who each submit a blind bid; the killer property - enforced by Canton's party-scoped disclosure (each bid is a separate contract shared only with the supplier) - is that no financier can see another's bid, only the supplier (auctioneer) sees them all. The View as… control re-queries the ledger as each party: as a financier only your own envelope opens (rivals stay wax-sealed); as the supplier every envelope opens; the buyer/auditor see no pricing. On close the supplier accepts a bid and it settles atomically. What testSealedBidAuction (daml/Tests.daml) proves on-ledger is the sealed-bid privacy and atomic settlement; lowest-wins is the server's selection convention, not a ledger-enforced rule. Served from server/src/server.ts (/api/auction/*) + web/src/AuctionBoard.tsx.

Veild - as a financier, only your own bid is visible; rivals stay wax-sealed

This is impossible on a public chain, where sealed bids would leak via logs before the reveal.

Honest scope

  • Mock cash. Cash is an owner-signed bearer token standing in for a token-standard holding. The DvP atomicity is real; the cash model is not production-grade. Real token-standard DvP is a stretch goal.
  • Notified factoring. Listing an invoice reveals the financier's identity to the buyer; the sensitive figure - the pricing - stays hidden. Undisclosed factoring (hiding the financier too) would use Canton explicit disclosure.
  • Invoice identity is the number. Uniqueness is enforced per invoiceNumber, so the buyer must attest each number, and each is financed once. Guaranteeing that two different numbers do not point at the same real-world receivable is a business-layer control (dedup on an external receivable id), not something the ledger can know.
  • Anti-forgery assumes an honest financier. A FinancingOffer is co-signed by the financier and supplier, so a financier who colludes with the supplier could mint one directly and skip the attestation check. That harms only the colluding financier: the buyer merely observes the resulting receivable (it never signs it) and can repudiate one it never attested, and no receivable is financeable twice regardless. An honest financier only ever funds offers minted through AcceptProposal, whose attestation gate it relies on, so it is never tricked into funding an invoice the buyer never signed.

Status

  • Daml model + both money-shots proven by Daml Script
  • AI credit-scoring agent (rules engine + optional LLM memo), unit-tested
  • Live JSON API gateway + party-scoped role views
  • Four-role frontend (supplier / buyer / financier / auditor)
  • Business brief + pilot plan
  • Persistent deployment (systemd + Caddy) at ledgerfactor.unitynodes.com
  • Veild - sealed-bid auction mode (multi-financier blind auction)
  • Real token-standard DvP settlement (stretch)
  • Runtime compliance citation via CCPEDIA MCP in the auditor view (stretch)

License

Apache-2.0. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors