Compose

Each service is a building block. These are the canonical compositions used in production by OpenClaw agents on Helsinki.

Safe research workflow

The canonical pattern for any agent that accepts user-provided URLs or queries. Screen untrusted input → risk-check the URL → scrape → clean for LLM → validate output.

PromptGuard ($0.002) → LinkRisk ($0.005) → ScrapePay ($0.010) → MarkdownOpt ($0.005) → SchemaGate ($0.001)
        

Total cost per run: $0.023 USDC

import { McpClient } from "@modelcontextprotocol/sdk/client";

async function safeResearch(userQuery: string, targetUrl: string, schema: object) {
  // 1. Screen the user input for prompt injection
  const guard = await mcp.callTool("promptguard", {
    prompt: userQuery,
    sensitivity: "medium",
  });
  if (!guard.safe) throw new Error(`Input rejected: ${guard.risk}`);

  // 2. Risk-check the URL before visiting
  const risk = await mcp.callTool("linkrisk", { url: targetUrl });
  if (risk.risk_level === "high") throw new Error(`URL blocked: ${risk.flags.join(", ")}`);

  // 3. Scrape the page
  const page = await mcp.callTool("scrapepay", {
    url: targetUrl,
    format: "html",
  });

  // 4. Clean the HTML to markdown for the LLM
  const clean = await mcp.callTool("markdownopt", { html: page.content });

  // 5. Call your LLM with the clean content
  const llmOutput = await callLLM(userQuery, clean.markdown);

  // 6. Validate the LLM output matches your expected schema
  const valid = await mcp.callTool("schemagate", {
    response: JSON.stringify(llmOutput),
    schema,
  });
  if (!valid.valid) throw new Error(`LLM output invalid: ${valid.hint}`);

  return llmOutput;
}

Why this specific composition?

PromptGuard prevents jailbreak-style inputs from reaching your scraper or LLM. LinkRisk is cheap ($0.005) and catches obviously dangerous URLs before spending $0.01 on ScrapePay. MarkdownOpt's ~70% token reduction pays for itself on the first LLM call. SchemaGate catches hallucinated fields before they propagate downstream.

Notification pipeline

Monitor a page for changes, extract structured data, and alert when conditions are met. Works with any Telegram chat or public webhook endpoint.

ScrapePay ($0.010) → StructExtract ($0.002) → [condition check] → NotifyRelay /notify ($0.002)
        

Total cost per triggered alert: $0.014 USDC

async function monitorAndNotify(
  pageUrl: string,
  chatId: string,
  previousCount: number
) {
  // 1. Scrape the page as HTML for structured extraction
  const page = await mcp.callTool("scrapepay", {
    url: pageUrl,
    format: "html",
  });

  // 2. Extract structured tables from the HTML
  const data = await mcp.callTool("structextract", {
    html: page.content,
    extract: ["tables", "links"],
  });

  const currentCount = data.tables[0]?.rows.length ?? 0;

  // 3. Only notify if something changed
  if (currentCount > previousCount) {
    await mcp.callTool("notifyrelay_telegram", {
      chat_id: chatId,
      message: [
        `🆕 *${currentCount} items* found (was ${previousCount})`,
        `Page: ${pageUrl}`,
      ].join("\n"),
    });
    return currentCount;
  }

  return previousCount;
}

Why StructExtract before LLM?

Raw HTML from ScrapePay includes nav, ads, and boilerplate. Passing it to an LLM to count table rows costs 10–20× more in tokens than using StructExtract first. StructExtract costs $0.002 and returns clean, typed JSON. The LLM only sees what matters.

Document generation workflow

Fetch a web page, clean it to markdown, generate a PDF, and deliver it by email. Useful for report generation, document archival, and content delivery agents.

ScrapePay ($0.010) → MarkdownOpt ($0.005) → DocConvert-PDF ($0.005) → NotifyRelay /email ($0.005)
        

Total cost per document delivered: $0.025 USDC

async function generateAndDeliverReport(
  reportUrl: string,
  recipientEmail: string,
  subject: string
) {
  // 1. Fetch the live page
  const page = await mcp.callTool("scrapepay", {
    url: reportUrl,
    format: "html",
  });

  // 2. Clean to markdown (removes nav, ads, etc.)
  const clean = await mcp.callTool("markdownopt", { html: page.content });

  // 3. Convert markdown to PDF
  const pdf = await mcp.callTool("docconvert_pdf", {
    from: "md",
    to: "pdf",
    content: clean.markdown,
  });

  // 4. Email the PDF
  await mcp.callTool("notifyrelay_email", {
    to: recipientEmail,
    subject,
    body: `Report generated from ${reportUrl}\n\n[PDF attached — base64 below]\n${pdf.content}`,
  });

  return {
    pages: "(see PDF)",
    token_reduction: clean.reduction_pct,
    size_bytes: pdf.size_bytes,
  };
}

URL verification pipeline

Fast and authoritative URL safety checking for agents that handle user-supplied links. Start cheap, escalate to full sandbox only when needed.

LinkRisk ($0.005) → [if medium+] → LinkSafe ($0.010)
        

Cost: $0.005 for clean URLs, $0.015 for suspicious ones

async function verifyUrl(url: string) {
  // Fast heuristic check first
  const risk = await mcp.callTool("linkrisk", { url });

  if (risk.risk_level === "high") {
    return { safe: false, verdict: "blocked by heuristic", flags: risk.flags };
  }

  if (risk.risk_level === "medium") {
    // Escalate to full Playwright sandbox + VirusTotal
    const safe = await mcp.callTool("linksafe", { url });
    return {
      safe: safe.safe,
      verdict: safe.safe ? "clean" : "threat detected",
      threats: safe.threats,
      virustotal_positives: safe.virustotal_positives,
    };
  }

  return { safe: true, verdict: "clean (heuristic)", flags: risk.flags };
}

Cache and convert workflow

For multi-agent pipelines where several agents may need the same page. Cache at the session level, extract and convert once.

CacheServe ($0.001) → StructExtract ($0.002) → DocConvert-Text ($0.001)
        

Total cost: $0.004 per session (even if 10 agents need the same page)

async function cachedFetchAndConvert(url: string) {
  // Fetch with 1-hour cache — subsequent calls return the cached version
  const cached = await mcp.callTool("cacheserve", {
    url,
    ttl_seconds: 3600,
  });

  if (cached.cached) {
    console.log(`Cache hit from ${cached.cached_at}`);
  }

  // Extract tables and emails from the HTML
  const structured = await mcp.callTool("structextract", {
    html: cached.content,
    extract: ["tables", "emails", "links"],
  });

  // Convert the tables to CSV for a spreadsheet agent
  if (structured.tables.length > 0) {
    const csv = await mcp.callTool("docconvert_text", {
      from: "json",
      to: "csv",
      content: JSON.stringify(structured.tables[0]),
    });
    return csv.content;
  }

  return structured;
}