Tutorial · Content intelligence

Build an information-gain research queue with the API

Help an editor decide what a page should contribute next. Turn originality signals into explicit research tasks, keep API findings separate from editorial proposals, and require evidence before making new claims.

Give your editor a concrete research decision

An SEO content tool becomes more useful when it can explain both what a page is missing and what the page could contribute. This tutorial builds the second part: a research queue your application can attach to a content brief or editorial workflow.

We will use the best SEO API comparison. Its saved report contains measurable originality signals, a repeated-content example, and an empty list of uncovered questions. That combination makes a practical test of whether an integration can help without overstating the data.

The finished program reads a Standard or Deep report and optional editorial proposals. It writes a JSON brief containing source context, available signals, research tasks, and their next required step. Choose Node.js 20+ or Python 3.10+.

Understand the three different signals

SignalQuestion it helps answerWhat it does not establish
Entity coverageWhich relevant topics occur on comparison pages but are missing from this page?That adding the term will contribute new information.
Originality and shared passagesWhich passages are flagged as shared or duplicative in the report?Plagiarism, factual correctness, or a Google ranking score.
Information-gain opportunitiesWhat unanswered questions or distinctive evidence might be worth investigating?That a proposed answer is true or nobody has covered it elsewhere.

Use the entity-coverage guide to repair useful omissions. Use this workflow to plan a contribution a reader could learn from: a measured result, an original example, a comparison method, or a carefully supported answer.

Originality signals from the saved report
{
  "score": 42,
  "grade": "Moderately original",
  "sentence_analysis": {
    "original": 120,
    "shared": 145,
    "duplicative": 23,
    "total_scored": 288
  },
  "information_gain": {
    "potential_uncovered_topics_for_information_gain": [],
    "unique_data_points": {
      "your_count": 0,
      "page1_average": 11.1,
      "examples": []
    }
  }
}

The report returns an originality score of 42. It also reports zero distinctive data points on the target and a page-one average of 11.1. That is a reason to inspect the evidence on the page, not a target of “add twelve statistics.”

The uncovered-question array is present and empty. Your UI should say “No uncovered questions returned.” If the section is absent, say “Information-gain questions unavailable.” The distinction matters for Lite reports, partial data, and schema changes.

Make a different decision for each finding

Observed findingEditorial decisionReason
A shared passage defines what an SEO data API returns.Keep an accurate definition if it helps the reader.Common explanatory wording is not automatically a problem worth rewriting.
A shared-content finding discusses a social-media platform’s scheduling and pricing.Dismiss it for this comparison unless the reader’s requirements make it relevant.Three competitors sharing a passage does not make it useful for choosing an SEO API.
No uncovered questions; zero detected unique data points.Propose an original provider-reliability experiment.Measured completion behavior could help developers make a concrete integration decision.

The experiment is our editorial proposal. The API did not return it as an uncovered question. Preserve that distinction in your data model with source: "editorial_proposal".

Choose research by the decision it can improve

Before prioritizing a task, require three answers: Which reader decision would this change? What evidence could support the answer? Can we collect it honestly within the available effort? A useful, narrow measurement can be more valuable than a broad claim about which provider is “best.”

Prepare a report and a research proposal

For the saved example, download the selected report fields as report.json and save the proposal below. Keep them beside the script. The report excerpt contains every field consumed by this example.

Download the saved report
curl --fail --output report.json \
  https://api.on-page.ai/docs/report-explorer/example.json

For your own completed scan, retrieve its report with your API key:

Retrieve your completed report
curl --fail --output report.json \
  "https://api.on-page.ai/v1/jobs/$ONPAGE_JOB_ID/result" \
  -H "Authorization: Bearer $ONPAGE_API_KEY"

Need to submit and wait for a scan first? Follow the content-optimization client. Use Standard or Deep for originality fields; the brief builder still handles absent sections without turning them into zeroes.

proposals.json
[
  {
    "question": "How reliably does each shortlisted SEO API complete the same small page-analysis workload?",
    "readerDecision": "Choose an API that fits the integration’s required data, turnaround time, and budget.",
    "method": "Define required output fields first. Run the same authorized URL set and region on comparable endpoints. Record accepted job IDs, completion outcomes, elapsed time, retry counts, and actual charges. Keep product tiers separate and disclose differences in scope.",
    "evidenceRequired": [
      "Raw request and response artifacts with credentials removed",
      "The fixed test corpus and required-field checklist",
      "A results table including failures and incomplete jobs",
      "Measured timing and billing records",
      "A written explanation of sample size and endpoint differences"
    ]
  }
]

Proposals are optional. Running with only report.json builds tasks from actual uncovered questions. With this report, that produces zero tasks. Adding the proposal above produces one task that still needs evidence.

Build a queue that keeps uncertainty visible

research-brief.mjs
import { readFileSync, writeFileSync } from "node:fs";

