Open, traceable ratings for life-science papers.
LabsTrail publishes life-science and health papers and gives them an open quality rating that anyone can request, read and verify (ADR 0037).
How much to trust a paper is usually inferred from where it was published, and the reviews behind that judgement are rarely public. Yet many published results cannot be repeated: in a 2016 Nature survey of more than 1,500 researchers, more than 70% had tried and failed to reproduce another scientist's experiments (Baker, Nature 533, 452–454). AI agents now read and build on papers at a scale no person can, with no better way to tell which ones give enough to be repeated. LabsTrail rates the quality of a paper in the open and attaches the rating to the paper itself: each paper and each rating is a public record on the OriginTrail Decentralized Knowledge Graph (DKG), anchored on chain, with the paper's PDF on IPFS at an address hashed from its content, so none of them can be quietly changed. A rating starts as a machine score of how completely the paper reports what others need to reproduce its work, and grows through transparent human review and, later, wet-lab replication (ADR 0012).
Status: phase 1 is live on staging and production, on testnets (since release v0.7.0,
2026-10-20; latest release v0.8.1, 2026-10-25). It is a one-person project under
construction: Known limitations lists what it does not do yet. LabsTrail is a rebuild of an earlier prototype, desci-rating-dapp, which ran
both flows (publish a paper, rate it) end to end on Base Sepolia. This repo starts again
from clean foundations (monorepo, tooling, CI), and its ADRs and domain facts record what
the prototype taught us. A visitor connects a wallet, publishes a PDF, follows it until it
is minted, then verifies its record (signature, token, UAL, CID) from the paper's page.
Every published paper is listed at /papers, newest first, read a page at a time from the
DKG, with its latest score. A published paper's page requests a rating from the visitor's
wallet, and each rating has a page that follows it to its score and then shows every check
and quote behind it. Deployed, a webhook starts each rating and a cron restarts a stuck
one; locally, ratings run against a contract on Anvil. The repo, its packages and the EIP-712 domain use the lowercase name labstrail; minted records,
graph names, the Inngest app and the node server keep the first name, verisci
(ADR 0042).
Everything runs on testnets. Production is open to search engines and AI search: each paper
and rating has its own title and structured data, each paper its own share image, and
/method explains the score (apps/web → Search engines and AI search,
ADR 0040). Pages load the wallet only when a visitor uses it or comes back connected, which keeps
every page at 92-97 on Lighthouse mobile, and production counts visits without cookies
(apps/web → Speed and the wallet,
ADR 0041).
Built:
- A paper becomes a Target KA. Its submitter signs it with their wallet; the PDF is parsed, its metadata read, a small Gemini model checks it is a life-science or health paper, and it is published to the DKG as a Knowledge Asset that records who submitted it (ADR 0010, ADR 0037). A paper the check places outside the field is refused before anything is stored; when Gemini cannot answer, the paper is published and its rating checks the field. A publish that fails after its retries is recovered by publishing the same PDF again.
- Every write is safe to retry. Each step checks what is already done before acting, so a retry never duplicates anything (ADR 0007).
- Staging and production are kept apart, with their own contracts, graphs, oracle wallets and upload-limit counters; the DKG node, the Reown project, the pinning account, the Upstash database, the Alchemy RPC account, the Gemini key and the alerts chat are shared (ADR 0005).
- A requested paper is rated. A rating run reads the paper's text, has an LLM answer the reporting checklists of the kinds of study it reports (a trial, an animal study, lab work, a review…) with quotes, and computes the score from those answers, once: it measures whether the paper gives what others need to check and reproduce its work, not whether the work is right; our DKG node publishes the rating as its own Rating KA (R-KA), and the oracle records the score on the contract (ADR 0011, ADR 0014, ADR 0018). Locally, it runs against a contract on Anvil (ADR 0036).
- Anyone with a wallet requests a rating from the paper's page, paying only gas (ADR 0015). The rating's page follows it to its score, then shows every check, answer and quote behind it; the paper's page and the papers list show its latest score, the contract's (ADR 0014). Locally, the page announces its own request, since no webhook watches Anvil (ADR 0017).
- Deployed, an Alchemy Notify webhook turns each request on chain into a rating run (ADR 0017), and a cron reconciler restarts a request whose run stopped, from the contract's own list of pending requests; that request's run finishes or cancels it (ADR 0020).
- The maintainer hears of it on Telegram: every paper published, every rating settled or delayed (with when it would be cancelled), a failed run that needs fixing, the DKG node down, wallets running low and requests pending too long (ADR 0038).
Deployed on Vercel (apps/web → Deploying): staging from
develop, production from main.
Terms:
- A KA (Knowledge Asset) is a record on the DKG: content off-chain on the DKG,
an ERC-721 token and a merkle root of the content on-chain (
docs/domain.md→ DKG). The ones LabsTrail publishes are minted to its DKG node's agent address, which alone can update them. - A Target KA is a paper's KA (the pages call it a paper asset); an R-KA is a rating's KA (a rating asset), which points at its Target KA.
- A UAL is a KA's stable address, the link between its off-chain content and its
on-chain token; the ones LabsTrail publishes read
did:dkg:base:84532/<agent address>/<number>. - A context graph is the DKG graph a KA lives in; each environment has its own.
- The submitter is the wallet that signed a paper's submission; the oracle is LabsTrail's account that records rating results on the contract.
- Testnet only, and requests are free. LabsTrail pays the oracle's gas, the DKG publish and the LLM. The cap of 3 pending requests per address is easy to dodge with fresh addresses, so a throttle of 3 rating runs an hour is the real bound (ADR 0015).
- The score measures reporting, not correctness. It says whether a paper gives what others need to check and repeat its work, not whether the work is right: a fabricated paper that reports everything would score well. Fourteen reference papers catch regressions in the scorer; they are far too few to call the score calibrated.
- Scores still move between ratings. Gemini 3.8 Flash has no documented seed, and
temperature is deprecated for it, so judgement checks such as
limitationscan be answered differently. With scorer version 4, 14 runs on 3 papers spread by 0, 7 and 9 points (docs/domain.md). - One shared DKG node on one free VM serves staging and production: an outage takes both down, and the node's single admin token means staging's credential could in principle write the production graph; a configuration guard covers that for now.
- Gemini's free tier allows 20 scorer requests a day, shared by every environment.
- One hot oracle key per environment, holding a few days of gas.
- One copy of each PDF, on Pinata (roadmap item 4).
- No end-to-end tests yet; the pages have component tests.
In order, without dates. An ADR link marks what is decided; the rest is planned, and each plan records its decisions in an ADR.
- Transparent human review (phase 2, next). The machine score checks what a paper reports, not whether its content holds up, and in traditional peer review the choice of reviewers is opaque. Reviewers are drawn at random with Chainlink VRF, whose proof is checked on chain, so no one can steer the draw and anyone can audit it. They score the paper; our DKG node writes their score into the R-KA, then the oracle records it on the contract (ADR 0012). Planned, not decided yet: reviewers stake to join the pool, are paid for their reviews and lose part of their stake for reviews in bad faith.
- Payment, for people and AI agents. Today a requester pays only their gas and LabsTrail pays the rest, bounded by a cap and a throttle; a payment plan replaces that before mainnet (ADR 0015). Planned, not decided yet: publishing and rating each carry a fee that a person or an agent can pay, covering the costs and the reviewers' pay.
- An API for AI agents, to read papers and ratings and to request ratings without the web app.
- A lasting, decentralized copy of every PDF. Pinata holds the only copy today (ADR 0010): the CID proves a file is unchanged but does not keep it available. Planned before mainnet.
- Mainnet, after the payment plan (ADR 0015).
- Wet-lab replication (phase 3). A lab repeats the experiments, and a second round of human review checks its results before the oracle scores them.
The diagrams below show phase 1 as built.
Architecture views (overall architecture, the publish and rating pipelines, external
integrations, state machines, routes) are in .omm/, written with
oh-my-mermaid, a pinned dev dependency:
each element describes a folder, a flow or a service, with a Mermaid diagram. Each branch
refreshes the elements it changes (/implement), and /review-branch blocks a PR that
leaves one stale; each workspace README links to its own. pnpm exec omm view --port 4000
shows them in a browser.
Each environment has its own context graph on the shared DKG node
(ADR 0005): verisci-staging and
verisci-main-prod. Our node publishes the ratings of every contract of an environment to that environment's
graph, so a redeployed contract keeps the graph of the one it replaces; the old
contract is paused and drained (ADR 0023).
Each contract's R-KA names are its own, because the request id hashes the chain
id, the contract's address and its deploy time
(ADR 0016).
The DKG node runs with GROBID and an RPC proxy on its own server, a free Oracle Cloud VM,
reached at https://verisci-dkg.duckdns.org and https://verisci-grobid.duckdns.org
(ADR 0006). Caddy is its only public
entry, and each environment, developers' computers included, reaches it with its own
credential (ADR 0039). The node reads
Base Sepolia only through that proxy, infra/rpc-proxy,
which asks free public endpoints first and keeps Alchemy RPC within a daily budget.
docs/node-host.md describes the server, how to operate it and how to
rebuild it from the node's backup; its configuration files are in
infra/node-server.
Each environment also has its own oracle wallet, Alchemy Notify webhook, Inngest
environment and upload-limit counters, in one shared Upstash database. Only develop holds staging's oracle key and runs
its ratings; previews share staging's contract and graph, and local development shares
staging's graph but rates, pages included, on a contract on a local Anvil chain with its
oracle key (ADR 0019,
ADR 0036).
flowchart TB
subgraph staging["Staging: develop"]
direction TB
sa["App server<br/>upload page, Upstash limits"]
sp["Publish runs<br/>Inngest environment"]
sc["RatingController (staging)<br/>Base Sepolia<br/>+ past contracts, paused"]
sr["Rating runs<br/>oracle wallet, Alchemy Notify webhook,<br/>same Inngest environment"]
sg["Staging graph<br/>on the shared DKG node host"]
sa -->|"paper.submitted"| sp -->|"stores and mints Target KAs"| sg
sc --> sr -->|"stores and mints R-KAs"| sg
end
subgraph production["Production: main"]
direction TB
pa["App server<br/>upload page, Upstash limits"]
pp["Publish runs<br/>Inngest environment"]
pc["RatingController (production)<br/>Base Sepolia<br/>+ past contracts, paused"]
pr["Rating runs<br/>oracle wallet, Alchemy Notify webhook,<br/>same Inngest environment"]
pg["Production graph<br/>on the shared DKG node host"]
pa -->|"paper.submitted"| pp -->|"stores and mints Target KAs"| pg
pc --> pr -->|"stores and mints R-KAs"| pg
end
develop is staging and main is production, both on testnets for now. Feature PRs
target develop; release PRs move develop into main
(CONTRIBUTING.md).
sequenceDiagram
actor U as Submitter browser
participant P as IPFS pinning
participant A as App server
participant R as Publish run (Inngest)
participant G as GROBID (node host)
participant M as Gemini Flash-Lite
participant N as DKG node
U->>A: ask for an upload URL
A-->>U: short-lived signed upload URL
U->>P: upload the PDF
P-->>U: CID
U->>A: CID + EIP-712 signature (with graph and deadline)
A->>A: verify the signature
alt already minted
A-->>U: its UAL at once, nothing new written
else
A->>A: the submitter's limit and the pinned file
A->>R: event with the CID, submitter, signature (skipped while a run holds this PDF)
R->>R: verify the signature, Target KA name from CID
R->>N: read the Target KA's state (stop if already minted)
R->>P: fetch the PDF by its CID
R->>G: parse the PDF
R->>R: read metadata from the TEI and the start of the text (refuse with a reason if unreadable)
R->>M: title, abstract and start of the text: in the field? (refuse if outside)
R->>N: store with the submitter address and signature, then mint and poll
N-->>R: Target KA UAL, ready to be rated
end
U->>A: poll the paper's page
A-->>U: reading, saving, minting, then published with its UAL, or refused or stopped with why
Every PDF goes through one uploader with fixed IPFS import settings, so the same PDF always
has the same CID and publishing it again converges on the existing Target KA, which keeps its
first submitter (ADR 0010). Anyone with a
wallet may publish, within daily limits (ADR 0034,
ADR 0035). The pages are in
apps/web; the work behind them, and a dev script that publishes
without the page, in packages/workflows.
sequenceDiagram
actor U as Requester wallet
participant C as RatingController
participant W as Webhook route
participant R as Rating run (Inngest)
participant N as DKG node
participant O as Oracle function
U->>C: requestPhase1(targetUal)
C-->>W: Phase1Requested(requestId, …) via the Alchemy Notify webhook
W->>R: event keyed on requestId
R->>C: read the request and the contract's oracle
Note over R: Stop if settled, or if the oracle is not this deployment's key.
R->>N: is the target one of our published papers?
alt not a paper LabsTrail published (its UAL, then the node's answer)
R->>O: cancel(requestId, invalid target)
O->>C: cancelRequest(requestId, invalid target)
else otherwise
R->>R: R-KA name from requestId
R->>N: read R-KA state by name
alt past the maximum age, and no R-KA minted (a stored one is polled 10 minutes first)
R->>O: cancel(requestId, expired)
O->>C: cancelRequest(requestId, expired)
else otherwise
opt nothing stored
R->>N: find the PDF the target links
R->>R: read its text, then Flash-Lite checks its field
R->>R: one Gemini request gives its field, study types and checks, with quotes
Note over R,O: Outside the life sciences and health, no readable text,<br/>or two answers the check refuses: cancel(requestId, invalid target), and stop.
R->>R: check the quotes against the paper, compute the score
R->>N: store the R-KA
end
opt not minted yet (a draft is shared first)
R->>N: mint and poll until minted
end
R->>N: read the score back from the minted R-KA
R->>O: fulfil(requestId, score, rKaUal)
O->>C: fulfilPhase1(requestId, score, rKaUal), then check the receipt
end
end
Note over C,O: The oracle function re-reads the request and sends nothing if it is already settled.
Note over C,R: Every 30 minutes, a cron reconciler pages through pendingRequestIds(offset, limit)<br/>on every contract of the environment, past ones included,<br/>and restarts the run of any request pending over 4 hours.
Every run reads the request on chain and its R-KA on the node, then does what
is left: a retry, or a run restarted by the reconciler, recomputes the same name
from the request id and converges on the same R-KA
(ADR 0007, ADR 0017,
ADR 0020). A run
that finds an R-KA stored by an earlier run mints it at once: a KA cannot be
minted twice
(ADR 0008). Every
fulfil and cancel goes through the one serialized oracle function, which reads
the request again and sends nothing if it is already settled, then checks the receipt in
later steps and raises the fees of a transaction stuck unmined
(ADR 0019). The score the oracle
records is always the one read back from the minted R-KA
(ADR 0018); a target that is
not a paper LabsTrail published is cancelled
(ADR 0011), as is a UAL not in
canonical form (ADR 0031) and a
paper outside the life sciences and health
(ADR 0037). The
contract's calls and events are documented in the
@labstrail/contracts README.
A pnpm and Turborepo monorepo: a Next.js app, five internal packages, and the
RPC proxy that runs on the DKG node server (infra/rpc-proxy). Packages ship
TypeScript source, with no build step; Next.js compiles them through
transpilePackages, and the server runs the proxy's source with plain Node.
| Workspace | What it is | Depends on |
|---|---|---|
apps/web |
Next.js app: the pages and the server they call | all five packages |
packages/env |
Typed environment variables | none |
packages/core |
Domain logic, no IO | none |
packages/dkg |
DKG adapter | core, env |
packages/contracts |
Solidity contracts and their TypeScript side | core, env |
packages/workflows |
Inngest workflows and the pages' calls | core, env, dkg, contracts |
infra/rpc-proxy |
JSON-RPC proxy run on the DKG node server | none |
Also infra/node-server: the DKG node server's Caddy site and
systemd units, configuration copied to the server by hand
(docs/node-host.md).
Each workspace may only import the workspaces it declares. pnpm does not
hoist undeclared workspace packages, so breaking this rule fails pnpm typecheck.
- Node 24.15 or later within 24.x;
.nvmrcpins 24.21.0, which CI uses.pnpm installrefuses anything outside>=24.15 <25. - Corepack, which provides the pnpm version pinned in
packageManager. jq, used by the Claude Code hooks in.claude/hooks/.- Foundry 1.8.4 (
foundryup --install 1.8.4). Needed forpackages/contracts, and bypnpm checkandpnpm test, which callforge.
corepack enable
pnpm install
(cd packages/contracts && forge soldeer install) # Solidity dependencies
pnpm dev # starts apps/web on http://localhost:3000To publish a PDF locally (the Inngest dev server, with the node server's URLs and your
credential in .env.local), follow packages/workflows → Running a publish locally,
then open http://localhost:3000/publish (it needs the workflows' settings and
REOWN_PROJECT_ID; see apps/web). To rate a paper
locally, from its page or from a script, against a contract on Anvil, follow
packages/workflows → Running a rating locally.
Environment variables are listed in .env.example: copy it to
.env.local at the repo root (gitignored), where apps/web loads it from.
pnpm test runs without it, and pnpm build needs only APP_ENV (APP_ENV=local).
pnpm dev starts without it, but the home page's latest papers, the papers list, and the
publish and paper pages need the DKG node and publish run settings, and the wallet window
REOWN_PROJECT_ID. The rating parts of the pages also read the chain: CHAIN_RPC_URL
when deployed, or locally Anvil and the two LOCAL_* settings. How workspaces declare
and validate them is in the @labstrail/env README.
| Command | What it does |
|---|---|
pnpm check |
Biome format, lint and import order; then forge fmt --check, forge lint (any warning or note fails) and the NatSpec check in packages/contracts |
pnpm check:fix |
Biome rewrites what it can fix safely |
pnpm typecheck |
tsc in every workspace |
pnpm test |
Vitest in every workspace, plus forge test in packages/contracts |
pnpm test:coverage |
Vitest across all workspaces with coverage thresholds: core ≥ 90% branches, the others ≥ 70% lines. Only files imported by tests count until end-to-end tests cover the pages (see vitest.config.ts) |
pnpm vitest related <file> --run |
Only the tests that touch <file> |
pnpm build |
Builds apps/web; needs APP_ENV (from the root .env.local, or APP_ENV=local pnpm build) |
typecheck, test and build run through Turbo, which caches results by
input. Biome and coverage run once at the root.
Every PR into develop or main, and every push to them, runs
.github/workflows/ci.yml with two parallel jobs:
ts:biome ci,typecheck,test:coverage(report uploaded as an artifact),build(withAPP_ENV=local)contracts: Soldeer install,forge fmt --check,forge lintand NatSpec,forge build --sizes, a check that the committed ABI matches the contract,forge testwith theciprofile (5000 fuzz runs)
Architecture decisions are in docs/adr/; facts about the DKG,
chain, Inngest, Vercel and tooling are in docs/domain.md.
CLAUDE.md and one CLAUDE.md per workspace give Claude Code the project
rules. .claude/ holds the shared settings, hooks, skills and the read-only reviewer agent
(.claude/agents/reviewer.md) that /review-branch and /review-adrs run:
| Skill | What it does |
|---|---|
/plan-feature <name> |
Writes docs/plans/NNN-<name>.md from the plan template |
/implement <NNN> |
Branches from develop, writes the tests first, then implements until green |
/review-branch |
Runs CI's checks, tests and builds, then a read-only review; blocks on any failure, stale docs (the architecture views included) or ADR conflict, then drafts the PR |
/review-adrs |
On demand: checks every ADR against every other and reports conflicts for the maintainer to decide |
Hooks format, lint and test each file Claude edits, block edits to generated files and
reads of env files (except .env.example), and typecheck the changed workspaces and the
workspaces depending on them before Claude finishes. They need jq. Permission rules deny
git push and the usual deploy commands (forge script --broadcast, forge create,
cast send); git commit asks first. These rules match how a command is written, so they
are guard rails, not a sandbox. Personal overrides go in .claude/settings.local.json
(gitignored).
Branches, commits, docs rules, pull requests and releases are covered in CONTRIBUTING.md. LabsTrail is built with Claude Code.