Building a portfolio agent that can't make things up A developer built a portfolio chat agent that refuses to answer unless every sentence cites a fact from sources they control, using Mastra for the workflow, Neo4j for a knowledge graph, an OpenAI-compatible model, and Railway for hosting. The agent classifies each question into a topic mapped to fixed, pre-written graph read contracts rather than letting the model write Cypher queries, and a plain-code citation validator drops any claim it cannot verify, falling back to "not enough verified information". Disagreeing sources become candidate claims resolved by a declared authority tier, so nothing is silently overwritten. My portfolio has a chat box. Ask it "What has Owen built with Angular?" and it answers in a few sentences, with links to where each fact came from. Ask it something it can't back up, and it says so. That second part is the whole design. A portfolio agent that invents an employer or a project is worse than no agent at all, because the person reading it is deciding whether to trust me. So the rule is simple: every sentence must cite a fact from sources I control, or the agent doesn't say it. This post walks through how that's built, so you can build one like it. The stack is Mastra for the workflow, Neo4j for the knowledge graph, any OpenAI-compatible model, and Railway to host it. plain text question → scope check is this about my public work at all? → graph plan pick fixed, pre-written graph reads → optional web search trusted domains only, never overrides the graph → model writes claims each claim lists the evidence IDs it relies on → citation validator plain code: drops any claim it can't verify → answer + citations, or "not enough verified information" The model sits in the middle, boxed in on both sides. It never chooses what to read, and it never has the last word on what gets said. Here is the whole path as a diagram, with a lane for each owner and a fail-closed exit for every reason an answer can be refused: Explore the interactive diagram of the agent's answer path https://owenadirah.com/blog/agent-answer-path.html Every exit ends in the same fail-closed reply, sent back through the same stream as a real answer. Infrastructure failures, such as the graph being down, also email me, at most once per reason per hour, and never with the visitor's question in them. 1. Build a knowledge graph from sources you already have I didn't write a separate knowledge base. The facts already existed in a few places: - the portfolio site's data files portfolio.ts , resume.tsx - short prepared answers for the site's chat companion.ts - my CV, which is a Google Doc - my older personal site, kept only to reconcile against An ingestion script reads all of them and writes one graph: a person, their roles, projects, skills, education and credentials, plus an evidence node for every fact, recording where it came from. Each source is declared in a manifest with an authority tier, because sources disagree: the site and the CV can give different date ranges for the same job, or different URLs for the same project. typescript 'source:portfolio:resume-pdf': { title: 'Published résumé PDF', mediaType: 'application/pdf', // Downloaded from the live Google Doc on every ingest. publicUrl: 'https://docs.google.com/document/d/