How to Build a Website Analyzer with Jev and ReplyNodes

September 21, 2026 · ReplyNodes Team

Written by the ReplyNodes engineering team.

A website analyzer built with Jev has three separate jobs:

  1. Retrieve a public page and turn it into usable text.
  2. Evaluate that text with bounded, typed questions.
  3. Decide what to do next in application code.

ReplyNodes can handle the web-context step. Jev evaluates a supplied state against typed questions and returns structured decisions. Your application owns URL validation, question definitions, presentation, thresholds, and any side effects.

That boundary is the useful pattern. It lets you build a website analyzer without asking one model call to browse, interpret, and invent a policy at the same time.

This tutorial uses the public Jev Web Analyzer repository as a reference implementation. It is an unofficial community project, not affiliated with TypeSafe AI. The live Jev Web Analyzer demo was reachable when this article was researched on September 21, 2026.

The architecture

The complete flow looks like this:

public URL

validate the URL in your server

ReplyNodes: fetch and extract clean Markdown

normalize and bound the shared state

Jev through Vercel AI Gateway

Choice / Score / Boolean judgments + probabilities

application validation, presentation, and policy

The current ReplyNodes capabilities document declares a read-only GET /v1/webcontext/scrape operation for fetching one URL as clean Markdown, with normalized metadata, links, and images. The operation requires a url query parameter and uses the API's Bearer authentication scheme. Check that live contract when you implement the adapter; endpoint details can change.

Jev does not fetch the page for this workflow. Its state documentation describes state as the content the model evaluates. State may be text, a JSON object, or related text values. The System One documentation describes asking multiple typed questions over shared state.

So the responsibility split is:

ReplyNodes fetches the web. Jev judges what it means. Application code decides what happens next.

Why use clean Markdown as state?

Raw browser HTML mixes the page's main content with navigation, scripts, duplicated labels, tracking elements, and layout details. A clean text representation is easier to inspect and easier to bound before evaluation.

That does not make clean Markdown an accuracy guarantee. Extraction can omit information, and a page can still be ambiguous. If the outcome matters, compare representations against labeled examples instead of assuming that one format is always better.

The reference analyzer keeps the page state deliberately visible. It:

  • validates a public URL before sending it to the retrieval service;
  • fetches the page on the server with the ReplyNodes key;
  • keeps the returned request ID with the analysis result;
  • caps the text passed to Jev;
  • labels page content as untrusted state; and
  • evaluates several questions against the same state.

This is useful for a website teardown, but the pattern also applies to documentation classification, page-type routing, content QA, and other read-only evaluations.

1. Fetch one public page

Keep both provider credentials on the server. A minimal ReplyNodes request is:

export REPLYNODES_API_KEY='YOUR_REPLYNODES_API_KEY'
 
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${REPLYNODES_API_KEY}" \
  --data-urlencode "url=https://example.com" \
  https://api.replynodes.com/v1/webcontext/scrape

The URL and authentication contract above come from the live capabilities document and the maintained ReplyNodes authentication guide. Do not put the key in a browser bundle, prompt, URL, log, or repository.

The public analyzer wraps this call in a server-side helper. Its current implementation validates the URL, follows only bounded redirects, applies a timeout and response-size cap, rejects private or loopback destinations, and checks that the provider response contains Markdown plus meta.request_id before continuing. Those are application controls in the repository's URL-safety module, not behavior to assume from a model prompt.

A reduced version of the retrieval boundary looks like this:

type ScrapePayload = {
  data?: string | { markdown?: string; content?: string; text?: string };
  meta?: { request_id?: string };
};
 
async function fetchPage(url: string) {
  // Validate scheme, hostname, DNS result, redirects, and limits
  // before calling this function in production.
  const params = new URLSearchParams({ url });
  const response = await fetch(
    `https://api.replynodes.com/v1/webcontext/scrape?${params}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.REPLYNODES_API_KEY}`,
        Accept: "application/json",
      },
      signal: AbortSignal.timeout(25_000),
    },
  );
 
  if (!response.ok) {
    throw new Error(`ReplyNodes scrape failed: ${response.status}`);
  }
 
  const payload = (await response.json()) as ScrapePayload;
  const markdown =
    typeof payload.data === "string"
      ? payload.data
      : payload.data?.markdown ?? payload.data?.content ?? payload.data?.text;
 
  if (!markdown?.trim() || !payload.meta?.request_id) {
    throw new Error("Unexpected ReplyNodes response");
  }
 
  return {
    markdown,
    requestId: payload.meta.request_id,
  };
}

