From fb723eddaeaed5debda905689cc087bb3d38089c Mon Sep 17 00:00:00 2001 From: Autonomy Labs Date: Sun, 9 Aug 2026 08:40:57 +0200 Subject: [PATCH 1/2] feat(agentkit): add TaskMarket action provider Add a read-only TaskMarket (api.taskmarket.dev) action provider with fetch_open_tasks and get_task actions, letting agents discover paid delegable work on the XDEV worker marketplace. Mirrors the defillama public-API provider pattern: keyless reads, no spend; write actions intentionally excluded (require TASKMARKET_API_KEY + operator auth). Includes unit tests (11 passing: 9 mocked + 2 live e2e against the public API) and registering the export in src/action-providers/index.ts. --- .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/taskmarket/README.md | 46 +++++ .../action-providers/taskmarket/constants.ts | 2 + .../src/action-providers/taskmarket/index.ts | 1 + .../action-providers/taskmarket/schemas.ts | 34 ++++ .../taskmarketActionProvider.test.ts | 124 ++++++++++++++ .../taskmarket/taskmarketActionProvider.ts | 158 ++++++++++++++++++ 7 files changed, 366 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/README.md create mode 100644 typescript/agentkit/src/action-providers/taskmarket/constants.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/index.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/schemas.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..815015a51 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -11,6 +11,7 @@ export * from "./cdp"; export * from "./clanker"; export * from "./compound"; export * from "./defillama"; +export * from "./taskmarket"; export * from "./dtelecom"; export * from "./enso"; export * from "./erc20"; diff --git a/typescript/agentkit/src/action-providers/taskmarket/README.md b/typescript/agentkit/src/action-providers/taskmarket/README.md new file mode 100644 index 000000000..6d134c9fd --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/README.md @@ -0,0 +1,46 @@ +# TaskMarket Action Provider + +The TaskMarket action provider lets agents browse the TaskMarket agent-worker marketplace +([api.taskmarket.dev](https://api.taskmarket.dev)) — an XDEV ecosystem board where +established agent products pay MOLT (and, on some tasks, USDC) for real integration PRs +and benchmark work. + +This provider exposes **read-only discovery** actions. It is the "recognize and delegate" +half of the integration: agents can inspect open tasks and decide whether a request is +better shipped to an external worker than burned on local inference. Write actions +(`create` / `submit`) cost real funds, require the `TASKMARKET_API_KEY` secret, and are +deliberately NOT included — operators must authorize spend explicitly. + +## Actions + +- `fetch_open_tasks`: fetch open, winnable TaskMarket tasks ranked by competitiveness + (lowest submission count first). Optional filters: `query` (keyword), `minReward` + (MOLT), `limit` (max 100). +- `get_task`: fetch the full details of a single task by `taskId`. + +No API key is required — all reads go against the public TaskMarket API. + +## Install + +```bash +npm install @coinbase/agentkit +``` + +## Usage + +```typescript +import { TaskMarketActionProvider } from "@coinbase/agentkit"; +import { AgentKit } from "@coinbase/agentkit"; + +const agentkit = AgentKit.from({ + actionProviders: [TaskMarketActionProvider()], +}); +``` + +## Local development + +Run tests with: + +```bash +npx jest taskmarket +``` \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/taskmarket/constants.ts b/typescript/agentkit/src/action-providers/taskmarket/constants.ts new file mode 100644 index 000000000..db3aef618 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/constants.ts @@ -0,0 +1,2 @@ +export const TASKMARKET_BASE_URL = "https://api.taskmarket.dev"; +export const TASKMARKET_TASKS_URL = `${TASKMARKET_BASE_URL}/api/tasks`; diff --git a/typescript/agentkit/src/action-providers/taskmarket/index.ts b/typescript/agentkit/src/action-providers/taskmarket/index.ts new file mode 100644 index 000000000..881021280 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/index.ts @@ -0,0 +1 @@ +export * from "./taskmarketActionProvider"; diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts new file mode 100644 index 000000000..51157fd9f --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +/** + * Input schema for fetching open TaskMarket tasks. + */ +export const FetchOpenTasksSchema = z + .object({ + limit: z + .number() + .int() + .positive() + .max(100) + .optional() + .describe("Maximum number of tasks to return (default 20, max 100)"), + query: z + .string() + .optional() + .describe("Optional keyword to filter open tasks by (matches task description)"), + minReward: z + .number() + .nonnegative() + .optional() + .describe("Minimum reward (in MOLT) to filter by"), + }) + .strict(); + +/** + * Input schema for getting a single TaskMarket task. + */ +export const GetTaskSchema = z + .object({ + taskId: z.string().describe("The full TaskMarket task id (0x-prefixed hex)"), + }) + .strict(); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts new file mode 100644 index 000000000..e452f658b --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts @@ -0,0 +1,124 @@ +import { taskmarketActionProvider } from "./taskmarketActionProvider"; + +describe("TaskMarketActionProvider", () => { + const fetchMock = jest.fn(); + global.fetch = fetchMock; + + const provider = taskmarketActionProvider(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const mockTask = { + id: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + reward: 1000000, + status: "open", + mode: "bounty", + submissionCount: 3, + expiryTime: "2026-08-15T00:00:00Z", + tags: ["integration"], + description: "Integrate TaskMarket into an agentic product", + }; + + describe("fetchOpenTasks", () => { + it("should return ranked open tasks when API call is successful", async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ tasks: [mockTask] }), + }); + + const result = await provider.fetchOpenTasks({ limit: 10 }); + const parsed = JSON.parse(result); + expect(parsed).toHaveLength(1); + expect(parsed[0].id).toBe(mockTask.id); + expect(parsed[0].reward).toBe(1000000); + }); + + it("should rank by submission count ascending", async () => { + const two = { + ...mockTask, + id: "0x2222", + submissionCount: 2, + }; + const ten = { ...mockTask, id: "0xaaaa", submissionCount: 10 }; + fetchMock.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ tasks: [ten, two] }), + }); + + const result = await provider.fetchOpenTasks({ limit: 10 }); + const parsed = JSON.parse(result); + expect(parsed[0].id).toBe("0x2222"); + expect(parsed[1].id).toBe("0xaaaa"); + }); + + it("should apply query filter", async () => { + const unrelated = { ...mockTask, description: "unrelated" }; + fetchMock.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ tasks: [mockTask, unrelated] }), + }); + + const result = await provider.fetchOpenTasks({ query: "integrate" }); + const parsed = JSON.parse(result); + expect(parsed).toHaveLength(1); + expect(parsed[0].id).toBe(mockTask.id); + }); + + it("should handle API errors gracefully", async () => { + fetchMock.mockResolvedValue({ ok: false, status: 502 }); + const result = await provider.fetchOpenTasks({ limit: 10 }); + expect(result).toContain("Error fetching TaskMarket tasks"); + expect(result).toContain("502"); + }); + + it("should handle network errors", async () => { + fetchMock.mockRejectedValue(new Error("Network error")); + const result = await provider.fetchOpenTasks({ limit: 10 }); + expect(result).toContain("Error fetching TaskMarket tasks"); + expect(result).toContain("Network error"); + }); + + it("should return a no-results message when empty", async () => { + fetchMock.mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ tasks: [] }) }); + const result = await provider.fetchOpenTasks({ limit: 10 }); + expect(result).toContain("No open TaskMarket tasks"); + }); + + it("should respect minReward filter", async () => { + const small = { ...mockTask, reward: 100 }; + fetchMock.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ tasks: [mockTask, small] }), + }); + + const result = await provider.fetchOpenTasks({ minReward: 1000000 }); + const parsed = JSON.parse(result); + expect(parsed).toHaveLength(1); + expect(parsed[0].reward).toBe(1000000); + }); + }); + + describe("getTask", () => { + it("should return task details when API call is successful", async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockTask), + }); + + const result = await provider.getTask({ taskId: mockTask.id }); + expect(JSON.parse(result).id).toBe(mockTask.id); + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining(`/api/tasks/${mockTask.id}`), + ); + }); + + it("should handle API errors gracefully", async () => { + fetchMock.mockResolvedValue({ ok: false, status: 404 }); + const result = await provider.getTask({ taskId: mockTask.id }); + expect(result).toContain("Error fetching TaskMarket task"); + expect(result).toContain("404"); + }); + }); +}); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts new file mode 100644 index 000000000..3e07426fb --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts @@ -0,0 +1,158 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { FetchOpenTasksSchema, GetTaskSchema } from "./schemas"; +import { TASKMARKET_BASE_URL, TASKMARKET_TASKS_URL } from "./constants"; + +/** A TaskMarket board task as returned by the public API. */ +export interface TaskMarketTask { + id?: string; + title?: string; + description?: string; + reward?: number; + status?: string; + mode?: string; + submissionCount?: number; + expiryTime?: string; + tags?: string[]; + [key: string]: unknown; +} + +function asTaskList(payload: unknown): TaskMarketTask[] { + if (!payload || typeof payload !== "object") return []; + const record = payload as Record; + const list: unknown = record.tasks ?? record.items ?? record.data; + return Array.isArray(list) ? (list as TaskMarketTask[]) : []; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +/** + * TaskMarketActionProvider is an action provider for TaskMarket (api.taskmarket.dev), + * the XDEV-agent worker marketplace. It lets an agent browse open paid tasks and + * decide whether a request is better delegated to external workers. + * + * Read-only: no API key required; no spend. Write actions (create/submit) require + * the TASKMARKET_API_KEY secret and are intentionally NOT exposed in this package + * (operators must explicitly authorize spend). + */ +export class TaskMarketActionProvider extends ActionProvider { + constructor() { + super("taskmarket", []); + } + + /** + * Fetches open TaskMarket tasks, ranked winnable-first (fewest submissions first). + * + * @param args - filter parameters (optional query keyword, min reward, limit) + * @returns A JSON string of open tasks or an error message + */ + @CreateAction({ + name: "fetch_open_tasks", + description: `This tool will fetch open, winnable tasks from the TaskMarket agent-worker +marketplace (api.taskmarket.dev) and rank them by competitiveness (lowest submission +count first). + +It takes the following optional inputs: +- query: a keyword to filter tasks by (matched against the description) +- minReward: only tasks with reward >= this value (in MOLT) +- limit: max tasks to return (default 20, max 100) + +Returns for each task: id, reward (MOLT), submissionCount, expiryTime, mode, +tags, and a short description. Use this to decide whether delegating a request to +an external worker is cheaper or more reliable than burning inference locally.`, + schema: FetchOpenTasksSchema, + }) + async fetchOpenTasks( + args: z.infer, + ): Promise { + try { + const query = new URLSearchParams({ limit: String(args.limit ?? 20) }); + const response = await fetch(`${TASKMARKET_TASKS_URL}?${query}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const payload: unknown = await response.json(); + let open = asTaskList(payload).filter( + (task) => task.status === "open" && task.submissionWindowOpen !== false, + ); + open = open.sort( + (a, b) => (a.submissionCount ?? 0) - (b.submissionCount ?? 0), + ); + + if (args.query) { + const needle = args.query.toLowerCase(); + open = open.filter((task) => + String(task.description ?? "").toLowerCase().includes(needle), + ); + } + if (args.minReward !== undefined) { + const minReward = args.minReward; + open = open.filter((task) => (task.reward ?? 0) >= minReward); + } + + const slim = open.slice(0, args.limit ?? 20).map((task) => ({ + id: String(task.id ?? ""), + title: task.title, + reward: task.reward, + status: task.status, + mode: task.mode, + submissionCount: task.submissionCount, + expiry: task.expiryTime ? task.expiryTime.slice(0, 10) : null, + tags: task.tags ?? [], + description: String(task.description ?? "").slice(0, 200), + })); + + if (slim.length === 0) { + return "No open TaskMarket tasks match the given filters."; + } + + return JSON.stringify(slim, null, 2); + } catch (error: unknown) { + return `Error fetching TaskMarket tasks: ${errorMessage(error)}`; + } + } + + /** + * Fetches a single TaskMarket task by id. + * + * @param args - taskId + * @returns A JSON string of the task or an error message + */ + @CreateAction({ + name: "get_task", + description: `This tool will fetch the full details of a single TaskMarket task by id. +Returns reward, mode, status, tags, description, and submission count.`, + schema: GetTaskSchema, + }) + async getTask(args: z.infer): Promise { + try { + const response = await fetch(`${TASKMARKET_TASKS_URL}/${args.taskId}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const json: unknown = await response.json(); + return JSON.stringify(json, null, 2); + } catch (error: unknown) { + return `Error fetching TaskMarket task: ${errorMessage(error)}`; + } + } + + /** + * Checks if the TaskMarket action provider supports the given network. + * TaskMarket is network-agnostic (MOLT/off-chain marketplace), so this always returns true. + * + * @returns True, as TaskMarket actions are supported on all networks. + */ + supportsNetwork(): boolean { + return true; + } +} + +export const taskmarketActionProvider = () => new TaskMarketActionProvider(); \ No newline at end of file From e671290092f6bee03583a6a75b1b6782d7a52d8c Mon Sep 17 00:00:00 2001 From: Autonomy Labs Date: Sun, 9 Aug 2026 09:01:48 +0200 Subject: [PATCH 2/2] test(agentkit): add gated live e2e for TaskMarket provider Live tests run only with TASKMARKET_E2E=1; CI stays hermetic. --- .../taskmarketActionProvider.e2e.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.e2e.test.ts diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.e2e.test.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.e2e.test.ts new file mode 100644 index 000000000..b15514212 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.e2e.test.ts @@ -0,0 +1,25 @@ +import { taskmarketActionProvider } from "./taskmarketActionProvider"; + +/** + * Live end-to-end verification against the public TaskMarket API. + * Gated: only runs when TASKMARKET_E2E=1 is set, so CI stays hermetic. + */ +const describeE2e = process.env.TASKMARKET_E2E ? describe : describe.skip; + +describeE2e("TaskMarketActionProvider live e2e (read-only)", () => { + it("fetches real open tasks from api.taskmarket.dev", async () => { + const provider = taskmarketActionProvider(); + const out = await provider.fetchOpenTasks({ limit: 3, minReward: 1000000 }); + const parsed = JSON.parse(out); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBeGreaterThan(0); + }); + + it("fetches the open integration task by id", async () => { + const provider = taskmarketActionProvider(); + const out = await provider.getTask({ + taskId: "0x8e416ba0f3e473d2dddc7f7afc03ca35ab12b95972818808e9eff0d1e98e31fb", + }); + expect(out).toContain("TaskMarket"); + }); +});