When the Chain Is Not Enough: Building a Full-Stack Agentic DApp
A DeSci peer-review case study, told as a user journey — where every step in the workflow introduces the piece of the stack that makes it possible.
Companion piece to the BioVerify project page.
Peer review is broken: opaque, unpaid, and riddled with conflicts of interest. What if we could make it verifiable, fair, and economically aligned?
I built BioVerify: a full-stack agentic DApp where authors stake ETH on their research, reviewers are selected by Chainlink VRF, every artifact is content-addressed on IPFS, and AI agents coordinate the rest — with humans making every verdict call. You can try it on the live demo (Base Sepolia and Ethereum Sepolia, no setup), and the architecture and source are open on GitHub.
This is a case study in trust-minimised coordination: the chain records what happened, and agents orchestrate what happens next. Having spent fifteen years in Molecular Biology and Civil/Environmental Engineering before software — and having been on the author side of peer review myself — I had a specific reason to care. Rather than describe the stack in the abstract, this article follows the system as a user experiences it, and introduces each piece of technology precisely at the moment the story needs it.
"Truth on-chain, orchestration off-chain — with humans making every verdict call."
Why decentralised peer review — and why now
Peer review is a coordination game with a trust deficit: opaque processes, inconsistent conflict disclosure, and reviewers who are rarely compensated. Blockchain cannot fix science, but it can make parts of the process verifiable — stakes and outcomes settle on-chain, participants can prove what they signed, and for one specific subproblem (selecting reviewers fairly) Chainlink VRF attaches a cryptographic proof to the randomness so the draw can be audited.
BioVerify is an experiment in the coordination patterns that matter here: long-running workflows, randomly selected participants from a staked pool, human-in-the-loop delays, economic accountability, and settlement back on-chain.
- What it is — a DeSci peer-review case study: stake, verifiable reviewer selection, AI screening, human verdicts, on-chain settlement.
- CQRS as a first principle — the contract emits events (writes); an off-chain Postgres projection handles all reads. The chain is truth, not a database.
- Durable coordination — Inngest wraps agents in durable
step.runwith retries and crash-resume on serverless; LangGraphinterrupt()pauses for human signatures, and Postgres checkpointers persist graph state between resumes. - Human-in-the-loop — AI agents coordinate the workflow, but reviewers make every verdict call; agents record and settle, they never override.
Choosing a network
An author wishing to submit their work first chooses the network: Base Sepolia or Ethereum Sepolia. BioVerify deploys BioVerifyV3 independently on both testnets, each with its own contract address, reward and slash pools, and reviewer pool. The same Solidity source is deployed to both networks, producing the same ABI on each — only per-chain parameters such as the Chainlink VRF coordinator and subscription differ. This multi-chain design is deliberate: BioVerify is chain-agnostic across EVM-compatible networks; the same coordination patterns work without modifying application logic.
Building the submission: IPFS and content addressing
The author fills a form with the components of their research: title, abstract, authors, body content, and any supporting files. Before anything touches the blockchain, each of these components is uploaded to IPFS — the InterPlanetary File System.
IPFS addresses files by content, not location: a CID (Content IDentifier) is a cryptographic hash of the bytes. Change a single byte and the CID changes — once recorded on-chain, it is a commitment to exactly those bytes.
BioVerify uses Pinata as the IPFS pinning service. Each component is uploaded individually, producing its own CID. The frontend then assembles a final research manifest — a structured JSON object containing all the component CIDs — and pins that too. The manifest's CID becomes the single identifier for the whole submission.
Calling the contract: stake and submission fee
With the manifest CID in hand, the author calls submitPublication(cid) on the BioVerifyV3 contract — alongside two distinct ETH amounts:
- A stake. The author puts skin in the game. If the publication is rejected for plagiarism or fails peer review, this stake is slashed. If it passes, the stake is returned plus a reputation boost. That alignment makes the incentive structure legible: the author is incentivised to submit only non-plagiarised work and only work they believe will survive human peer review.
- A submission fee. This is not a platform fee — it funds Chainlink VRF (Verifiable Random Function). On-chain, the fee is
msg.valueminus the fixed publisher stake; callers may overpay so spikes in gas or VRF costs stay covered. WhenpickReviewersruns, the contract forwardspaidSubmissionFeeinto the VRF subscription viafundSubscriptionWithNative, so each submission directly tops up the oracle budget. The frontend can estimate that fee in real time from current conditions while the contract enforces a minimum.
The contract records the author's address, the publication CID, and the paid submission fee, transitions the publication to SUBMITTED status, and emits SubmitPublication.
Getter-less contract: CQRS as a first principle
BioVerifyV3 is deliberately getter-light. It emits a granular event for every state transition and avoids exposing view functions for UI queries. This is CQRS — Command Query Responsibility Segregation — applied at the chain boundary: the contract is the write model; an off-chain Postgres projection is the read model.
The contract benefits: smaller bytecode means cheaper deployment, lower ongoing interaction gas where it matters, and a smaller attack surface to audit. The frontend benefits: the UI reads a fast Postgres projection rather than orchestrating dozens of eth_call requests against the chain — work the EVM is not optimised for product lists — while Drizzle-backed queries support rich filtering, sorting, and pagination. The chain is the source of truth; Postgres reflects that truth and is the source of responsiveness, lowering UI latency.
Alchemy Notify: from block to webhook
Alchemy Notify is Alchemy's webhook service. For each network, BioVerify configures one webhook scoped to a single contract address — the deployed BioVerifyV3 — so Alchemy POSTs a signed payload containing the event logs that contract emitted in each new confirmed block. Two such webhooks run in parallel — one for Base Sepolia, one for Ethereum Sepolia — both reacting to the same set of BioVerifyV3 events and both POSTing to the Next.js API route at /api/webhooks/alchemy/all-events.
That route runs as a Vercel serverless function. The first thing it does is verify the webhook's HMAC-SHA256 signature: Alchemy and the server share a per-network secret key, and any payload that does not match is rejected immediately. This prevents spoofed events from triggering agent actions or corrupting the read model.
The Neon Postgres read model: OCC projection
Once the signature is verified, the raw log is decoded with viem and dispatched to processContractEvent() — the core projector in the @packages/cqrs layer. This function translates on-chain events into typed updates and writes them into Neon Postgres.
The correctness mechanism is Optimistic Concurrency Control (OCC): each event carries a position stamp from the blockchain — its block number and log index. The projector only applies an update if the incoming event is more recent than what it has already stored. Late webhook deliveries can repeat; they cannot roll the read model backward. A viem WebSocket subscription to NewPublicationStatus invalidates the matching TanStack Query keys the moment the event is mined, so open lists update without polling.
RewardPool, SlashPool; members / stakes: RewardMember, SlashMember, IsAvailableReviewer, MemberReputation, MemberAvailableStake, MemberLockedStake, MemberLockedStakeOnPubId, Claim; publications: SubmitPublication, LockedStakeOnPubId, NewPublicationStatus; agent + VRF: Agent_RequestVRF, Agent_PickReviewers, Agent_RecordReview, Agent_FinalizePublication, treasury moves (Agent_TransferSlashPoolToTreasury, Agent_MoveSlashPoolToRewardPool). The projector in @packages/cqrs folds these into three Drizzle-backed tables: protocol, member, publication.Why Inngest, and why only two events trigger agents
Almost every BioVerify contract event exists to update the Postgres read model. Only two events also mean new long-running off-chain work must begin: SubmitPublication kicks off plagiarism screening for a manuscript; Agent_PickReviewers means VRF has finished, reviewers for that submitted publication have been picked, and human verdict collection must start. Everything else — stake movements, status transitions after settlement, treasury plumbing — is bookkeeping the projector already knows how to fold into Drizzle tables.
Those two moments map to Inngest events CHAIN_SUBMISSION_RECEIVED (submission agent) and CHAIN_PICKED_REVIEWERS_RECEIVED (review agent). Each handler wraps a LangGraph invocation inside a durable Inngest step.run.
Why Inngest? Vercel serverless functions have tight per-invocation time limits and no guaranteed in-memory state between invocations. Inngest wraps each agent run in a durable execution shell: automatic retries, step isolation so successful steps are not replayed, and clean crash-recovery if a worker dies mid-run. It is not the mechanism that waits days for a reviewer — that pause lives in LangGraph.
Why LangGraph? Human review needs real suspension inside the graph: interrupt() stops execution until a verified EIP-712 payload arrives, while the Postgres checkpointer remembers partial progress (who already reviewed, whether escalation opened). When a Next.js server action resumes the thread with Command({ resume }), the checkpointer reloads that state so the agent continues without replaying completed nodes.
- Author submits: manuscript uploaded to IPFS, CID + stake + submission fee recorded on-chain
- Contract emits
SubmitPublication→ Alchemy Notify → Next.js webhook (HMAC-verified) - Projector writes to Neon Postgres read model with OCC (no event can roll the state back)
- Inngest triggers the Submission Agent via a wrapped LangGraph call
The submission agent runs a linear sequence of nodes — no branching until the very end, which keeps the logic easy to follow. Here is what it does, step by step.
Fetch from IPFS. The agent retrieves the full publication manifest by its CID. This is the same content the author uploaded — content-addressed, so the agent is guaranteed to read exactly what was submitted, unchanged.
Plagiarism check with Exa. The agent passes the abstract to Exa AI — a semantic web search API optimised for research literature. Exa returns a ranked list of potentially similar publications from across the public web and academic databases.
LLM synthesis with Gemini. The raw Exa results are passed to a Gemini language model, which synthesises the similarity signals and produces a structured verdict inside the graph: pass (continue to review) or fail (plagiarism). The LLM node itself never touches the chain — no contract calls happen inside that step. Crucially, the output is schema-constrained; after the graph finishes, agent-start.ts maps pass/fail to exactly one CQRS command path.
Branch: plagiarism detected → earlySlashPublication(). On fail, the agent invokes earlySlashPublicationCommand: the AI verdict is pinned to IPFS, then earlySlashPublication(pubId, verdictCid) is called through the agent's immutable wallet. The contract slashes the author's stake and transitions the publication to EARLY_SLASHED. The workflow ends here for this publication.
Branch: clean → pickReviewers(). On pass, pickReviewersCommand calls pickReviewers(pubId) on the contract. This triggers the Chainlink VRF request — which is where the randomness story begins.
Access control: agent-gated transitions
Only the agent's immutable wallet address — set once at contract deployment and unchangeable afterwards — can call earlySlashPublication, pickReviewers, recordReview, publishPublication, slashPublication, and pool-management moves guarded by onlyAgent. Arbitrary callers cannot trigger those transitions — the contract simply checks msg.sender against the immutable agent address.
Demo — AI plagiarism detection and early slashing. Dual-device view: User A (left, mobile, no wallet) on /publications; User B (right, tablet, wallet on Base Sepolia).