export function buildResearchBrief(report, proposals = []) {
  if (!report || typeof report !== "object" || Array.isArray(report)) throw new Error("Expected a report object.");
  if (!Array.isArray(proposals)) throw new Error("Proposals must be an array.");
  const gain = report.originality?.information_gain;
  const warnings = [];
  const questionField = gain?.potential_uncovered_topics_for_information_gain;
  const available = Array.isArray(questionField);
  const questions = available ? [...new Set(questionField.filter(q => {
    if (typeof q !== "string" || !q.trim()) { warnings.push("Ignored an invalid uncovered question."); return false; }
    return true;
  }).map(q => q.trim()))] : [];
  if (!available) warnings.push("Uncovered questions unavailable; do not interpret this as an empty result.");
  const validNumber = value => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
  const data = gain?.unique_data_points;
  const tasks = questions.map((question, i) => ({
    id: "api-" + (i + 1), source: "api_question", question,
    readerDecision: null, method: null, evidenceRequired: [], status: "needs_review",
  }));
  for (const [i, p] of proposals.entries()) {
    if (!p || ["question", "readerDecision", "method"].some(k => typeof p[k] !== "string" || !p[k].trim()) ||
        !Array.isArray(p.evidenceRequired) || !p.evidenceRequired.length || p.evidenceRequired.some(v => typeof v !== "string" || !v.trim())) {
      throw new Error("Each editorial proposal needs question, readerDecision, method, and a nonempty evidenceRequired list.");
    }
    tasks.push({ id: "editor-" + (i + 1), source: "editorial_proposal", question: p.question.trim(),
      readerDecision: p.readerDecision.trim(), method: p.method.trim(), evidenceRequired: p.evidenceRequired.map(v => v.trim()), status: "needs_evidence" });
  }
  return {
    jobId: report.jobId ?? null, targetUrl: report.meta?.url ?? null,
    keyword: report.meta?.target_keyword ?? null, region: report.meta?.location ?? null,
    signals: { originalityScore: validNumber(report.originality?.score),
      uncoveredQuestions: { available, count: available ? questions.length : null },
      uniqueDataPoints: { yourCount: validNumber(data?.your_count), page1Average: validNumber(data?.page1_average) } },
    tasks, warnings,
  };
}

// Reading saved files never starts a scan or changes the source article.
const [reportPath, proposalsPath, outputPath = "research-brief.json"] = process.argv.slice(2);
if (reportPath) {
  const report = JSON.parse(readFileSync(reportPath, "utf8"));
  const proposals = proposalsPath ? JSON.parse(readFileSync(proposalsPath, "utf8")) : [];
  const brief = buildResearchBrief(report, proposals);
  writeFileSync(outputPath, JSON.stringify(brief, null, 2) + "\n", { mode: 0o600 });
  console.log("Saved " + brief.tasks.length + " research tasks to " + outputPath);
}
Build the brief from saved JSON
node research-brief.mjs report.json proposals.json research-brief.json

The program preserves a real zero, represents an unavailable number as null, deduplicates returned questions, and warns when an array contains invalid entries. API questions start as needs_review; complete editorial proposals start as needs_evidence. Neither state means a claim is ready to publish.

Expected task and availability fields
{
  "signals": {
    "originalityScore": 42,
    "uncoveredQuestions": {
      "available": true,
      "count": 0
    },
    "uniqueDataPoints": {
      "yourCount": 0,
      "page1Average": 11.1
    }
  },
  "tasks": [
    {
      "id": "editor-1",
      "source": "editorial_proposal",
      "question": "How reliably does each shortlisted SEO API complete the same small page-analysis workload?",
      "status": "needs_evidence"
    }
  ],
  "warnings": []
}

The full output also preserves the job ID, target URL, keyword, region, reader decision, method, and required evidence. An application can render each task as an editor’s work item without asking a language model to infer which fields are observations and which are proposals.

Turn the proposal into a defensible contribution

For this comparison, define a small authorized URL corpus and the fields your integration needs. Test comparable endpoints on those same inputs. Record failures as carefully as successful completions.

CollectWhy the reader needs it
Required fields and endpoint/tier mappingA fast endpoint returning less information is not directly equivalent to a deeper analysis.
Accepted jobs, completed jobs, failed jobs, and unfinished jobsA completion-rate claim needs a denominator and an explicit treatment of unfinished work.
Submission-to-result elapsed time and retry countsReaders can assess application waiting time and operational complexity.
Actual charges and included workloadA cost comparison needs the same scope and a clear measurement basis.
Raw artifacts, sample size, and method limitationsAnother developer can inspect the evidence and judge how far the conclusion applies.

Store evidence references beside the task. Have a reviewer check the calculation, relevance, and wording before moving it into a draft. If the experiment has not been run, publish the methodology as a methodology; do not populate a results table with estimates presented as measurements.

Your finished addition could be a reproducible reliability comparison that answers a developer’s purchasing question. After publishing, rescan the same URL, keyword, and region. Inspect the new passage and report fields, keeping cohort and algorithm changes in view. A changed originality score alone does not demonstrate improved rankings or traffic.

Questions developers and editors will ask

Is an uncovered question guaranteed to be new?

No. It is a candidate relative to the analyzed material. Check the source pages and relevant external evidence before claiming novelty. A question can still be worth answering clearly even if an answer exists elsewhere.

What if the report returns no questions?

Keep that empty state. Review the data-point and shared-content evidence, then let an editor add a clearly labeled proposal. The saved example demonstrates exactly this path; the application does not manufacture API suggestions.

Should I rewrite every shared or duplicative sentence?

No. Definitions, necessary explanations, and common questions can overlap for good reasons. Inspect the passage and its role. Spend effort on useful specificity, original evidence, and attribution where needed.

Can an LLM complete the research tasks?

It can help refine a question, organize supplied evidence, or draft from verified results. Generated claims are not new measurements. Keep source artifacts and reviewer approval in your application’s workflow, and do not mark a task complete merely because text was generated.

How do I choose between a missing topic and original research?

Repair omissions that prevent readers from understanding or using the page first. Then prioritize research that improves an important decision and has a feasible evidence plan. A high-prevalence missing term and a low-prevalence novel contribution solve different problems.

You have built a research queue that explains what the API observed, what an editor proposed, and what evidence is still needed. Combine it with entity coverage for a content tool that helps a page become both more complete and more useful.