Build your own agent

๐Ÿงช Alpha, and still moving. This guide does exactly one thing, end to end: it puts an OpenAI-shaped API behind x402 and gets it paid in stablecoin. Not a platform, not a standard โ€” a working proof you can run today, check with the checker, and take apart. Expect the surroundings to keep changing: @x402/evm shifts between versions and the client tooling is thin. Known limitations is the honest list.

By the end of this page you'll have an HTTP endpoint that answers OpenAI-style chat requests and charges a fraction of a cent in USDC per message โ€” no API keys, no accounts, no invoices.

Who this is for

A backend developer comfortable with Node and TypeScript who already has (or can put together) an LLM endpoint. No prior x402 or crypto-payments experience assumed โ€” the one concept you need is explained below, and everything deeper is linked out rather than re-taught here.

What you'll need

  • An OpenAI-compatible LLM to proxy โ€” Mistral, an OpenAI key, a local model, anything that speaks /chat/completions.
  • Node + TypeScript, and the two SDK packages: npm install @x402/core @x402/evm. The snippets below target v2.20 (check scw_js/package.json for what we actually run) โ€” batch-settlement is young and its APIs still move between minor versions, so pin what you test against.
  • An EVM wallet (two keys: one to receive funds, one off-chain signer โ€” explained in step 2).
  • A place to store channel state โ€” Redis, or a file for a single instance. The SDK ships both.
  • An x402 facilitator that supports batch-settlement (a public one, or your own).
  • A scheduled job (cron) โ€” this is how you actually collect the money.

How the payment works

When someone calls your endpoint without paying, you reply 402 Payment Required plus a header describing how to pay. Their client pays in USDC (a dollar stablecoin) and retries. One blockchain transaction per chat message would be far too slow and expensive, so payment uses a channel: the user locks funds once in an on-chain escrow, each message is then just a tiny signed IOU (a voucher), and you redeem the accumulated vouchers on-chain later in a single batch. That scheme is called batch-settlement.

Batch-settlement payment flow

You don't implement the protocol yourself โ€” the @x402/evm SDK does that. New to x402? These are the canonical docs:

The API you expose

One route: POST /, in the OpenAI chat-completions format โ€” so it looks like any other LLM API and existing types just work.

Loading the live specโ€ฆ

Two rules the schema can't express on its own: usage must be in your response because the charge is computed from it, and stream: true must be rejected (settlement needs the final token count, which needs the whole reply).

Try it right now

Send an unpaid request to our live agent. You can run this verbatim โ€” it costs nothing, and the 402 it returns is exactly what your own endpoint has to produce:

curl -i -X POST https://llm-agent.fretchen.eu/ \
    -H "Content-Type: application/json" \
    -d '{"model":"mistral-large-latest","messages":[{"role":"user","content":"Hello"}]}'

The interesting part of the response โ€” note receiverAuthorizer and withdrawDelay: those are the fields enhancePaymentRequirements injects for you in step 3.

HTTP/2 402
  access-control-expose-headers: Payment-Required, X-Payment, PAYMENT-REQUIRED
  payment-required: eyJ4NDAyVmVyc2lvbiI6MiwicmVzb3VyY2UiOnsidXJs...   โ† same JSON, base64

  {
    "x402Version": 2,
    "resource": { "url": "/", "description": "AI Assistant chat message", "mimeType": "application/json" },
    "accepts": [{
      "scheme": "batch-settlement",
      "network": "eip155:8453",
      "amount": "3000",                                    โ† ceiling: 0.003 USDC
      "asset":  "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo":  "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
      "maxTimeoutSeconds": 120,
      "extra": {
        "name": "USD Coin", "version": "2",
        "receiverAuthorizer": "0xF9B7...2c93",             โ† injected by the SDK
        "withdrawDelay": 86400                             โ† injected by the SDK
      }
    }, { "network": "eip155:84532", "...": "the same, for Base Sepolia" }]
  }

You can't get past this point with curl โ€” the next request has to carry a signed payment, which means opening a channel. For a real, runnable paid round-trip, use the buyer notebook: it drives a server through deposit โ†’ voucher โ†’ verify โ†’ settle over plain HTTP, on testnet by default (USE_MAINNET = false).

