feat(agentswarm): M5 — x402 billing wrapper for the swarm route

withX402() wraps the Web handler so unpaid calls get a standard HTTP 402
challenge (x402Version + accepts[]) and paid calls fall through to the agent.
Payment verification is injectable (host wires CoinPay/x402); CORS preflight
passes through untouched; custom challenge responses supported. README documents
the optional c0mpute.com GPU backend.

4 new tests; 38/38 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-25 17:48:12 +00:00
parent fbbb323477
commit 0e5a13f5fb
4 changed files with 137 additions and 0 deletions

View file

@ -89,6 +89,23 @@ const runner = createRubricRunner({
Pass the rubric per request: `{ "messages": [...], "rubric": "- one sentence\n- mentions chlorophyll" }`.
## Optional: run on community GPUs (c0mpute.com)
Inference is swappable. If a user connects their [c0mpute.com](https://c0mpute.com)
account (peer-shared GPUs), run agents/judges/routers on it instead of a hosted
provider. agentswarm does no OAuth — the host's c0mpute connector supplies the
credentials; this just consumes them:
```ts
import { createC0mputeModel, createDeepAgentRunner } from "@logicsrc/agentswarm";
// `connector` comes from the user connecting their c0mpute.com account
const model = await createC0mputeModel({ apiKey: connector.apiKey, model: "llama-3.1-70b" });
const runner = await createDeepAgentRunner({ model });
```
Opt-in: omit it and the default hosted provider is used. Requires `@langchain/openai`.
## Status
- **M1** ✓ core handler + injectable runner + deepagents adapter + x402 gate hook.

View file

@ -6,6 +6,8 @@
*/
export { createSwarmHandler } from "./handler.js";
export type { SwarmHandlerOptions } from "./handler.js";
export { withX402 } from "./x402.js";
export type { X402Options, X402Accept } from "./x402.js";
export { createDeepAgentRunner } from "./runner.js";
export type { DeepAgentRunnerOptions } from "./runner.js";
export { createSwarm, createLLMRouter } from "./swarm.js";

View file

@ -0,0 +1,61 @@
import { describe, expect, it, vi } from "vitest";
import { createSwarmHandler, withX402 } from "./index.js";
import type { SwarmRunner } from "./index.js";
function okRunner(): SwarmRunner {
return {
run: async (input) => ({
threadId: "t",
messages: [...input.messages, { role: "assistant", content: "ok" }],
output: "ok"
})
};
}
function post(): Request {
return new Request("http://localhost/swarm", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] })
});
}
const handler = createSwarmHandler({ runner: okRunner() });
const accepts = [{ scheme: "exact", network: "base", asset: "USDC", amount: 1000, payTo: "0xabc" }];
describe("withX402", () => {
it("passes paid requests through to the handler", async () => {
const paid = withX402(handler, { verify: () => true, accepts });
const res = await paid(post());
expect(res.status).toBe(200);
expect((await res.json()).output).toBe("ok");
});
it("returns a 402 x402 challenge when unpaid, without running the agent", async () => {
const verify = vi.fn(() => false);
const paid = withX402(handler, { verify, accepts });
const res = await paid(post());
expect(res.status).toBe(402);
const body = await res.json();
expect(body.x402Version).toBe(1);
expect(body.accepts).toEqual(accepts);
});
it("lets CORS preflight through without payment", async () => {
const verify = vi.fn(() => false);
const paid = withX402(handler, { verify, accepts });
const res = await paid(new Request("http://localhost/swarm", { method: "OPTIONS" }));
expect(res.status).toBe(204);
expect(verify).not.toHaveBeenCalled();
});
it("uses a custom challenge when provided", async () => {
const paid = withX402(handler, {
verify: () => false,
challenge: () => new Response("pay up", { status: 402, headers: { "x-pay": "url" } })
});
const res = await paid(post());
expect(res.status).toBe(402);
expect(res.headers.get("x-pay")).toBe("url");
});
});

View file

@ -0,0 +1,57 @@
/**
* x402 billing wrapper gate a swarm route behind payment. Wraps the Web
* handler so unpaid requests get a standard HTTP 402 challenge (the x402
* "accepts" shape) and paid requests fall through to the agent. The actual
* payment verification is injectable so a host wires it to CoinPay/x402.
*/
export interface X402Accept {
scheme: string;
network: string;
asset: string;
/** Price in the asset's minor units. */
amount: number;
/** Address / CoinPay account that receives payment. */
payTo?: string;
}
export interface X402Options {
/** Returns true when the request carries valid payment/authorization. */
verify: (request: Request) => boolean | Promise<boolean>;
/** Payment options advertised in the 402 challenge. */
accepts?: X402Accept[];
/** Fully custom 402 response; overrides `accepts`. */
challenge?: (request: Request) => Response | Promise<Response>;
}
const CORS_ORIGIN = { "access-control-allow-origin": "*" };
/**
* Wrap a `(Request) => Response` swarm handler so each call must be paid for.
* CORS preflights pass through untouched; everything else must satisfy `verify`
* or receives a 402 with the advertised payment options.
*/
export function withX402(
handler: (request: Request) => Promise<Response>,
options: X402Options
): (request: Request) => Promise<Response> {
return async function paid(request: Request): Promise<Response> {
if (request.method === "OPTIONS") {
return handler(request);
}
if (await options.verify(request)) {
return handler(request);
}
if (options.challenge) {
return options.challenge(request);
}
return new Response(
JSON.stringify({
x402Version: 1,
error: "payment_required",
accepts: options.accepts ?? []
}),
{ status: 402, headers: { "content-type": "application/json", ...CORS_ORIGIN } }
);
};
}