-
-
Every step is hash-chained, refusals included. Alter one entry and verification names the exact link that broke.
-
The reviewer gets a verdict and every rule that produced it, each naming itself and explaining itself in words.
-
The agent has six tools. Three capabilities are withheld, each with the reason - signing is absent from the catalogue, not disabled.
-
A clause-level diff against the approved template, word by word. The approver reads what moved, not the whole contract.
-
And then it stops. Send for signature is inert - not because it failed, but because signing was never in the agent's reach.
-
One sentence in. Foxit drafts, Nutrient scans for personal data, the policy engine scores it - with the real timing of each call.
Inspiration
The Foxit brief says it plainly: their MCP server hands an agent about forty tools for the reversible document work, and signing was left out of the catalogue on purpose. That is not a limitation to work around. It is the interesting question, and they invite you to argue with it.
So Handoff is not a document generator. It is a position on where an agent's authority should end, with running code behind it. An agent that can draft a contract is useful. An agent that can sign one can bind your company to terms nobody read.
What it does
You type one sentence: draft a freelance contract for Anna Weber, twelve weeks, EUR 6,400 per month, German law.
The agent drafts it through Foxit, runs it past Nutrient for personal data, and scores it against a policy engine. Then it stops. Not because it failed, and not because it ran out of steps - because sending for signature is not a step it has.
A reviewer sees the verdict, every rule that produced it with its own rationale, and a clause-level diff against the approved template, word by word. They approve or reject. Only after that may the backend call eSign, using a credential the agent was never constructed with. Every step lands in a hash-chained, append-only trail - refusals included.
Where we drew the line, and why
The rules score five axes: contract value, counterparty novelty, clause deviation from the approved template, jurisdiction, and irreversibility. On real drafts that produces a spread rather than a constant: a 184,000 EUR statement of work scores 88 and needs finance; a 6,400 EUR monthly engagement scores 74; an NDA with no money attached scores 25 and clears at requester level.
The strongest argument against us is that a well-scoped agent could safely sign a low-value renewal with a counterparty it has dealt with fifty times. We think that is probably right - and it is exactly why the policy is data rather than code. Edit policy-rules.json and the boundary moves. In the demo we move it by changing two words: the same contract under English law instead of German goes from 74 to 100, because a jurisdiction rule fires. Nothing is recompiled.
What we would not make configurable is the shape: the agent never holds the signing credential, and approval is never something the agent can grant itself.
How we built it
Node and Express, plain ES modules on the front, no bundler. Foxit PDF Services for the document work and Foxit eSign - deliberately in a separate module, behind a separate environment variable, called from exactly one place after an approval record has been verified. Nutrient DWS for the PII pass over the draft. Xano as the store. Doctavian on the structured-template path.
It runs with no API keys at all. Fixture mode serves recorded responses so a reviewer gets the complete product from a fresh clone: npm install && npm start.
Challenges we ran into
The five components were built in parallel and none of the seams matched. The router called a policy function that did not exist with that signature, so every staging attempt returned a 500. The document renderer and the template library disagreed on field names, so every contract rendered with its placeholders intact - and since the signature block is built from those fields, the envelope had no valid recipient and the signature step failed for a reason that had nothing to do with the approval boundary.
The one worth naming: for a long time every document scored 100 out of 100. That looks like a strict policy. It was a broken one. The engine was being handed a document row instead of the shape its rules read, so every fact resolved to null and the rules - correctly, by design - failed closed on all of them at once. An engine that cannot tell a 6,400 EUR contract from a 184,000 EUR one is decoration, however severe it looks.
Accomplishments that we're proud of
The boundary is checked, not claimed. assertBoundary() runs before the server can serve a request: it verifies the agent's tool list against Foxit's own published catalogue and refuses to boot if a signing capability is ever added. Add sendForSignature to make a demo easier and the process names the offending tool and stops.
The tamper demonstration writes nothing. Proving an append-only trail catches alteration usually means shipping a write path into the one collection whose value is that it has none. Instead the chain is recomputed over an in-memory alteration, the broken link is named, and the response says plainly that the stored trail is untouched.
What we learned
Read the response you actually get, not the one the documentation describes. And drive the product in a browser before believing it works: the Approve button - the single control this whole project exists to put in front of a human - returned HTTP 400 on every click, and nothing server-side showed it.
What's next
Counterparty history, so "first engagement" stops being permanently true. Unifying the two template libraries so the policy engine can reason about clause-level deviation directly instead of us withholding that evidence from it.
Created with AI assistance, curated and quality-checked by a human.
Built With
- css3
- doctavian
- esign
- express.js
- foxit
- html5
- javascript
- mcp
- node.js
- nutrient
- xano
Log in or sign up for Devpost to join the conversation.