Skip to the page

Convex

Ask for live web data in plain words from your Convex app and watch the matching records fill your own tables, each value with the page it came from. The @specstream/convex component prices each request and starts it only within the maximum your code sets.

SpecStream for ConvexAPI & cloud platforms

What you need

  • A Convex project on Convex 1.43 or later, with Node.js 20 or later.
  • A SpecStream account and an API key. A free account starts with $10 of work to use within 30 days and needs no card.
  1. 1. Install the component

    Install the @specstream/convex (opens in a new tab) package in your Convex project.

    Terminal
    npm install @specstream/convex
  2. 2. Add the component to your app

    In convex/convex.config.ts, declare the key as a typed env value and mount the component. The optional httpPrefix serves the webhook route at <your-site>/specstream/webhook.

    convex/convex.config.ts
    import { defineApp } from "convex/server";
    import { v } from "convex/values";
    import specstream from "@specstream/convex/convex.config";
    const app = defineApp({
    env: {
    SPECSTREAM_API_KEY: v.string(),
    SPECSTREAM_WEBHOOK_SECRET: v.optional(v.string()),
    },
    });
    app.use(specstream, {
    // Mounts the webhook route at <your-site>/specstream/webhook. Optional.
    httpPrefix: "/specstream/",
    env: {
    SPECSTREAM_API_KEY: app.env.SPECSTREAM_API_KEY,
    SPECSTREAM_WEBHOOK_SECRET: app.env.SPECSTREAM_WEBHOOK_SECRET,
    },
    });
    export default app;
  3. 3. Create a key and set it on your deployment

    In the SpecStream workspace, go to Agents & API, then API keys, then Create API key. Label it for the app.

    Reading and pricing a request need Create and edit requests and Read saved requests. Starting work needs Prepare work and follow-ups and Authorize paid work. Following the results needs Read results and their sources. Add Read account and balance to use the component's account call.

    Set a Total spending limit too. It stacks with the maximum your code passes to each call, and both apply. Copy the key when it is shown, then set it on your deployment.

    Terminal
    npx convex env set SPECSTREAM_API_KEY ss_live_your_key
    npx convex dev
  4. 4. Find records from an action

    Call the component from your own actions. findList reads the request, prices it, and starts it only when the most it can cost is within maxPriceUsd.

    It returns started with a listId, needs_edit with readAs when SpecStream couldn't price the request as written, or over_maximum when the price was above your maximum. Only started runs anything. A request can name its own count, such as "50 roofers in Columbus rated 4.5 or better", and rows sets the count you are pricing for, from 1 to 1000.

    convex/lists.ts
    import { v } from "convex/values";
    import { SpecStreamClient } from "@specstream/convex";
    import { action } from "./_generated/server";
    import { components } from "./_generated/api";
    const specstream = new SpecStreamClient(components.specstream);
    export const findFoodBanks = action({
    args: {},
    handler: async (ctx) => {
    const result = await specstream.findList(ctx, {
    request: "food banks in Texas, with their annual revenue",
    rows: 100,
    maxPriceUsd: 15,
    });
    if (result.status === "needs_edit") return result.message; // add the place, or what every row must meet
    if (result.status === "over_maximum") return result.message; // nothing ran
    return result.listId; // subscribe to this
    },
    });
  5. 5. Read the rows

    listRows and getList are plain Convex queries. Your client subscribes to them like any other data, and the row count climbs as results arrive.

    Confirm the caller owns the list before you return its rows.

    convex/lists.ts and your React component
    // convex/lists.ts
    import { v } from "convex/values";
    import { paginationOptsValidator } from "convex/server";
    import { query } from "./_generated/server";
    export const rows = query({
    args: { listId: v.string(), paginationOpts: paginationOptsValidator },
    handler: async (ctx, args) => {
    // Confirm the caller owns this list first.
    return await specstream.listRows(ctx, args);
    },
    });
    // React
    const { results, status, loadMore } = usePaginatedQuery(
    api.lists.rows,
    { listId },
    { initialNumItems: 50 },
    );

What a row holds

