Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
46 changes: 46 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/README.md
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TASKMARKET_BASE_URL = "https://api.taskmarket.dev";
export const TASKMARKET_TASKS_URL = `${TASKMARKET_BASE_URL}/api/tasks`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./taskmarketActionProvider";
34 changes: 34 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/schemas.ts
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -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");
});
});
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
Loading
Loading