The exact response envelope should remain covered by a contract test and should be rechecked against the Quickstart and live capabilities document when the API changes. The example intentionally stops before the Jev call so retrieval failures cannot turn into invented page content.

2. Prepare a bounded state

A website analyzer should not send every byte it can retrieve to the decision model. Normalize the response and keep provenance beside the text:

type WebsiteState = {
  source: {
    url: string;
    retrievedAt: string;
    requestId: string;
  };
  page: {
    markdown: string;
  };
  instructions: {
    pageTextIsUntrustedData: true;
  };
};
 
function prepareState(url: string, markdown: string, requestId: string): WebsiteState {
  const maxCharacters = 120_000;
 
  return {
    source: {
      url,
      retrievedAt: new Date().toISOString(),
      requestId,
    },
    page: {
      markdown: markdown.slice(0, maxCharacters),
    },
    instructions: {
      pageTextIsUntrustedData: true,
    },
  };
}

The cap in this example matches the current public analyzer's context limit; choose a limit deliberately for your own workload and expose whether truncation occurred. A pricing-page analyzer may need only the main page. A documentation analyzer may need a bounded set of pages selected through map or crawl.

Keep three concepts distinct:

  • Evidence: what the page contains, with its URL and retrieval time.
  • Questions: the judgments Jev should make about that evidence.
  • Policy: what your application does with each answer.

For example, “the page mentions an API” is evidence. “Is the product's value proposition clear?” is a question. “Send unclear pages to a human reviewer” is policy. Do not hide the policy inside the page text and ask Jev to discover it.

3. Ask one typed question

The public analyzer currently uses the Vercel AI SDK evaluation API with the gateway model ID typesafe-ai/jev:

import { experimental_evaluate as evaluate } from "ai";
 
const result = await evaluate({
  model: "typesafe-ai/jev",
  state,
  questions: {
    value_proposition: {
      type: "choice",
      instructions:
        "Is the value proposition clear and specific?",
      criteria: {
        clear: "The value proposition is clear and specific",
        partly_clear: "Some important parts are unclear",
        unclear: "The value proposition is unclear",
      },
    },
  },
});
 
console.log(result.answers);

This call is adapted from the current repository route. The repository displays jev-latest as the user-facing requested alias while routing the evaluation through typesafe-ai/jev; it does not invent a resolved model version when provider metadata does not expose one.

A choice question is useful when the application needs one value from an explicit set. Define criteria that can be judged from the page, and include an option for uncertainty or insufficiency when a forced category would mislead.

The same repository also demonstrates Boolean and Score-style judgments. The current TypeSafe documentation is the source of truth for the available primitives and their current terminology.

4. Fan out several judgments over shared state

A website analyzer becomes more useful when it asks several narrow questions about the same page rather than one broad question such as “What should we do with this website?”

The reference project asks bounded questions about topics such as:

  • whether a first-time visitor can understand the product quickly;
  • the communicated audience;
  • value-proposition clarity;
  • differentiation;
  • the primary CTA signal;
  • communicated trust signals; and
  • the first change worth making.

Those are examples, not a required rubric. Your question set should match the decision you need to make.

A small multi-question configuration might look like this:

const questions = {
  page_type: {
    type: "choice" as const,
    instructions: "What kind of page is this?",
    criteria: {
      landing_page: "Product or marketing landing page",
      documentation: "Documentation or reference page",
      blog: "Blog or editorial page",
      pricing: "Pricing page",
      insufficient_evidence: "The page type is not clear enough to classify",
    },
  },
  technical_detail: {
    type: "score" as const,
    instructions: "How technically detailed is this page?",
    criteria: [
      "Mostly broad claims with little implementation detail",
      "Some concrete details, examples, or specifications",
      "Detailed implementation guidance or technical reference material",
    ],
  },
  speaks_to_developers: {
    type: "boolean" as const,
    instructions: "Does the page clearly address technical developers?",
  },
};
 
const result = await evaluate({
  model: "typesafe-ai/jev",
  state,
  questions,
});