โ†’ sc_llm_x402_buyer.ipynb (Deno notebook)

After payment your endpoint returns the ordinary OpenAI completion object shown in the response table above โ€” usage included. Errors use the OpenAI shape, { error: { message, type, code } }, with model_not_found for an unadvertised model and stream_unsupported for a streaming request.

Build it, step by step

Each step shows the requirement and the real code from our implementation (sc_llm_x402.ts and x402_server.ts), trimmed for readability and with our infrastructure swapped for portable equivalents. Each snippet ends with a line-anchored link to the original, so you can always diff against something that runs in production.

Snippets use a plain-object handler โ€” an event in, a { statusCode, headers, body } out. That's what serverless platforms hand you, and it maps to Express or Fetch handlers in a couple of lines.

Where everything goes

This is the whole thing, with a slot for each step. Read it once โ€” every later snippet fills exactly one of these slots, so you always know whether code belongs at module scope (runs once) or inside the handler (runs per request).

// โ•โ•โ• server.ts โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• MODULE SCOPE (once)
  import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server";
  import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/server";
  import { RedisChannelStorage } from "@x402/evm/batch-settlement/server/redis-storage";
  import { privateKeyToAccount } from "viem/accounts";
  import { createClient } from "redis";

  const MODELS = ["mistral-large-latest"];        // must match the enum you publish (step 6)
  const NETWORKS = ["eip155:10", "eip155:8453"];  // Optimism + Base; add "eip155:84532" to test
  const RESOURCE = { url: "https://your-agent.example/", description: "chat", mimeType: "application/json" };

  // Your price ceiling per message, in USDC atomic units (6 decimals). Pick it as
  // (tokens you expect per message) x (your OUTPUT rate) โ€” pricing the whole estimate at the
  // dearer output rate guarantees the ceiling is never an underestimate. 3000 = 0.003 USDC.
  const MAX_PRICE_PER_MESSAGE = "3000";

  const redis = createClient({ url: process.env.REDIS_URL });
  await redis.connect();

  const USDC = { /* ... */ };                                    // โ”€โ”€ step 3
  const { resourceServer, scheme } = setupX402();                // โ”€โ”€ step 2

  export async function handler(event) {
    // โ”€โ”€ preflight: browsers send OPTIONS before a paid POST (step 3)
    if (event.httpMethod === "OPTIONS") return { statusCode: 204, headers: CORS, body: "" };

    // โ”€โ”€ step 1: parse + validate the OpenAI body
    // โ”€โ”€ step 3: no payment? advertise how to pay, return 402
    // โ”€โ”€ step 4: verify โ†’ run your model โ†’ settle โ†’ 200
  }

  // โ•โ•โ• helpers (module scope) โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // json() / CORS                                                โ”€โ”€ step 1
  // respond402()                                                 โ”€โ”€ step 3
  // extractPaymentPayload(), settlementHeaders(), priceFromUsage() โ”€โ”€ step 4

  // โ•โ•โ• two more files โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // cron.ts        โ€” claim your money on a schedule              โ”€โ”€ step 5
  // openapi.json   โ€” discovery doc + the route that serves it    โ”€โ”€ step 6

1Start from an OpenAI-shaped endpoint

If you already proxy an OpenAI-compatible model, you're done with this step โ€” just validate the input and reject streaming. Nothing here is x402-specific yet.

Show the request validation + the json/CORS helpers
// Every response needs these โ€” the assistant is a browser client, and
  // Allow-Headers must cover PAYMENT-SIGNATURE or the preflight fails.
  const CORS = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Content-Type, PAYMENT-SIGNATURE, X-PAYMENT",
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Expose-Headers": "Payment-Required",
    "Content-Type": "application/json",
  };

  function json(statusCode, payload, extraHeaders = {}) {
    return { statusCode, headers: { ...CORS, ...extraHeaders }, body: JSON.stringify(payload) };
  }

  // โ”€โ”€ in the handler โ”€โ”€
  const body = JSON.parse(event.body);

  if (body.stream === true) {
    return json(400, {
      error: { message: "Streaming is not supported.", type: "invalid_request_error", code: "stream_unsupported" },
    });
  }
  if (!Array.isArray(body.messages) || body.messages.length === 0) {
    return json(400, { error: { message: "'messages' must be a non-empty array.", type: "invalid_request_error" } });
  }
  if (!MODELS.includes(body.model)) {
    return json(404, {
      error: { message: `Unknown model '${body.model}'.`, type: "invalid_request_error", code: "model_not_found" },
    });
  }

