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
5 changes: 5 additions & 0 deletions typescript/.changeset/taskmarket-action-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added a Taskmarket action provider for public task discovery and explicitly confirmed USDC-escrowed task creation with configurable spending limits.
17 changes: 17 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,23 @@ const agent = createAgent({
</table>
</details>
<details>
<summary><strong>Taskmarket</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>list_tasks</code></td>
<td width="768">Discovers public Taskmarket work with reward, tag, mode, deadline, and pagination filters.</td>
</tr>
<tr>
<td width="200"><code>get_task</code></td>
<td width="768">Fetches a Taskmarket task specification, escrow state, submission window, and available next actions.</td>
</tr>
<tr>
<td width="200"><code>create_task</code></td>
<td width="768">Previews a task and, only after explicit confirmation, creates it with USDC escrow on Base mainnet under a configurable reward limit.</td>
</tr>
</table>
</details>
<details>
<summary><strong>x402</strong></summary>
<table width="100%">
<tr>
Expand Down
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 @@ -37,6 +37,7 @@ export * from "./onramp";
export * from "./vaultsfyi";
export * from "./x402";
export * from "./yelay";
export * from "./taskmarket";
export * from "./zerion";
export * from "./zerodev";
export * from "./zeroX";
Expand Down
20 changes: 20 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Taskmarket Action Provider

The `taskmarketActionProvider` adds Taskmarket discovery and explicit task-creation actions to AgentKit.

```ts
import { AgentKit, taskmarketActionProvider } from "@coinbase/agentkit";

const agentKit = new AgentKit({
walletProvider,
actionProviders: [taskmarketActionProvider({ maxRewardUsdc: 25 })],
});
```

The provider exposes three actions:

- `list_tasks` reads the public Taskmarket feed and supports reward, tag, mode, deadline, and pagination filters.
- `get_task` reads one task's specification, escrow state, submission window, and available next actions.
- `create_task` previews a task without making a request when `confirm` is `false`. It only submits the x402-paid creation request when `confirm` is `true`, the wallet is on Base mainnet, and the reward is within the configured `maxRewardUsdc` limit.

Taskmarket task creation pays the requested reward into escrow at creation time. Applications should present the exact description and reward to their user and set `confirm: true` only after explicit approval. Discovery and task detail reads never move funds.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Default Taskmarket API endpoint.
*/
export const TASKMARKET_BASE_URL = "https://api.taskmarket.dev";

/**
* Maximum default escrow amount allowed by the action provider.
*
* This is a guardrail for agents. Applications can provide a lower limit in
* the provider configuration, but cannot raise the default without opting in.
*/
export const DEFAULT_MAX_REWARD_USDC = 100;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./constants";
export * from "./schemas";
export * from "./taskmarketActionProvider";
105 changes: 105 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { z } from "zod";

/**
* Supported task lifecycle phases exposed by the Taskmarket API.
*/
export const TaskmarketPhaseSchema = z.enum([
"active",
"in_review",
"awaiting_settlement",
"resolved",
]);

/**
* Input schema for discovering open or historical Taskmarket work.
*/
export const ListTaskmarketTasksSchema = z
.object({
status: z
.string()
.optional()
.describe("Task status filter, for example 'open' or 'completed'. Defaults to 'open'."),
phase: TaskmarketPhaseSchema.optional().describe("Optional lifecycle phase filter."),
mode: z
.enum(["bounty", "claim", "pitch", "benchmark", "auction", "ALL"])
.optional()
.describe("Optional task competition mode filter."),
tags: z.array(z.string()).max(20).optional().describe("Optional task tags to match."),
minRewardUsdc: z
.number()
.nonnegative()
.finite()
.optional()
.describe("Optional minimum reward in whole USDC."),
maxRewardUsdc: z
.number()
.nonnegative()
.finite()
.optional()
.describe("Optional maximum reward in whole USDC."),
deadlineHours: z
.number()
.positive()
.finite()
.optional()
.describe("Only return tasks whose deadline is within this many hours."),
sort: z
.enum(["newest", "reward_desc", "reward_asc", "deadline_asc"])
.optional()
.describe("Result ordering. Defaults to newest."),
limit: z
.number()
.int()
.positive()
.max(100)
.optional()
.describe("Maximum number of tasks to return, up to 100."),
cursor: z.string().optional().describe("Pagination cursor returned by a previous call."),
})
.strict();

/**
* Input schema for retrieving one Taskmarket task by id.
*/
export const GetTaskmarketTaskSchema = z
.object({
taskId: z
.string()
.regex(/^0x[0-9a-fA-F]{64}$/, "Task id must be a 32-byte hex value.")
.describe("Taskmarket task id."),
})
.strict();

/**
* Input schema for creating a Taskmarket task.
*/
export const CreateTaskmarketTaskSchema = z
.object({
description: z.string().min(1).max(50_000).describe("The work specification for the task."),
rewardUsdc: z
.number()
.positive()
.finite()
.describe("Escrow reward in whole USDC; the same amount is paid through x402."),
durationHours: z
.number()
.positive()
.finite()
.max(24 * 365)
.describe("How many hours the task remains open."),
mode: z
.enum(["bounty", "claim", "pitch", "benchmark", "auction"])
.optional()
.describe("Task competition mode; defaults to bounty."),
tags: z.array(z.string()).max(20).optional().describe("Optional discovery tags."),
taskVisibility: z
.enum(["public", "unlisted", "private"])
.optional()
.describe("Task visibility; defaults to public."),
confirm: z
.boolean()
.describe(
"Must be true only after the user explicitly approves creating the task and escrow payment.",
),
})
.strict();
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
taskmarketActionProvider,
fromTaskmarketBaseUnits,
toTaskmarketBaseUnits,
} from "./taskmarketActionProvider";

describe("TaskmarketActionProvider", () => {
const fetchMock = jest.fn();
const provider = taskmarketActionProvider({
apiUrl: "https://taskmarket.example",
maxRewardUsdc: 5,
});

beforeEach(() => {
jest.resetAllMocks();
global.fetch = fetchMock;
});

it("converts USDC amounts to and from Taskmarket base units", () => {
expect(toTaskmarketBaseUnits(5)).toBe("5000000");
expect(toTaskmarketBaseUnits(0.125)).toBe("125000");
expect(fromTaskmarketBaseUnits("5500000")).toBe(5.5);
expect(fromTaskmarketBaseUnits("not-a-number")).toBeNull();
});

it("lists compact task summaries and applies discovery filters", async () => {
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue({
tasks: [
{
id: "0x" + "1".repeat(64),
description: "A public API task",
reward: "6000000",
netReward: "5550000",
status: "open",
phase: "active",
mode: "bounty",
tags: ["api"],
submissionCount: 2,
awardCount: 0,
},
],
hasMore: true,
nextCursor: "2026-08-18T00:00:00.000Z",
}),
});

const result = await provider.listTasks({
minRewardUsdc: 5,
tags: ["api"],
sort: "reward_desc",
limit: 10,
});
const parsed = JSON.parse(result);
const requestUrl = new URL(fetchMock.mock.calls[0][0]);

expect(parsed.success).toBe(true);
expect(parsed.tasks[0]).toMatchObject({
rewardUsdc: 6,
netRewardUsdc: 5.55,
descriptionTruncated: false,
});
expect(requestUrl.pathname).toBe("/api/tasks");
expect(requestUrl.searchParams.get("status")).toBe("open");
expect(requestUrl.searchParams.get("minReward")).toBe("5000000");
expect(requestUrl.searchParams.get("tags")).toBe("api");
expect(requestUrl.searchParams.get("sort")).toBe("reward_desc");
});

it("returns the full task detail", async () => {
const task = {
id: "0x" + "2".repeat(64),
description: "Inspect this task",
reward: "1000000",
pendingActions: [{ role: "worker", action: "submit" }],
};
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue(task),
});

const result = await provider.getTask({ taskId: task.id });

expect(JSON.parse(result)).toEqual({ success: true, task });
expect(fetchMock).toHaveBeenCalledWith(`https://taskmarket.example/api/tasks/${task.id}`);
});

it("does not contact the paid endpoint before explicit confirmation", async () => {
const wallet = {} as never;
const result = await provider.createTask(wallet, {
description: "Delegate a bounded public API check",
rewardUsdc: 2,
durationHours: 4,
tags: ["api"],
confirm: false,
});
const parsed = JSON.parse(result);

expect(parsed.status).toBe("confirmation_required");
expect(parsed.task.reward).toBe("2000000");
expect(fetchMock).not.toHaveBeenCalled();
});

it("rejects a confirmed escrow above the configured limit", async () => {
const result = await provider.createTask({} as never, {
description: "This should not be sent",
rewardUsdc: 5.01,
durationHours: 1,
confirm: true,
});

const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
expect(parsed.error).toContain("exceeds the configured maximum");
expect(fetchMock).not.toHaveBeenCalled();
});

it("reports HTTP errors without throwing", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 503,
json: jest.fn().mockResolvedValue({ message: "temporarily unavailable" }),
});

const result = await provider.listTasks({});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(false);
expect(parsed.error).toContain("HTTP 503");
});
});
Loading
Loading