TL;DR: We migrated 9 agreement types from hardcoded legal templates to Accord Project's TemplateMark system — typed data models, reusable clause packages, and a generic evaluation pipeline. 21 commits across 4 days. Here's the full architecture.
The Problem
Our agreement platform had hardcoded HTML templates for each agreement type. Adding a new type meant copy-pasting a template, manually wiring up field interpolation, and hoping the field names matched the smart contract interface. At 9 types this was already painful. At 100 it would be impossible.
Worse, the legal prose had no typed data model. Field names were strings, validation was ad-hoc, and there was no systematic way to verify that the prose template matched the smart contract's ABI. A typo in a field name wouldn't surface until someone tried to create an agreement on-chain.
We needed composable prose that mirrors our composable on-chain architecture — both layers built from atomic, reusable primitives.
The Approach: Accord Project
Accord Project provides two key technologies:
- Concerto — a modeling language for typed data structures
- TemplateMark — a templating language that binds Concerto models to legal prose
Each clause becomes a self-contained package with four files:
clauses/{slug}/
package.json # Faceted metadata (function, domain, on-chain mapping)
model/model.cto # Concerto model (typed fields with decorators)
text/grammar.tem.md # TemplateMark prose template
test/sample.json # Sample data for testing
Clauses compose into contracts via manifests:
{
"id": "software-milestone",
"name": "Software Contractor Milestone Agreement",
"model": "org.papre.contracts.milestone@1.0.0.SoftwareMilestoneContract",
"sections": [
{ "heading": "1. Parties", "clause": "parties", "as": "parties" },
{ "heading": "2. Project Overview", "clause": "project-scope", "as": "projectScope" },
{ "heading": "3. Milestones", "clause": "milestone-payment", "as": "milestonePayment" },
{ "heading": "4. Deliverables", "clause": "deliverables", "as": "deliverables" },
{ "heading": "5. IP Assignment", "clause": "ip-assignment", "as": "ipAssignment" }
]
}
An assembler reads the manifest, fetches clause templates from the library, and stitches them into a single TemplateMark document wrapped in {{#clause}} blocks.
The On-Chain Bridge
This is where it gets interesting. Concerto models can declare their Solidity parameter mappings directly using custom decorators:
concept MilestonePaymentClause {
@OnChain("_descriptions,_amounts,_deadlines", "bytes32[],uint256[],uint256[]")
@Conversion("milestones")
o MilestoneItem[] milestones
@OnChain("_paymentToken", "address")
@Conversion("tokenAddress")
o PaymentCurrency currency default="ETH"
}
The @OnChain decorator maps a prose-layer field to its Solidity counterpart — name and type. The @Conversion decorator specifies the transformation function. The assembler can extract these to produce an ABI parameter summary:
@OnChain ABI parameters:
_client : address (from client)
_contractor : address (from contractor)
_descriptions : bytes32[] (from milestones)
_amounts : uint256[] (from milestones)
_deadlines : uint256[] (from milestones)
_paymentToken : address (from currency)
This connects the prose layer to the smart contract interface at the model level. When someone fills out the legal template, the system knows exactly how to encode the data for the on-chain call. A mismatch between the prose and the contract is caught at build time, not at transaction time.
The Evaluation Pipeline
The runtime pipeline flows through Redis KV and a VM sandbox:
papre-accord repo → seed-accord.mjs → Redis KV
→ /api/accord/template/[id] → clause stitching → rendered prose
→ /api/accord/evaluate → VM sandbox → ALLOW/DENY/NEEDS_INPUT
→ intent-executor → writeContract()
Seeding: seed-accord.mjs reads clause packages from the papre-accord repo and stores them in Redis KV:
accord:clause:{slug} → clause grammar, model, facets
accord:metadata:{id} → template metadata, field manifests
accord:evaluator:{id} → compiled TypeScript evaluator logic
accord:index → list of all available templates
Template resolution: The /api/accord/template/[contractId] route fetches clause grammars from KV and stitches them into a single TemplateMark document at request time.
Evaluation: The client sends form data to /api/accord/evaluate, which runs Accord's TypeScript logic in a Node.js VM sandbox. The evaluator returns a GenericActionDecision:
// Three possible outcomes:
{ action: 'ALLOW', message: 'All requirements met' }
{ action: 'DENY', message: 'Missing required field: ...' }
{ action: 'NEEDS_INPUT', message: 'Please provide: ...' }
On ALLOW, the intent-executor encodes the parameters per the @OnChain decorator mappings and calls writeContract().
Accept Strategy Pattern
Each agreement type registers a self-describing strategy that handles the on-chain creation:
interface AcceptStrategy {
templateId: string;
singletonAddress: string;
abi: any[];
usesEncryption?: boolean;
usesMultiSignerAcceptance?: boolean;
createOnChain(params): Promise<`0x${string}`>;
buildPartyMapping(data): PartyMapping;
}
Strategies self-register at module scope:
// In acceptStrategies.ts
registerAcceptStrategy('software-milestone', {
templateId: 'software-milestone',
singletonAddress: MILESTONE_AGREEMENT_ADDRESS,
abi: MILESTONE_AGREEMENT_ABI,
usesEncryption: true,
createOnChain: async (params) => { /* ... */ },
buildPartyMapping: (data) => { /* ... */ },
});
9 strategies across 3 categories:
- 5 traditional — dedicated Solidity contracts (freelance, milestone, retainer, safety-net, NDA)
-
3 Accord — generic factory via
createAccordStrategy()(equipment-rental, papreboat-rental, liability-waiver) - 1 simple-document — generic on-chain anchor for non-contract templates
Adding a new agreement type is now: write a manifest, register a strategy, seed to KV.
Clause Library Design
The clause library uses faceted classification instead of folder hierarchy. Each clause carries independent metadata facets:
{
"facets": {
"function": ["payment", "escrow"],
"domain": ["software", "freelance", "construction"],
"onChain": ["MilestoneClause", "EscrowClause"],
"lifecycle": ["execution"],
"risk": ["financial"],
"variables": true
}
}
A single hierarchy breaks when a clause belongs in multiple categories (an "Escrow with Dispute Resolution" clause would need to go in financial/ or governance/, but not both). Facets solve this — you can search from any direction: "all financial-risk clauses," "all clauses that map to EscrowClause," "all static prose for universal use."
Inspired by DigiKey's parametric component catalog, Ranganathan's Colon Classification, and legal clause banks like SALI and Practical Law.
Results
- 9 agreement types running through one generic pipeline
- 0 hardcoded templates remaining
- 12 reusable clause packages in the library
- Adding a new agreement type = manifest + strategy + seed
What Else Shipped This Week
- API v1 with public signing — external users can sign agreements without a Papre account
- Terms of Service migrated to papre-accord — no more special-case rendering
- Ctrl+Shift+D test data for 3 new agreement types (equipment-rental, papreboat-rental, liability-waiver)
- Lit Protocol encryption gracefully degrades instead of crashing
- Embedded wallet fallback when smart wallet unavailable on Fuji
- 104 ESLint warnings eliminated, deduplicated hooks across the codebase
- 16 architecture documents written (7 new, 6 migrated to vault, 3 updated)
- CHANGELOG catch-up with 12 missing entries
Builder's Note
Blockchain development, running group therapy, managing staff and clients, and personal meditation and yoga practice create an ever-changing, ever-shifting flow. All I can do is relax into it and let it pass through me. If I clench, the current catches me and throws me around. If I stay present but relaxed, the flow leaves nothing behind except the invigoration of energy in motion and the bliss of exploration.
Stress and anxiety in me are reactions, not to external circumstances, but to my own clenching against those circumstances. In private work with others, I often hear a familiar fear: if they meditate too much, they'll lose their edge. They'll lose their drive to succeed.
Maybe. And if you do, it's only because you've shifted out of the energy flow your biology, your family, your society, your experience have imposed upon you. By relaxing, you stop doing their bidding. You enter a more natural flow, where life is, frankly, easier. But releasing the NEED to achieve isn't universal in those who meditate, or in those who go deep within themselves to discover.
Others—like me—dig deep and find that the natural course is to attempt to make big changes in reality, to bring lots of energy to bear on the material world.
That's the thing about deep internal work: you don't get to choose where it heads ahead of time. I should say, your ego doesn't get to choose ahead of time. In my life, I've come to feel like there's a kind of higher-self calling the shots. I've come to experience this higher-order autonomy, not as an idea, but as something alive in me.
When I let go of the need to control, what I thought I wanted and needed usually falls away, and something far better manifests itself. This has happened over and over and over. My wife is 10x the woman I was seeking. My project is 1000x as incredible as I envisioned. My life is indescribably better than anything I imagined, even at my most optimistic. And it sure as hell is an improvement over the version of me that would have been satisfied with not dying totally broken and alone.
Sometimes it feels like I've died, and that God—if there is one—is so merciful that it shifted me to heaven. And in order to keep me from freaking out, that heaven looks a lot like the world I left at death.
Top comments (0)