The reviewer pool: anyone can join by staking
Any user can register as a reviewer by depositing a reviewer stake into the contract. This stake is the reviewer's skin in the game: if a reviewer delivers a verdict that is later deemed negligent (more on this below), their stake is slashed. Staking is also what makes Sybil attacks expensive: each fake reviewer account costs real ETH, and VRF only draws a handful of reviewers per publication, so the probability that any given fake account is drawn falls as the honest pool grows.
Why VRF, and why it matters when stakes are real
Blockchains are deterministic. Any randomness derived from public on-chain fields (blockhash, block.timestamp) is predictable or influenceable in principle — not sufficient when selection has economic consequences.
Chainlink VRF v2.5 follows the usual hybrid smart-contract pattern: random words are produced off-chain together with a cryptographic proof; the consumer contract verifies that proof on-chain before consuming the words. The draw is not merely opaque — it is verifiable. Think of it as a public lottery where you do not have to trust the organiser: anyone can re-check the draw afterward.
In BioVerify's flow: the agent calls pickReviewers() → the contract calls requestRandomWords() → Chainlink fulfills via fulfillRandomWords() → the callback selects reviewers from the staked pool and emits Agent_PickReviewers. That event re-enters the exact same event pipeline — Alchemy Notify, HMAC-verified webhook, Neon Postgres projection — closing the loop without any special-casing.
Demo — Submitting a publication (success path). Split view: BioVerify Telegram bot (left) and DApp (right).