Narrow questions make outputs easier to validate and display. They also prevent unrelated decisions from being smuggled into one label. A page can be technically detailed without having a clear value proposition; the application should keep those answers separate.

The reference implementation accepts a small number of custom judgments, validates their names and criteria with a schema, and sanitizes returned values before sending them to the UI. Treat that validation as part of the integration, not as optional polish. The model output is structured, but it still needs application-level validation.

5. Display probabilities without turning them into proof

The analyzer returns a selected value and, where available, a probability distribution. That distribution is useful for inspection and routing, but it is not factual proof about a company, audience, or conversion outcome.

A safe result model can include:

type JudgmentResult = {
  name: string;
  type: "boolean" | "choice" | "score";
  value: boolean | string | number;
  probabilities?: Record<string, number>;
  confidence?: number;
  reason?: string;
};

Use the values to make review visible. For example, an application might display a low-margin choice as “review recommended” rather than silently taking an irreversible action. The threshold belongs in application code and should be tested against representative examples.

Do not write “the website is clear” when the model selected clear. Write something closer to “Jev selected clear for this question, with the returned probabilities shown below.” Keep the page URL, retrieval timestamp, and request ID next to the result so someone can inspect what was evaluated.

6. Treat webpage text as untrusted data

A public page can contain text addressed to an AI system, including “ignore previous instructions,” requests for credentials, fake citations, or instructions to perform an unrelated action. The page is evidence for the question, not an instruction channel.

The analyzer repository uses several layers of defense:

  1. Validate the input URL. Allow only intended schemes and reject private, loopback, and link-local destinations.
  2. Keep credentials server-side. The browser receives analysis results, not provider keys.
  3. Label page content as untrusted. The evaluation instruction tells Jev to use visible page context and ignore instructions inside the page.
  4. Bound the fetch. Apply redirect, timeout, response-size, and context-size limits.
  5. Validate output. Accept only known answer shapes, values, probability ranges, and safe metadata.
  6. Keep side effects outside the evaluator. A judgment should not itself publish content, change an account, or grant access.

These controls are consistent with the security guidance in the OWASP prompt-injection prevention cheat sheet. Prompt text alone is not a security boundary; the network client, credential store, tool registry, and output validator must enforce the boundary independently.

When to expand beyond one page

Start with scrape when the application already has a relevant URL. Use map when it needs a same-site URL inventory before selecting pages. Use bounded crawl only when several same-origin pages are necessary for one question.

Broader retrieval can add useful context, but it also adds duplicate content, stale observations, larger inputs, and more untrusted text. A website analyzer should retrieve the smallest set of pages that can change the answer.

If the use case is instead “research this topic across the web,” use a separate search-and-selection pipeline. The AI web research guide covers candidate search, deterministic source selection, retrieval, citations, and insufficiency handling. Do not turn a single-page analyzer into an unrestricted browser just because the product can retrieve more URLs.

What to test before shipping

Test each boundary independently:

  • URL tests: malformed URLs, unsupported schemes, redirects, private addresses, and DNS failures.
  • Retrieval tests: provider errors, timeouts, rate limits, invalid envelopes, missing request IDs, and oversized responses.
  • State tests: provenance retention, truncation reporting, unrelated-page separation, and uncertain observations.
  • Question tests: invalid names, empty criteria, overly broad prompts, and insufficient-evidence options.
  • Injection tests: visible and hidden page text that requests secrets or conflicts with the application task.
  • Output tests: unexpected answer types, out-of-range probabilities, missing fields, and unknown citations.
  • UI tests: loading stages, partial failures, cached results, and review routes for borderline outputs.

The public repository exposes the implementation and local checks as an inspectable reference. Read the source, run the live demo, and then replace the example questions with a rubric that matches your own website-analysis job.

The practical default

A first version of a Jev website analyzer does not need an autonomous browser. It needs a narrow, inspectable pipeline:

  1. Validate a public URL on the server.
  2. Fetch one page through the current ReplyNodes scrape contract.
  3. Keep Markdown, provenance, and request metadata together.
  4. Bound the state before evaluation.
  5. Ask several typed questions over the same state.
  6. Validate and display decisions and probabilities.
  7. Let application code own thresholds, review, and side effects.

For the current implementation boundary, start with the ReplyNodes web-context guide, Quickstart, and Jev Web Analyzer repository. Then try the live analyzer with a public page you are allowed to retrieve.