How it works
An EduGenAI Extension is a function-calling tool: you give EduGenAI an HTTP endpoint and a function definition, and its assistant calls that endpoint (as a JSON request) whenever it needs to. This citation checker splits cleanly across the two sides:
| Step | Who does it |
|---|---|
| Read the paper, pull out the reference list and the sentences that cite each source | EduGenAI's assistant — the paper is already in its chat context |
| Look each reference up in OpenAlex; check title, authors, year, journal, DOI, volume, issue, pages; return the abstract | This extension endpoint — deterministic, no LLM |
| Compare the citing sentence to the abstract and flag misquotes | EduGenAI's assistant, using the abstract the endpoint returned |
Before you start
You need this app deployed at a public HTTPS URL — EduGenAI calls it server-to-server. Everywhere below, replace https://YOUR-DEPLOYMENT-HOST with your deployment's address.
Step by step
-
Open the Extension builder
In EduGenAI, create a new Extension (the panel with Name, Short description, Headers, and Functions).
-
Name and describe it
Name
OpenAlex Citation Verifier
Short description
Verifies a paper's references against OpenAlex: checks that each work exists and that the printed title, authors, year, journal, DOI and pages match, and returns the abstract. Run it whenever the user says "start", "start the extension", "check", or asks to verify citations or references.
Detail description — this is the instruction EduGenAI's assistant follows. Paste it verbatim:
Use this extension to fact-check the reference list of an uploaded paper. TRIGGER - WHEN TO RUN. Run all four steps below end to end, WITHOUT asking any clarifying question first, whenever the user gives ANY go-ahead while a document is present in the conversation. Triggers include (not exhaustive): "start", "go", "run", "check", "verify", "start the check", "start the extension", "run the extension", "start the references extension", "start the reference verification", "check my citations", "verify my references", any message containing "start" or "run" together with "check", "extension" or "references", or a paper uploaded with no other instruction. Never respond to a trigger with a greeting, a menu of options, or a request to paste the bibliography - the document already in the conversation IS the input. Only if NO document has been provided at all, ask for one, then run. The steps run IN ORDER: you must finish STEP 1 (reading the document and extracting the references yourself) BEFORE calling the function in STEP 2. Never call verify_references with an empty, placeholder, or not-yet-extracted references array. AUDIENCE. The user is a marker / examiner / supervisor checking SOMEONE ELSE'S paper (typically a student's). Never address the user as the paper's author: write "the paper" or "the student's paper", never "your paper" / "you cite". Report findings as facts for a reviewer. Never give feedback or improvement advice to the writer. OUTPUT DISCIPLINE. Produce ONLY the output defined in Step 4 - the table, the Details section, the Missing-references section when relevant, and the one-line summary. NOTHING else: no APA or formatting critique, no style fixes, no suggested extra literature, no praise, no summary of what the paper is about, no advice - unless the user explicitly asks for such things in a later message. EXCEPTION - these are citation ERRORS, not style, and MUST be reported: wrong author names, wrong AUTHOR ORDER (e.g. the text or reference says "Kahneman & Tversky, 1974" where the record is Tversky & Kahneman), wrong year/journal/DOI/pages, in-text citations that contradict their reference entry, and citations missing from the reference list. Punctuation, capitalisation, and italics are style - stay silent on those. If the function call fails or returns an empty result, say exactly that in one or two sentences ("the verification service returned no data, so the references could not be checked - try again") and STOP: do not substitute an APA review, a plausibility opinion, or any other unrequested analysis. STEP 1 - EXTRACT. Read the document. For every entry in the reference list, pull out: title, the full list of author names in order, whether it ends in "et al.", year, DOI (only if printed), journal/venue, volume, issue, and page range. Also find the sentence(s) in the body where each source is cited, with one sentence of context on each side. Keep those citation sentences in your own working notes for Step 3 - do NOT send them to the function. Also list every IN-TEXT citation (e.g. "(Smith, 2020)", "Jones et al. (2019)") that has NO matching entry in the reference list - these ORPHAN CITATIONS cannot be looked up by any reader. Keep them in your notes for Step 4; do not send them to the function. If the paper has NO reference list at all, every in-text citation is an orphan: do not fabricate entries and do not send author+year-only items - skip the function call and go straight to Step 4, reporting every citation as missing its reference. STEP 2 - VERIFY. Call the "verify_references" function once, passing every reference that has at least a title or a DOI (an entry with neither cannot be identified and comes back as lookup_failed - report it as unverifiable, never as a hallucination). If the function's only parameter is "references_json" (a string), serialize the reference array to a JSON string and pass it as that parameter. For each one it returns: "badge" and "severity" (0-100), "status" (found / fuzzy / not_found / lookup_failed), "mismatched_fields" and "minor_fields", a "field_check" comparing each printed detail to OpenAlex (with reference_value vs openalex_value; each field's status is "match", "close" = a minor naming variation such as an abbreviated journal name, or "mismatch"), the matched "work" (authors, year, venue, doi, url) and its "abstract", and - for FUZZY matches only, where "work" is null and "field_check" is empty - a "candidates" array of the closest works (each with title, authors, year, venue, doi, url). STEP 3 - JUDGE MISQUOTES (THE CORE OF THIS TOOL - NEVER SKIP). This comparison is the main reason the user runs the check. For EVERY reference whose status is "found" and whose returned "abstract" is non-empty, you MUST compare the citation sentence(s) you kept in Step 1 against that abstract and produce a real verdict: if the paper uses the source as though it were about a different topic than the abstract shows (e.g. the abstract is about macroeconomic productivity but it is cited for labour productivity specifically), that is a MISQUOTE; otherwise it is consistent. Writing "-" in the Misquote column for a found row that has an abstract is an ERROR. Only when there is no abstract, or the reference is never cited in the body, is the verdict "unclear". Never invent a verdict. STEP 4 - PRESENT (follow this format exactly). The user must NEVER see the raw JSON from the function - it is a machine interface. Convert it into ONE readable Markdown table, one row per reference, sorted by DESCENDING severity. Use the function's "severity" value; if you judge a reference to be a misquote MISMATCH, raise its severity to AT LEAST 80 (keep the function's number if it is already higher). Columns: | # | Flag | Reference (as printed) | Status | Metadata issues | Misquote | What the paper is about | - Flag: 🔵 if status is lookup_failed (it could not be checked); otherwise 🔴 if severity >= 70, 🟠 if 45-69, 🟢 otherwise. - Status: the "badge" value with an emoji - 🔴 Potential hallucination (not_found), 🟠 Fuzzy match (fuzzy), 🔵 Lookup failed (found no result to compare), 🟢 Verified (found). - Metadata issues: if "mismatched_fields" is empty, write "-". Otherwise, for each mismatched field take its reference_value and openalex_value from "field_check" and write: field: "printed" -> "OpenAlex". Put **authors** in bold when authors is one of them. (For a fuzzy row there is no field_check yet - write "-".) Fields with status "close" ("minor_fields") are NOT errors - leave them out of this column, or mention them only as "minor naming variant" without a flag. - Misquote: 🔴 Mismatch / 🟠 Likely mismatch / 🟢 Consistent / ➖ Unclear, plus a 6-10 word reason. (Only "found" rows have a misquote; for others write "-".) - What the paper is about: one short clause summarising the "work" abstract, or "-" if none. After the table, add a "Details" section covering only the 🔴 and 🟠 rows: - For a FOUND row: the matched title as a link to its "work.url", the field-by-field comparison, and one sentence on the misquote if any. - For a FUZZY row: "work" is null - instead list the entries in "candidates" (title as a link to each "url", with authors and year) and ask the user to confirm which, if any, is the intended source. Do NOT present a fuzzy row as verified. Then, if Step 1 found orphan citations, add a "Missing references" section: one line per orphan - the citation as printed, the sentence where it appears, and the note that it has no entry in the reference list so it cannot be verified. This is a real problem for the reader and must never be silently omitted. End with one summary line that does NOT hide problems inside "verified": "N references - X verified & consistent, Y need review, Z fuzzy, W potential hallucinations, V in-text citations missing from the reference list." (Drop the last part when there are no orphans.) A reference that EXISTS but has a metadata mismatch or a misquote counts under "need review", never under "verified". If helpful, break Y down (e.g. "3 need review: 2 with wrong details, 1 cited out of context"). Never invent a DOI, author, year, or verdict the function did not return. If "status" is "lookup_failed", say the reference could not be checked - do NOT call it a hallucination.Installed this extension before? The saved copy in EduGenAI does not update itself. If "start the extension" gets you a menu instead of a run, your extension still has an older Detail description — open it in the Extension builder and re-paste the block above. -
Add the Header
In the Headers section, add one header so the endpoint receives JSON:
Key Value Content-Typeapplication/jsonOptional — OpenAlex Premium: if you have an OpenAlex Premium key and want higher rate limits, add a second header
X-OpenAlex-Keyand paste the key as its value. Use the Secure header values option so it is stored in Azure Key Vault, not in plain text. -
Add the function
Under Functions, add a function that points at the batch endpoint.
Method & URL
POST https://YOUR-DEPLOYMENT-HOST/api/verify_batch
If POST sends an empty body on your platform (see Troubleshooting), useGETwith the same URL instead — the endpoint accepts the arguments in the query string too.Function definition (name, description, JSON parameters)
{ "name": "verify_references", "description": "Call this whenever the user says 'start', 'start the extension', 'start the check', or wants to check, verify, or fact-check the citations or references in a paper. Verifies a list of bibliographic references against OpenAlex. For each reference, returns whether the work exists (found / fuzzy / not_found), a field-by-field comparison of the printed metadata (title, authors, year, journal, DOI, volume, issue, pages) against the real record, and the abstract of the matched work.", "parameters": { "type": "object", "properties": { "references": { "type": "array", "description": "Every reference in the paper's bibliography.", "items": { "type": "object", "properties": { "title": { "type": "string", "description": "The work's title." }, "authors": { "type": "array", "items": { "type": "string" }, "description": "Every author name printed, in order, e.g. 'Smith, J.'." }, "et_al": { "type": "boolean", "description": "True if the reference abbreviates the author list with 'et al.'." }, "year": { "type": "integer" }, "doi": { "type": "string", "description": "Only if printed in the reference." }, "journal": { "type": "string", "description": "Journal or venue name." }, "volume": { "type": "string" }, "issue": { "type": "string" }, "pages": { "type": "string", "description": "Page range as printed, e.g. '123-145'." } }, "required": ["title"] } } }, "required": ["references"] } }Plan B — if the arguments never arrive (the Troubleshooting hint shows an empty body and an empty query string on both POST and GET): the platform's gateway cannot serialize the nestedreferencesarray. Replace the function definition with this flat, single-string version — every gateway can transmit one string. The endpoint understands it natively; the assistant just passes the same reference array serialized as a JSON string.{ "name": "verify_references", "description": "Verify bibliographic references against OpenAlex. Call this whenever the user says 'start', 'start the extension', 'start the check', or asks to check, verify, or fact-check the citations or references in a paper. Pass the full reference list as ONE JSON string: an array of objects, each with title, authors (array of strings), et_al, year, doi, journal, volume, issue, pages. Returns, per reference: whether it exists (found / fuzzy / not_found), a field-by-field comparison against the real record, and the abstract.", "parameters": { "type": "object", "properties": { "references_json": { "type": "string", "description": "The paper's references as a JSON string: [{\"title\":\"...\",\"authors\":[\"Surname, F.\"],\"et_al\":false,\"year\":2020,\"doi\":\"\",\"journal\":\"\",\"volume\":\"\",\"issue\":\"\",\"pages\":\"\"}, ...]" } }, "required": ["references_json"] } } -
Submit and test
Click Submit. Then, in an EduGenAI chat, upload a paper and give a go-ahead — the assistant should extract the references itself and then call your endpoint. The most dependable prompt names the extension and the steps explicitly:
Use the execution steps in the verify_references extension in order to check the references in the attached pdf.
Why not just “start”? EduGenAI decides whether to call an extension from what you ask for, and a bare “start” often reads as a generic greeting — you get a menu instead of a run. The descriptions above tell it to treat “start” as a trigger, but the explicit prompt above works even when the platform's tool-selection is being obtuse.
What the endpoint returns
Example request EduGenAI sends to /api/verify_batch:
{
"references": [
{
"title": "Highly accurate protein structure prediction with AlphaFold",
"authors": ["Smith, J.", "Jones, B."],
"et_al": false,
"year": 2019,
"journal": "Science",
"pages": "100-110"
}
]
}Example response (abbreviated) — note the wrong authors, year, and journal are caught, and the abstract is returned for the misquote step:
{
"count": 1,
"results": [
{
"index": 1,
"status": "found",
"badge": "Verified",
"severity": 85,
"priority": "Review",
"mismatched_fields": ["authors", "year", "journal"],
"minor_fields": [],
"work": {
"title": "Highly accurate protein structure prediction with AlphaFold",
"authors": ["John Jumper", "Richard Evans", "..."],
"year": 2021,
"venue": "Nature",
"doi": "10.1038/s41586-021-03819-2",
"url": "https://doi.org/10.1038/s41586-021-03819-2",
"abstract": "Proteins are essential to life ... (returned for the misquote check)"
},
"field_mismatch_count": 3,
"field_check": [
{ "field": "title", "status": "match" },
{ "field": "authors", "status": "mismatch", "reference_value": "Smith, J., Jones, B.", "openalex_value": "John Jumper, Richard Evans, ...", "detail": "Not on the record: Smith, J., Jones, B." },
{ "field": "year", "status": "mismatch", "reference_value": 2019, "openalex_value": 2021 },
{ "field": "journal", "status": "mismatch", "reference_value": "Science", "openalex_value": "Nature" }
],
"notes": ["Metadata mismatch on: authors, year, journal. The work exists but the citation details differ."]
}
]
}POST /api/verify — same fields, but without the outer references array — if you prefer one call per reference. The batch endpoint is recommended for a whole bibliography (one round trip, kinder to OpenAlex's rate limits).From JSON to a readable table — what you'll see in chat
You never look at that JSON. It is the machine interface between EduGenAI and the endpoint. The Detail description you pasted in Step 2 tells EduGenAI's assistant to convert it (STEP 4) into a severity-sorted table with flags, scores, and the mismatches spelled out — mirroring how this site presents results. A typical chat answer looks like:
| # | Flag | Reference (as printed) | Status | Metadata issues | Misquote | What the paper is about |
|---|---|---|---|---|---|---|
| 3 | 🔴 | Zorblatt, Q. (2021). Quantum blockchain effects on medieval cheese trading… | 🔴 Potential hallucination | — | — | — |
| 2 | 🔴 | Smith, J. & Jones, B. (2019). Highly accurate protein structure prediction… Science, 100-110. | 🟢 Verified | authors: "Smith, Jones" → "Jumper, Evans…" · year: 2019 → 2021 · journal: Science → Nature | 🔴 Mismatch — cited for labour productivity; paper is protein-structure AI | Deep-learning protein structure prediction |
| 1 | 🟢 | Vaswani, A. et al. (2017). Attention is all you need. NeurIPS. | 🟢 Verified | — | 🟢 Consistent | Attention-only sequence model (Transformer) |
…followed by a Details section for the flagged rows and a one-line summary count. The assistant does this conversion because the Detail description explicitly says the user must never see the raw JSON.
badge, severity, priority, mismatched_fields) precisely so the assistant only has to lay them out, not compute them.Troubleshooting — the extension returns count: 0
Every response now carries an api_version field, and any response with zero results carries a hint field describing what the server actually received. Ask the assistant to show the raw output and read those two fields:
- No
api_versionin the response? The extension is calling an old deployment. Vercel preview URLs and old branch deployments keep serving stale builds forever — open the extension in the builder and make sure the function URL is exactlyhttps://www.phantocite.com/api/verify_batch(or your current production host), not a preview/branch URL. - The hint says the body was empty (
Content-Length: 0)? The platform is not putting the function arguments into the POST body at all — the assistant composed them, but the HTTP call left without them. Two fixes: (1) change the function's Method from POST to GET — platforms serialize arguments into the URL query string on GET, and this endpoint accepts that; (2) if the builder has a request body / body template field for the function, set it to pass the arguments through. - Empty on BOTH POST and GET (body empty and
query-string keys: [])? The gateway cannot serialize the nestedreferencesarray parameter and drops it silently. Switch the function definition to the Plan B single-string schema in Step 4 (onereferences_jsonstring parameter) — a flat string survives any gateway, and the endpoint accepts it on both methods. - A
hintsaying no references could be read (body not empty)? It describes the shape that arrived (key names only, never content) — send that hint to the tool's maintainer, or adjust the function schema to match Step 4. - "start" produces a menu instead of a run? Your extension still has an old Detail description — re-paste it from Step 2 (the saved copy never updates itself). The most dependable prompt: “Use the execution steps in the verify_references extension in order to check the references in the attached pdf.”
To check the endpoint independently of EduGenAI, run this from any terminal — a healthy deployment answers in a couple of seconds with count: 1 and a Verified result:
curl -s -X POST https://www.phantocite.com/api/verify_batch \
-H "Content-Type: application/json" \
-d '{"references":[{"title":"Attention is all you need","year":2017}]}'The definitive transport test — does the platform send anything at all?
When every schema variant still yields count: 0, settle it with the echo endpoint: a byte-level mirror at /api/echo that reflects back exactly what arrives — method, headers, query string, and raw body — with no parsing in between. It cannot be wrong: if your text isn't in its response, it never reached the server.
1. In the extension builder, add a second, minimal test function (Method POST, and repeat later with GET):
POST https://www.phantocite.com/api/echo
{
"name": "echo_test",
"description": "Diagnostic echo. Whenever the user asks to run the echo test, call this with the exact text they give you.",
"parameters": {
"type": "object",
"properties": {
"message": { "type": "string", "description": "The exact text the user asked to send." }
},
"required": ["message"]
}
}2. In a chat, say: “Run the echo test with the message PHANTOCITE-TRANSPORT-TEST-42 and show me the raw response.”
3. Read the response — three possible outcomes, no ambiguity:
PHANTOCITE-TRANSPORT-TEST-42appears inbody_first_2000_charsorquery_params→ the platform CAN transmit arguments. The problem is specific to the verify function's schema — compare how the two functions are configured.- The response shows an empty body and no query params → proof the platform sends nothing, even for a single flat string. Nothing on the API side can fix that; take the echo output to EduGenAI support.
- No response / an error → the platform never even made the HTTP call.
For comparison, this is what a working transport looks like from a terminal:
curl -s -X POST https://www.phantocite.com/api/echo \
-H "Content-Type: application/json" \
-d '{"message":"PHANTOCITE-TRANSPORT-TEST-42"}'What data passes through your server?
Because the extension calls your deployment, it helps to be precise about what actually travels there — and what does not.
| Data | Where it goes |
|---|---|
| The paper's full text, the student's name, and the citing sentences | Stay inside EduGenAI. The assistant keeps the citing sentences for its own misquote reasoning; they are never sent to your endpoint. |
| Reference metadata only — title, authors, year, DOI, journal, volume, issue, pages | Sent to your /api/verify_batch endpoint, and (titles/DOIs) on to OpenAlex for the lookup. |
| Any LLM key | Not involved server-side at all — the reasoning runs on EduGenAI's model. |
One caveat about the OpenAlex Premium key: it is used per-request, never stored, and scrubbed from any error message this app returns. But when your endpoint calls OpenAlex, the key travels as a query parameter on the outbound URL — so if you run behind an egress proxy that logs full outbound URLs, it could appear there. If that matters, don't set a Premium key (the tool works fine without one) or use a proxy that redacts query strings.
Notes & limits
- No LLM key is stored or used by the extension; OpenAlex is free and needs no key.
- The metadata check compares against OpenAlex's canonical record. Occasionally OpenAlex's year differs from the printed year (e.g. online-first vs issue year) — treat a lone year mismatch as a prompt to double-check, not a verdict.
- The misquote judgement is based on the abstract only, so a claim supported by the full text but not the abstract may read as uncertain. These are leads for a human reviewer.
- Batch requests are capped at 200 references.