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.
Three guarantees are enforced by the ledger, not by the UI:
- 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. - 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. - Atomic DvP. Assigning the receivable and paying the supplier the discounted cash settle in a single transaction.
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.
Daml Script tests in daml/Tests.daml, each querying the ledger as its own party:
testSelectiveDisclosure- the buyer queries forFinancingOffer/FinancingProposaland gets nothing; financier and supplier see the margin.testNoDoubleFinancing- two offers on one invoice; the firstAcceptFinancingconsumes 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-forgedConfirmedinvoice 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"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.
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:5173Toggle 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.
This is impossible on a public chain, where sealed bids would leak via logs before the reveal.
- Mock cash.
Cashis 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
FinancingOfferis 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 throughAcceptProposal, whose attestation gate it relies on, so it is never tricked into funding an invoice the buyer never signed.
- 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)
Apache-2.0. See LICENSE.

