Skip to main content
Glama

Pricing Page Scan

check_pricing
Read-onlyIdempotent

Extract plan costs, free tiers, and plan names from a public pricing page to provide live evidence for quoting.

Instructions

Fetch a public pricing page and extract first-pass pricing signals before you quote plan costs, free tiers, or plan names. Use this when you have a likely pricing URL and need live evidence from the page itself. The tool uses heuristic text extraction from the fetched HTML, so it can miss JavaScript-rendered, logged-in, or heavily obfuscated pricing details. Results are cached for 5 minutes.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesPublic pricing-page URL to analyze, for example https://stripe.com/pricing.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesPricing page that was analyzed.
cachedNoTrue when the page body came from the 5-minute cache instead of a new fetch.
pricesFoundNoDistinct price-like strings extracted from the page text.
plansDetectedNoNormalized plan labels detected from the page text.
hasFreeOptionNoTrue when the page contains signals that a free plan or $0 option exists.
hasFreeTrialNoTrue when the page contains signals that a free trial exists.
pageLengthNoSize of the fetched page body in characters.
errorNoFetch or parsing error when the pricing page could not be analyzed.

Implementation Reference

  • src/index.ts:765-844 (registration)
    The check_pricing tool is registered via this.server.registerTool() with the name 'check_pricing'. This is the registration and handler combined — the tool is defined with inputSchema, outputSchema, and the async handler function all within the registerTool call.
    // ───────────────────────────────────────────────
    // PAID $0.02: check_pricing
    // ───────────────────────────────────────────────
    this.server.registerTool(
      "check_pricing",
      {
        title: "Pricing Page Scan",
        description:
          "Fetch a public pricing page and extract first-pass pricing signals before " +
          "you quote plan costs, free tiers, or plan names. Use this when you already " +
          "have a likely pricing URL and need a quick live scan of visible page text. " +
          "It returns price-like strings, heuristic plan labels, free or free-trial " +
          "signals, and cache information. It does not map prices to exact plans, " +
          "normalize currencies, execute checkout flows, or guarantee that a price " +
          "applies to a specific region or customer type. JavaScript-rendered, " +
          "logged-in, or heavily obfuscated pricing details can be missed. Results " +
          "are cached for 5 minutes.",
        inputSchema: {
          url: z.string().url().describe(
            "Public pricing or plans URL to analyze. Prefer the specific pricing page, for example https://stripe.com/pricing, rather than a generic homepage.",
          ),
        },
        outputSchema: {
          url: z.string().describe(
            "Pricing page that was analyzed.",
          ),
          cached: z.boolean().describe(
            "True when the page body came from the 5-minute cache instead of a new fetch.",
          ).optional(),
          pricesFound: z.array(z.string()).describe(
            "Distinct price-like strings extracted from the page text. These are not linked back to specific plans or billing conditions.",
          ).optional(),
          plansDetected: z.array(z.string()).describe(
            "Lowercased heuristic plan labels detected from the page text. They are useful hints, not authoritative plan identifiers.",
          ).optional(),
          hasFreeOption: z.boolean().describe(
            "True when the page contains signals that a free plan or $0 option exists somewhere on the page. This is a page-level signal, not proof that the offer is currently self-serve or globally available.",
          ).optional(),
          hasFreeTrial: z.boolean().describe(
            "True when the page contains signals that a free trial exists somewhere on the page.",
          ).optional(),
          pageLength: z.number().int().nonnegative().describe(
            "Size of the fetched page body in characters.",
          ).optional(),
          error: z.string().describe(
            "Fetch or parsing error when the pricing page could not be analyzed.",
          ).optional(),
        },
        annotations: readOnlyNetworkToolAnnotations,
      },
      async ({ url }) => {
        try {
          const { body, fromCache } = await cachedFetch(sql, url);
          // Extract pricing signals from page content
          const priceRegex = /\$\d[\d,]*(?:\.\d{2})?(?:\s*\/\s*(?:mo(?:nth)?|yr|year|user|seat|req|call|token))?/gi;
          const prices = [...new Set(body.match(priceRegex) || [])].slice(0, 20);
          const planRegex = /(?:free|starter|basic|pro|premium|enterprise|business|team|hobby|growth|scale)\s*(?:plan|tier)?/gi;
          const plans = [...new Set((body.match(planRegex) || []).map(p => p.trim().toLowerCase()))];
          const hasFree = /free\s*(?:plan|tier|forever|trial)|(?:\$0|0\.00)/i.test(body);
          const hasFreeTrial = /free\s*trial|try\s*(?:it\s*)?free|start\s*free/i.test(body);
    
          logUsage("check_pricing", true);
          return structuredToolResult({
            url,
            cached: fromCache,
            pricesFound: prices,
            plansDetected: plans,
            hasFreeOption: hasFree,
            hasFreeTrial: hasFreeTrial,
            pageLength: body.length,
          });
        } catch (e: unknown) {
          logUsage("check_pricing", false);
          return structuredToolResult({
            url,
            error: e instanceof Error ? e.message : String(e),
          });
        }
      }
    );
  • Input and output schema for check_pricing. Input: url (string). Output: url, cached, pricesFound, plansDetected, hasFreeOption, hasFreeTrial, pageLength, error.
    },
    outputSchema: {
      url: z.string().describe(
        "Pricing page that was analyzed.",
      ),
      cached: z.boolean().describe(
        "True when the page body came from the 5-minute cache instead of a new fetch.",
      ).optional(),
      pricesFound: z.array(z.string()).describe(
        "Distinct price-like strings extracted from the page text. These are not linked back to specific plans or billing conditions.",
      ).optional(),
      plansDetected: z.array(z.string()).describe(
        "Lowercased heuristic plan labels detected from the page text. They are useful hints, not authoritative plan identifiers.",
      ).optional(),
      hasFreeOption: z.boolean().describe(
        "True when the page contains signals that a free plan or $0 option exists somewhere on the page. This is a page-level signal, not proof that the offer is currently self-serve or globally available.",
      ).optional(),
      hasFreeTrial: z.boolean().describe(
        "True when the page contains signals that a free trial exists somewhere on the page.",
      ).optional(),
      pageLength: z.number().int().nonnegative().describe(
        "Size of the fetched page body in characters.",
      ).optional(),
      error: z.string().describe(
        "Fetch or parsing error when the pricing page could not be analyzed.",
      ).optional(),
    },
    annotations: readOnlyNetworkToolAnnotations,
  • The async handler function for check_pricing. It fetches the pricing URL, extracts price strings via regex, detects plan labels, checks for free/trial signals, and returns the structured result.
    async ({ url }) => {
      try {
        const { body, fromCache } = await cachedFetch(sql, url);
        // Extract pricing signals from page content
        const priceRegex = /\$\d[\d,]*(?:\.\d{2})?(?:\s*\/\s*(?:mo(?:nth)?|yr|year|user|seat|req|call|token))?/gi;
        const prices = [...new Set(body.match(priceRegex) || [])].slice(0, 20);
        const planRegex = /(?:free|starter|basic|pro|premium|enterprise|business|team|hobby|growth|scale)\s*(?:plan|tier)?/gi;
        const plans = [...new Set((body.match(planRegex) || []).map(p => p.trim().toLowerCase()))];
        const hasFree = /free\s*(?:plan|tier|forever|trial)|(?:\$0|0\.00)/i.test(body);
        const hasFreeTrial = /free\s*trial|try\s*(?:it\s*)?free|start\s*free/i.test(body);
    
        logUsage("check_pricing", true);
        return structuredToolResult({
          url,
          cached: fromCache,
          pricesFound: prices,
          plansDetected: plans,
          hasFreeOption: hasFree,
          hasFreeTrial: hasFreeTrial,
          pageLength: body.length,
        });
      } catch (e: unknown) {
        logUsage("check_pricing", false);
        return structuredToolResult({
          url,
          error: e instanceof Error ? e.message : String(e),
        });
      }
    }
  • cachedFetch helper used by check_pricing to fetch and cache the pricing page body with a 5-minute TTL.
    async function cachedFetch(sql: SqlTagFn, url: string): Promise<{ body: string; fromCache: boolean }> {
      const cached = cacheGet(sql, url);
      if (cached) return { body: cached, fromCache: true };
      const resp = await fetch(url, { headers: { "User-Agent": "GroundTruth/0.3" } });
      const body = await resp.text();
      if (resp.ok) cacheSet(sql, url, body);
      return { body, fromCache: false };
    }
  • cacheGet and cacheSet helpers used by cachedFetch for SQLite-backed caching of fetched page bodies.
    // --- Cache helpers using Durable Object SQLite tagged template ---
    function cacheGet(sql: SqlTagFn, key: string): string | null {
      const rows = sql<{ data: string; ts: number }>`SELECT data, ts FROM cache WHERE key = ${key}`;
      if (rows.length === 0) return null;
      const row = rows[0];
      if (Date.now() - row.ts > CACHE_TTL_MS) {
        sql`DELETE FROM cache WHERE key = ${key}`;
        return null;
      }
      return row.data;
    }
    
    function cacheSet(sql: SqlTagFn, key: string, data: string): void {
      if (data.length > MAX_CACHEABLE_BODY_BYTES) return;
    
      const ts = Date.now();
      try {
        sql`INSERT OR REPLACE INTO cache (key, data, ts) VALUES (${key}, ${data}, ${ts})`;
      } catch {
        // Cache writes should never take down a verification request.
      }
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond annotations (read-only, idempotent, open world), the description adds valuable behavioral details: heuristic text extraction, potential to miss dynamic content, and 5-minute caching. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences that are front-loaded with the tool's purpose and usage. Every sentence adds value, with no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter, annotations, and an output schema, the description covers purpose, usage, limitations, and caching. It is complete and leaves no obvious gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with a clear description for the url parameter. The description does not add additional meaning beyond the schema's 'Public pricing-page URL to analyze'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool fetches a public pricing page and extracts pricing signals. It specifies the resource (pricing page) and the action (extract signals), and distinguishes from siblings by being pricing-specific.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit use case: 'when you have a likely pricing URL and need live evidence.' It also mentions limitations about JS-rendered or obfuscated content, but does not explicitly state when not to use or provide direct alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/anish632/ground-truth-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server