After projection, Agent_PickReviewers fires CHAIN_PICKED_REVIEWERS_RECEIVED into Inngest, which kicks off the second LangGraph agent. This agent is the most complex part of the system — it manages a workflow that can pause for days, handles human escalation, and ultimately settles the publication on-chain.
Human-in-the-loop (HITL): what pausing actually means
HITL is wired directly into the LangGraph itself. When the review workflow reaches humanReviewsNode, the graph calls interrupt() and stops. The Inngest step that wrapped the agent invocation finishes normally — the work is paused, not blocked — and LangGraph's Postgres checkpointer persists the partial graph state in Neon. Days later, when a reviewer signs their verdict, a Next.js server action verifies the EIP-712 payload and calls resumeReviewersAgent, which issues Command({ resume }). LangGraph reloads the thread state from the checkpointer, applies the new review, records recordReview on-chain once the payload checks out, and continues until the next interrupt or settlement.
How reviewers submit verdicts: EIP-712 gasless signing
Reviewers are not push-notified by the system — they check the reviewer dashboard at /publications/assignments, find the publication they have been assigned to review, and step through the flow from there. They read the manuscript and submit their verdict — without paying gas for the review transaction itself. Instead, they sign using EIP-712: typed structured data that yields a standard cryptographic signature without broadcasting a transaction.
The signed payload is posted to the backend off-chain. After viem verifies the signature and confirms the signer is exactly the reviewer assigned to this publication, the agent calls recordReview(pubId, reviewer) through the agent wallet. On-chain, that call only records that this reviewer has submitted their review for this publication; the qualitative verdict stays in LangGraph until settlement encodes honest versus negligent addresses.
Anyone can also follow the public BioVerify Telegram bot, which broadcasts contract state transitions in real time across both Base Sepolia and Ethereum Sepolia.
Peer reviewers, senior reviewers, and the escalation path
BioVerify distinguishes two reviewer roles within a publication cycle (current Base Sepolia and Ethereum Sepolia deployments draw I_VRF_NUM_WORDS = 3 candidates so there are two explicit peers plus one senior; the senior is whichever drawn reviewer has the highest on-chain reputation):
- Peer reviewers. The non-senior members of the VRF draw. They submit independent pass/fail verdicts. When both agree, the graph routes straight to settlement —
publishPublicationif the shared verdict is pass,slashPublicationif it is fail; in that branch the settlement helper classifies both peers as honest: each gets their stake back, the reviewer reward, and a reputation bump — the senior reviewer is not invoked. - Senior reviewer. Drawn alongside peers but designated as the highest-reputation reviewer of the three. If peers disagree, the workflow escalates to the senior reviewer (tie-break assisted by Gemini), who signs a binding verdict. The peer who aligned with the senior is honest; the dissenting peer is negligent and is slashed (stake to the slash pool, reputation drop). Even when peers agree, the senior remains economically aligned: settlement code unconditionally credits them as honest — their locked stake unlocks and they still receive the reviewer reward, on the grounds that they served as the standby escalation authority.
Possible outcomes and what triggers settlement
The review agent settles the publication once the binding verdict is established — either by peer reviewer consensus or senior reviewer decision. It calls one of two contract functions through the agent address:
publishPublication(pubId, honest, negligent, verdictCid)→PUBLISHED. The publisher's locked stake unlocks back intoavailableStakeand their reputation increases. Every address listed as honest (peers who matched the decision plus the senior reviewer) unlocks stake, receives the configured reviewer reward, and gains reputation. Negligent reviewers lose their locked stake to the slash pool and take a reputation penalty.slashPublication(pubId, honest, negligent, verdictCid)→SLASHED. The publisher is slashed the same way as other failure paths. Honest reviewers — including those who caught the failure — still receive stake, reward, and reputation; negligent reviewers are slashed.
Settlement deliberately uses a pull payment pattern: instead of pushing ETH to every participant inside the settlement transaction — where one malicious or buggy receive() hook could revert and grief the entire cohort (a denial-of-service against honest actors) — the contract moves value into internal balances and lets each party call claim() on their own schedule. Gas isolates per claimant; failures stay local.
step.run boundaries while LangGraph owns human pauses: the checkpointer restores state after each interrupt() so the graph knows both what it last knew and what must happen next.Demo — Peer review with human-in-the-loop conflict resolution. Two peer reviewers return conflicting verdicts; the senior reviewer breaks the tie and settlement flips the publication to PUBLISHED.




