DEV Community

Chris
Chris

Posted on • Originally published at paragraph.com

How We Replaced Hardcoded Legal Templates with Accord TemplateMark: Composable Prose for Smart Contracts

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:

  1. Concerto — a modeling language for typed data structures
  2. 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
Enter fullscreen mode Exit fullscreen mode

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" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: ...' }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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) => { /* ... */ },
});
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)