Each row has entityId, ordinal, sourceUrl, observedAt, and cells. Each cell has a label, a display value, and the sourceUrl and observedAt of the page it was read from.

A value a source doesn't publish has an empty display and an absence such as "Not published". flattenRow(row) returns one field per column label, ready for a table or a prompt.

Show the price first

When a person should see the price before anything runs, call quote to read and price the request, then approve to start the list. Pass the quote's maximumCents as confirmMaxCents. The amount approved is then the amount shown.

Approving the same quote twice starts one list, and an expired quote runs nothing. Add an idempotencyKey so a double click is one start.

Run code when the work finishes

Pass onComplete: internal.lists.onFinished to findList or approve and your mutation runs once when the list reaches completed, partial, failed, canceled, or expired. partial means SpecStream finished with fewer rows than you asked for.

The mutation receives listId, specstreamListId, status, rowCount, requestedCount, error, and any context you passed, untouched.

Finish faster with a webhook

By default the component asks SpecStream for progress every few seconds and backs off to every fifteen. With the webhook route mounted, SpecStream tells your deployment the moment a list finishes, and the component asks only slowly as a backstop.

Webhooks belong to your SpecStream account, so register one once with registerWebhook, passing https://your-name.convex.site/specstream/webhook. It returns a hookId and a secret. Store the secret with npx convex env set SPECSTREAM_WEBHOOK_SECRET, then pass mode: "webhook" to findList or approve.

With the secret set, every delivery must carry a valid Specstream-Signature, signed over <timestamp>.<body> and refused after five minutes. A delivery only wakes a list the component already tracks, and rows always come from the SpecStream API with your key.

SpecStream delivers webhooks only to public https:// addresses on port 443, so a deployment on localhost uses the default polling.

Cancel, resume, and clean up

cancel stops a running list and keeps the rows found so far. refresh reads the latest status and rows now, and re-reading never duplicates rows. resume picks tracking back up, and deleteList removes a list and its stored rows from Convex while the list stays in your SpecStream workspace.

Other reads are listLists, getRow, getListBySpecstreamId, and getQuote. Every list has a link that opens it in your SpecStream workspace.

Keep access in your app

Components can't see ctx.auth. Authentication, authorization, and rate limiting belong in your app. Gate every function that spends on a signed-in user, and keep an owner table that maps each listId to a user. Look the owner up before any read, cancel, or delete.

Errors and stopped tracking

Errors are ConvexErrors with data: { code, message, status? }. The component raises specstream_missing_api_key, specstream_empty_request, specstream_bad_rows, specstream_bad_maximum, specstream_unknown_quote, specstream_quote_expired, specstream_quote_not_approvable, specstream_approval_mismatch, specstream_bad_idempotency_key, and specstream_request_failed. SpecStream's own codes arrive as specstream_<code>. A rate limit or a server error is retried three times first.

After a key stops working, or after 400 status requests, the component marks the list failed with a message starting "Stopped tracking this list". The list may still be running at SpecStream. Fix the key and call resume.

Convex limits a document to 1MB. A row that would exceed it loses its full detail set first, then its raw values, and keeps its displayed text and its page.

Test without spending

Register the component with convex-test through specstream.register(t), imported from @specstream/convex/test, and stub fetch for api.specstream.ai. Set SPECSTREAM_API_URL to point the component at another host, such as a mock server.

Cost

The component is free. Work is paid from your SpecStream balance, only for rows that match, and a search with no matches costs nothing. The component never spends on its own: every list starts from a price, and a mismatched or expired approval runs nothing.

Reading rows is free. Pricing has the current rates.

Remove

Remove app.use(specstream, ...) from convex/convex.config.ts, delete the env values from your deployment, and revoke the key under Agents & API. Lists you made stay in your SpecStream workspace. Connect your tools shows where else SpecStream works. Questions go to [email protected].

More

SpecStream for Convex covers what it does in Convex on one page. Every other tool has its guide in Connect your tools. You see the most a request can cost before it starts, and only the matches come out of your balance. Billing has the details. For help, email [email protected].