Ours: sc_llm_x402.ts:169-215 (validation), sc_llm_x402.ts:78-113 (CORS + error helpers).

2Wire up the x402 resource server

Create the resource server once at startup and register the batch-settlement scheme for each network you accept. Two keys are involved: the receiver address that funds go to, and a separate authorizer key that signs channel configuration off-chain (it never needs funding).

For FACILITATOR_URL, pick a facilitator that advertises batch-settlement on your network โ€” check its /supported endpoint. Public options are listed in the facilitator list (e.g. Solvador), or you can run your own.

Show the setup (fills the setupX402() slot)
function setupX402() {
    const facilitator = new HTTPFacilitatorClient({ url: process.env.FACILITATOR_URL });
    const authorizer = privateKeyToAccount(process.env.RECEIVER_AUTHORIZER_PRIVATE_KEY);

    const resourceServer = new x402ResourceServer(facilitator);

    const scheme = new BatchSettlementEvmScheme(process.env.RECEIVER_ADDRESS, {
      storage: new RedisChannelStorage({ client: redis }),   // or FileChannelStorage for one instance
      receiverAuthorizerSigner: {
        address: authorizer.address,
        signTypedData: (params) => authorizer.signTypedData(params),
      },
      onchainStateTtlMs: 5_000,      // keep low, or a user's first message after depositing can fail
      withdrawDelay: 86_400,         // must be >> your claim interval (step 5)
    });

    for (const network of NETWORKS) resourceServer.register(network, scheme);
    return { resourceServer, scheme, facilitator };
  }

Ours: x402_server.ts:89-109 โ€” same thing, with S3 for storage.

3Answer unpaid requests with a 402

Build the payment requirements โ€” the "here's how to pay me" description โ€” and return them as a 402. The SDK verifies and settles for you, but the HTTP transport โ€” which headers, encoded how โ€” is yours to write. It's three ~15-line helpers, shown in this step and the next.

The USDC constants you'll need:

