Inspiration
Every team shipping agents is having the same argument, and it is never about whether the model can write the contract. It can. The argument is what happens when it decides to send it. The usual answer is a line in the system prompt telling the model not to sign anything, and that is not a boundary. It is a request. Anything you can phrase, a model can phrase its way around.
What it does
Recuse gives an agent a real job: draft a services agreement and get it executed. It generates a real document, reads the terms back out of the finished file, checks a claim the contract makes about the world against live sources, and then reaches for the signature and is refused. It routes the document to a person instead. A human signs, and only a verified webhook can mark the record authorized.
How we built it
Four stages, each on a real API.
Foxit PDF Services converts the agreement to a PDF and Foxit eSign carries the signature, with fields placed by coordinate and a webhook whose HMAC-SHA-256 is verified against the raw request body before anything is parsed.
Nutrient Data Extraction reads the generated document back and the terms are parsed by rule, not by asking a model, because the point of the stage is that the model does not get to vouch for its own output.
SerpApi checks a factual claim the document makes. Every distinctive term must appear in
independent sources or the verdict is unverified, and the verdict names the term that went
unsupported.
The agent runs on Gemini through the Interactions API with a model fallback chain. Next.js, TypeScript, Tailwind, Neon Postgres via Drizzle, deployed on Vercel.
Challenges we ran into
Text tags never parsed. processTextTags: true was accepted and silently ignored, so two
envelopes went out with ${signfield:1:y:____} printed on a document a human was being asked
to sign. Fields are now placed by coordinate, and the origin is top-left, which cost a round
trip to discover.
403 error code: 1010 turned out to be a Cloudflare browser-signature ban, not a Foxit
permissions error. It was diagnosed as a permissions problem twice before a User-Agent header
fixed it.
The model fallback chain restarted runs after a quota error, which duplicated every side effect already performed while keeping only the second transcript. A record that omits a pass which sent a real envelope is not a degraded record, it is a false one, so fallback now refuses to restart once any tool has run.
Accomplishments that we're proud of
The boundary is structural rather than instructed, and the difference is demonstrable. An
earlier version told the model in its system prompt that it had no authority to sign, and the
model obediently never tried, which proved the prompt worked and said nothing about the
boundary. That sentence is gone. /boundary calls the real handler with six hostile argument
sets when you load it and prints what comes back.
What we learned
Writing the refusal into the prompt is the thing that feels like safety and is not. The uncomfortable version is to hand the agent the capability and let the refusal be observable, because a boundary nobody tests is indistinguishable from a boundary that does not exist.
What's next for Recuse
Attenuated authority rather than a binary: an agent that may sign under a value threshold and must escalate above it. Contradiction detection in corroboration, which currently reports only presence or absence. Rate limiting on the run trigger, and authentication so records are not world-readable.
Built With
- drizzle
- foxit-esign
- foxit-pdf-services
- gemini
- neon
- nextjs
- nutrient-dws
- react
- serpapi
- shadcn-ui
- tailwindcss
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.