- Author uploads to IPFS → calls submitPublication with stake + submission fee
SubmitPublication→ pipeline → Neon DB + Inngest → Submission Agent- Submission Agent: IPFS fetch → Exa plagiarism check → Gemini verdict →
earlySlashPublicationorpickReviewers pickReviewers→ Chainlink VRF →Agent_PickReviewers→ the same webhook + projector pipeline → Review Agent- Review Agent: LangGraph
interrupt()while reviewers sign EIP-712 payloads → consensus or escalation →publishPublication/slashPublication - Settlement: rewards honest actors and/or slashes negligent ones; pull payments credit internal balances on-chain — each party calls
claim()themselves
Incentives: making honesty the rational choice
The economic mechanics aim for honest participation to be the dominant strategy inside a trust-minimised system — not by assuming participants are virtuous, but by making dishonesty costly in every scenario the contract can observe.
| Outcome | Trigger | Publisher | Honest reviewers | Negligent reviewers |
|---|---|---|---|---|
| Early Slashed | Plagiarism detected pre-review | Stake slashed · rep penalty | None selected yet | N/A |
| Published | Peer review consensus: pass | Stake returned · rep boost | Stake + reward + rep boost | Stake slashed · rep penalty |
| Slashed | Peer review consensus: fail | Stake slashed · rep penalty | Stake + reward + rep boost | Stake slashed · rep penalty |
| Honesty is measured against the binding verdict (peer consensus or senior tie-break), not the paper's outcome. The senior reviewer is always classified as honest at settlement. | ||||
Honesty is measured against the binding verdict, not the paper's outcome. When peer reviewers agree, both peers aligned with that verdict are rewarded; the senior is always credited as honest at settlement so their standby stake and reward settle cleanly. When peers conflict, the senior reviewer's verdict is binding: the matching peer is honest; the dissenting peer is negligent. The system is intentionally simple, with a clear limitation: a wrong or compromised senior reviewer gets the final word — the roadmap addresses this with weighted majority voting based on on-chain reputation.
Security as a first-class concern
CEI pattern (Checks–Effects–Interactions) is applied consistently across external state-mutating functions in BioVerifyV3. OpenZeppelin nonReentrant additionally guards ETH-out paths: claim and transferSlashPoolToTreasury.
Pull payments. As described above, pushing ETH to every recipient in one transaction would concentrate griefing risk: the contract itself becomes the caller, and a single reverting receive() could deny service to the entire cohort. Crediting internal balances plus user-initiated claim() transfers isolates failure modes and caps settlement gas for the agent.
Contract test coverage. BioVerifyV3 ships with full coverage in Foundry: 50 tests across 12 suites — 100% lines, statements, branches, and functions. VRF flows use VRFCoordinatorV2_5Mock with deterministic fulfillment overrides and log assertions, including edge cases such as colliding random words.
The pattern generalises
Peer review is the domain BioVerify is grounded in, but the coordination pattern is not specific to science. The same problems show up wherever multiple mutually untrusting actors need a trust-minimised process, with verifiable selection, long-running human-in-the-loop steps, and on-chain settlement.
These are not claims I have validated — they are domains where I see the same structural ingredients (verifiable selection, long-lived HITL, on-chain settlement) and think the coordination pattern could apply:
- Legal arbitration — verifiable evidence submission, randomly selected arbitrators from a staked pool, durable multi-week deliberation, tamper-evident record of proceedings.
- Insurance claims — multi-assessor processing where each step needs an audit trail and assessors have economic skin in the game.
- DAO governance — proposal review with randomly drawn delegates, slashing for negligent voters, durable agent coordination across asynchronous human input.
- Supply chain compliance — multi-party certification workflows where documents need content-addressed storage and auditor selection needs to be bias-resistant.
Verifiable fairness, economic accountability, durable coordination — the throughline is always the same trio. BioVerify is one instantiation.
What this build is, and what it is not
BioVerify is a case study, not a finished product. The current build covers the optimistic happy paths. The hardening backlog and roadmap include:
- Author escalation path — today a plagiarism false positive from the submission agent (or a peer-review verdict the author believes is wrong) is terminal: settlement runs immediately and the author has no contractual recourse. The roadmap addresses this with an opt-in escalation: the author posts a larger escalation stake within a bounded window, which triggers a second review cycle restricted to humans only, drawn from a fresh VRF cohort that excludes the original reviewers. The second verdict is binding and reconciles the first — confirmed means an additional slash; overturned means prior rewards and slashes are reversed and recomputed. This implies that terminal settlement effects must be deferred or reversible until the escalation window closes.
- Weighted majority voting — replaces the single-senior-reviewer tie-break with reputation-weighted consensus, removing the single point of failure.
- ZK reputation via Reclaim Protocol — lets reviewers prove off-chain credentials (ORCID, h-index, institutional affiliation) without doxxing themselves, enabling credentialed review without exposing identity.
- Encrypted access (Lit Protocol) and monetisation (x402) — manuscript confidentiality during review; post-publication micropayment gate routing revenue to researchers, closing the privacy and monetisation gaps.
- Internal corpus + RAG (Neon + pgvector) — Exa handles general web plagiarism; an internal pgvector index compares submissions against BioVerify's own historical publications.
- Known edge cases — empty or malformed IPFS payloads can leave a publication stuck in
SUBMITTED; a gas spike during settlement can strand a publication inIN_REVIEWwith no retry. The README documents these at the file level, including specific nodes and commands affected.
Tech stack at a glance
- Blockchain & contracts — Foundry, Solidity, Chainlink VRF v2.5, EIP-712
- Infrastructure — Alchemy Notify (contract-scoped webhooks), Vercel serverless functions
- Agents & orchestration — LangGraph (graph state + HITL interrupts), Inngest (durable step.run, retries, crash recovery)
- Data & storage — Neon Postgres (CQRS read model + LangGraph checkpointer), Drizzle ORM, IPFS via Pinata
- LLM & search — Gemini, Exa AI
- Frontend — Next.js, wagmi, viem, Reown AppKit, TanStack Query, TanStack Table, nuqs
- Schema validation — Zod
Closing
What drew me into this build was the interplay between two layers with distinct responsibilities: the chain as the Truth layer — where stakes settle, agent-gated transitions execute, and VRF proofs are verified — and agents as the Orchestration layer — where multi-actor, multi-day workflows actually play out.
BioVerify is the case study I used to wire that idea end-to-end. The interesting work lives in the seams — between Inngest and LangGraph, between Alchemy webhooks and the OCC projector, between EIP-712 signatures and on-chain settlement — and in the contract incentives themselves: tuning staking, rewards, and slashing so that long-running review cycles stay economically coherent over days. That is what I want to keep building.
The contracts are deployed on Base Sepolia and Ethereum Sepolia, and the source is open.