const USDC = {
    "eip155:10":    { address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", name: "USD Coin", version: "2" }, // Optimism
    "eip155:8453":  { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "USD Coin", version: "2" }, // Base
    "eip155:84532": { address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", name: "USDC",     version: "2" }, // Base Sepolia
  };
The EIP-712 domain name is "USD Coin" on mainnet but "USDC" on testnet. Mix them up and payment verification fails silently, with no useful error.
Don't skip enhancePaymentRequirements. It injects the receiverAuthorizer and withdrawDelay fields the client needs to build a deposit โ€” you saw both in the curl output above. The same enhancement has to be applied again at verify time (step 4); a bare, un-enhanced object gets every deposit rejected with receiver_authorizer_mismatch.
Show the requirements builder + the 402 body
// One enhanced accepts[] entry for a single network. Step 4 calls this again for the
  // network the client picked โ€” which is why it takes `network` and `amount` as arguments
  // instead of hard-coding them.
  async function requirementsFor(network, amount) {
    const usdc = USDC[network];                         // address + EIP-712 name/version
    const base = {
      scheme: "batch-settlement",
      network,
      amount,                                           // USDC atomic units (6 decimals)
      asset: usdc.address,
      payTo: process.env.RECEIVER_ADDRESS,
      maxTimeoutSeconds: 120,                           // must be identical in the 402 and at verify
      extra: { name: usdc.name, version: usdc.version },
    };
    return scheme.enhancePaymentRequirements(
      base,
      { x402Version: 2, scheme: "batch-settlement", network, extra: base.extra },
      [],                                               // x402 extensions โ€” none, unless you use them
    );
  }

  // The 402 body advertises EVERY network you accept.
  async function build402Body() {
    const accepts = await Promise.all(
      NETWORKS.map((network) => requirementsFor(network, MAX_PRICE_PER_MESSAGE)),
    );
    return { x402Version: 2, resource: RESOURCE, accepts };
  }

  // โ”€โ”€ in the handler โ”€โ”€
  const payment = extractPaymentPayload(event.headers);
  if (!payment) return respond402(await build402Body());

Ours: x402_server.ts:127-171 (the 402 body), sc_llm_x402.ts:244-258 (the handler branch).

Two different objects, one confusing name. The 402 body is { x402Version, resource, accepts: [...] }. What you pass to verifyPayment/settlePayment is a single entry from that array, for the one network the client chose. Passing the whole 402 body there is the most common way to get this wrong.
Show the 402 transport helper (respond402)
// The 402 body must ALSO go, base64-encoded, into the Payment-Required header โ€”
  // browser clients read the header, not the body. CORS already exposes it (step 1),
  // which is exactly what the checker at the bottom of this page verifies.
  function respond402(body402) {
    return json(402, body402, {
      "Payment-Required": Buffer.from(JSON.stringify(body402)).toString("base64"),
    });
  }

Ours: x402_server.ts:233-255.

4Verify, answer, settle

This is the step that turns a plain endpoint into a paid one. Verify the voucher, run your inference, then settle โ€” and note the trick: you verify against a ceiling but settle the amount actually used, so a short reply costs the user less.

Show the handler flow
// 0. Which network did the client choose? It's in the payload โ€” you can't build the
  //    verify requirements without it, and you must reject anything you don't serve.
  const network = payment.accepted?.network;
  if (!network || !NETWORKS.includes(network)) {
    return json(402, { error: { message: "Unsupported network for this payment." } });
  }

  // 1. Rebuild the SAME enhanced requirements โ€” for that one network, at the ceiling price.
  //    (Not the 402 body: one accepts[] entry. See the note in step 3.)
  const requirements = await requirementsFor(network, MAX_PRICE_PER_MESSAGE);

  // 2. Verify the client's voucher.
  const check = await resourceServer.verifyPayment(payment, requirements);
  if (!check.isValid) {
    // Re-emit through the SDK so it can attach corrective channel state the client needs to
    // resync (passing the failed payload is what triggers that) โ€” a hand-rolled 402 body
    // breaks the client's automatic retry.
    const corrective = await resourceServer.createPaymentRequiredResponse(
      [requirements],
      RESOURCE,
      check.invalidReason,
      check.payer ? { payer: check.payer } : undefined,
      undefined,
      payment,
    );
    return respond402(corrective);
  }

  // 3. Do the actual work.
  const completion = await callYourModel(body.messages);

  // 4. Settle the amount actually used โ€” same requirements, smaller amount.
  const settlement = await resourceServer.settlePayment(payment, {
    ...requirements,
    amount: priceFromUsage(completion.usage),
  });
  if (!settlement.success) {
    return json(402, { error: { message: `Settlement failed: ${settlement.errorReason ?? "unknown"}` } });
  }

  // 5. 200 + the settlement receipt (json() already merges CORS).
  return json(200, completion, settlementHeaders(settlement));

Ours: sc_llm_x402.ts:260-268 (network check), sc_llm_x402.ts:287-362 (verify + corrective 402), sc_llm_x402.ts:396-418 (settle + response).

Show the other two transport helpers (extract + settlement headers)
// The payment arrives base64-encoded in the PAYMENT-SIGNATURE header.
  // Returns null when there's no payment โ€” that's the "send a 402" case in step 3.
  function extractPaymentPayload(headers) {
    const header = headers["payment-signature"] ?? headers["Payment-Signature"];
    if (!header) return null;
    try {
      return JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
    } catch {
      return null;
    }
  }

  // The settlement receipt goes back base64-encoded in the Payment-Response header
  // (merge these into your 200 response's headers).
  function settlementHeaders(settlement) {
    return { "Payment-Response": Buffer.from(JSON.stringify(settlement)).toString("base64") };
  }

Ours: x402_server.ts:257-282 and x402_server.ts:300-308.

Show priceFromUsage (tokens โ†’ USDC atomic units)
// Rates are quoted per 1,000,000 tokens; USDC has 6 decimals โ€” the two 1e6
  // factors cancel exactly, so no separate decimals conversion is needed.
  // Keep rates as integer fractions (num/den) to stay exact in bigint math.
  const INPUT_PER_M  = { num: 1n, den: 2n };  // $0.50 per 1M prompt tokens
  const OUTPUT_PER_M = { num: 3n, den: 2n };  // $1.50 per 1M completion tokens

  function priceFromUsage(usage) {
    const p = BigInt(usage.prompt_tokens);
    const c = BigInt(usage.completion_tokens);
    const cost =
      (p * INPUT_PER_M.num * OUTPUT_PER_M.den + c * OUTPUT_PER_M.num * INPUT_PER_M.den) /
      (INPUT_PER_M.den * OUTPUT_PER_M.den);
    // Never settle above the ceiling you verified against in the 402.
    const max = BigInt(MAX_PRICE_PER_MESSAGE);
    return (cost > max ? max : cost).toString();
  }

Ours: llm_service.ts:215-231 (the rate maths) and sc_llm_x402.ts:72-76 (the ceiling clamp).

5Collect your money

Per-message settlements are bookkeeping only โ€” no funds move. A scheduled job redeems the accumulated vouchers on-chain. Skip this and you never get paid. It's genuinely this short:

Show the claim job (cron.ts โ€” a separate entry point)
// Same setupX402() as step 2 โ€” this runs in its own process, on a schedule
  // (we use every 12h). Must run far more often than the withdrawDelay you set in
  // step 2, or a channel can be withdrawn before you claim it.
  const { scheme, facilitator } = setupX402();

  for (const network of NETWORKS) {
    // Pass the token explicitly: without it the SDK falls back to its own stablecoin
    // registry, which has no Optimism entry and throws. See the challenges section.
    const manager = scheme.createChannelManager(facilitator, network, USDC[network].address);
    const { claims, settle } = await manager.claimAndSettle();
    console.log({ network, claims: claims.length, settled: settle !== undefined });
  }

Ours: llm_x402_cron.ts:62-75 (a 12-hourly scheduled function).

6Publish discovery + allow the browser in

Finally, make yourself findable. Serve an OpenAPI document at GET /openapi.json containing "x-service-type": "llm/v1", your x-payment-info, and an ownership proof. Your CORS setup from step 1 already covers the browser side. One consistency rule: the model enum in your published schema must match the MODELS array your handler validates against (step 1) โ€” the spec is a promise, the validation enforces it.

Show the discovery doc + the route that serves it
// openapi.json (excerpt)
  {
    "openapi": "3.1.0",
    "x-service-type": "llm/v1",
    "servers": [{ "url": "https://your-agent.example" }],
    "x-discovery": { "ownershipProofs": ["0x<signature>"] },
    "paths": { "/": { "post": { "x-payment-info": {
      "protocols": ["x402"],
      "price": { "mode": "dynamic", "currency": "USD", "min": "0", "max": "0.003" }
    } } } },
    "components": { "schemas": {
      "LLMChatRequest":  { /* model enum must match MODELS from step 1 */ },
      "LLMChatResponse": { /* ... */ }
    } }
  }

And the route that serves it โ€” the first branch of your handler:

import openapiSpec from "./openapi.json" with { type: "json" };

  // โ”€โ”€ in the handler, before the POST logic โ”€โ”€
  if (event.httpMethod === "GET" && (event.path ?? "").replace(/^\/+/, "") === "openapi.json") {
    // Patch the live ceiling in, so the published price can't drift from what you charge.
    // MAX_PRICE_PER_MESSAGE is atomic units ("3000"); the spec wants decimal USD ("0.003").
    const spec = structuredClone(openapiSpec);
    spec.paths["/"].post["x-payment-info"].price.max = (Number(MAX_PRICE_PER_MESSAGE) / 1e6).toString();
    return json(200, spec);
  }

Ours: sc_llm_x402.ts:120-134 (with an exact bigint formatter, x402_server.ts:290-298, instead of the float division above).

Show how to sign the ownership proof
// One-off: sign your bare origin (scheme + host, no path, no trailing slash)
  // and paste the signature into x-discovery.ownershipProofs.
  import { privateKeyToAccount } from "viem/accounts";

  const account = privateKeyToAccount(process.env.RECEIVER_PRIVATE_KEY);
  const signature = await account.signMessage({ message: "https://your-agent.example" });
  console.log(signature);

Ours: sign_ownership_proof.ts.

Test it

Start on Base Sepolia (eip155:84532) โ€” same code path, no real money. Fund your test wallet from the Circle faucet, and use the testnet USDC constants from step 3.

While developing, the checker below can point straight at http://localhost:3000 โ€” browsers treat localhost as a secure context, so a page on https can still reach it (current Chrome and Firefox). Just remember your CORS headers apply locally too.

Expect one red step on testnet. The compatibility floor requires a payment option on Optimism or Base mainnet, so the "meets the floor" check stays red until you add eip155:10 or eip155:8453. Either one is enough. Everything above it โ€” discovery, service type, the 402 challenge โ€” should already be green.

The first real payment. Point the buyer notebook at your server and run it top to bottom. It opens a channel, sends a paid message, and prints the settlement โ€” the fastest way to see deposit โ†’ voucher โ†’ verify โ†’ settle actually working, and it stays on testnet unless you set USE_MAINNET = true.

Then: pay yourself from a browser. Once the checker passes on mainnet, open the assistant, open Use a different agent, paste your URL, and send one real message. That exercises the whole path โ€” deposit, voucher, verify, settle โ€” from a real client. Being honest: this is currently the only ready-made batch-settlement client there is (see Known limitations).

Check your endpoint

Paste your URL. This runs exactly the checks the assistant runs before it will talk to an endpoint โ€” down to reading the base64 Payment-Required header from step 3 โ€” and tells you which ones fail.

Known limitations

Documented openly โ€” these are rough edges of a young ecosystem, not of your code.

Few facilitators support batch-settlement
Most public facilitators (including Coinbase's) only support the simpler exact scheme. A handful run batch-settlement on mainnet โ€” Solvador and this project โ€” so your options are limited today. The scheme is standard; its EVM wire binding is still defined by the @x402/evm code rather than a ratified spec, which is why adoption is thin.
A plain OpenAI SDK can't pay it
There's no drop-in client helper for batch-settlement yet, so callers must hand-wire the payment side (channel storage, deposit, voucher signing). The OpenAI-shaped body keeps it familiar to read, but a plain Authorization: Bearer request just hits the 402 and stops.
One message at a time per channel
A channel processes requests serially โ€” a concurrent second request is rejected until the first settles, and an interrupted request can hold the lock for up to ~2 minutes. Clients handle it by waiting and retrying.
Chains outside the SDK's stablecoin registry need an explicit token
Your payment option must be on Optimism (eip155:10) or Base (eip155:8453). Optimism used to be impossible: it isn't in @x402/evm's stablecoin registry, and enhancePaymentRequirements() resolved the asset from that registry rather than from your requirements, so one Optimism entry threw and took the whole 402 down with it. Fixed in 2.20, which honours the asset you pass. The registry gap itself is still there, so anything that falls back to it needs the token spelled out โ€” notably createChannelManager(facilitator, network, token), whose third argument is optional but throws on Optimism when omitted.
Claim timing matters
Your withdrawDelay must stay well above how often your claim job runs, or a channel can become withdrawable before you claim it โ€” losing you earned revenue. We use a 24h delay against a 12h job.

Where this is going

The endpoint contract is stable; what's filling in is the ecosystem around it โ€” a drop-in client, more facilitators, more agents. The assistant already lets you point it at any compatible agent by URL; a curated picker only makes sense once there are enough of them to list.

Built one, or want to be listed when a picker ships? Reach out at fretchen.dev@proton.me or on GitHub. The full reference implementation lives in scw_js/README.md.

Feedback

Stuck on a step, or built one? Leave a note โ€” it helps the next builder as much as it helps us.

Comments

Loading comments...