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": minor
---

Added a Taskmarket action provider integrating the on-chain Taskmarket bounty marketplace. It exposes `list_tasks`, `get_task`, and `my_submissions` for discovering and tracking escrowed bounty work, plus `submit_work` and `create_task` which are gated behind an explicit `confirm` input so no funds move without user authorization. Requires the `taskmarket` CLI to be installed and initialized on the host.
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 @@ -41,3 +41,4 @@ export * from "./zerion";
export * from "./zerodev";
export * from "./zeroX";
export * from "./zora";
export * from "./taskmarket";
56 changes: 56 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Taskmarket Action Provider

This action provider integrates [Taskmarket](https://taskmarket.dev) — an on-chain bounty marketplace where agents complete digital work for escrowed USDC rewards — into AgentKit.

It enables the full delegation loop inside any AgentKit-powered agent:

1. **Discover work**: `list_tasks` browses open, escrow-backed bounties.
2. **Inspect before acting**: `get_task` returns the full task record (reward, expiry, escrow transaction, submission window).
3. **Track results**: `my_submissions` reports the wallet's submissions and their award state.
4. **Delegate with authorization**: `create_task` posts new work to the marketplace and `submit_work` delivers completed work — both are gated behind an explicit `confirm` input so funds and on-chain state are never touched without user approval.

## Setup

The provider delegates to the official `taskmarket` CLI, which must be installed and initialized on the host:

```bash
npm install -g @lucid-agents/taskmarket@latest
taskmarket init
taskmarket address
```

`taskmarket init` creates and registers the worker wallet used by every command. The provider never reads or exports wallet keys — signing and payment stay inside the CLI's own keystore.

## Usage with AgentKit

```typescript
import { taskmarketActionProvider } from "@coinbase/agentkit";

const actions = [
taskmarketActionProvider(),
// other action providers...
];
```

Then prompt your agent, for example:

```
List open Taskmarket tasks with a reward of at least 2 USDC, then show me the
full details of the first one. Do not submit anything without asking me.
```

## Actions

| Action | Description | Spend-gated |
| --- | --- | --- |
| `list_tasks` | Browse open tasks with status/limit/reward/tag filters | No |
| `get_task` | Fetch one task's full on-chain record | No |
| `my_submissions` | List the wallet's submissions across all tasks | No |
| `submit_work` | Submit a deliverable file to a task | `confirm` required |
| `create_task` | Create a task and escrow its USDC reward | `confirm` required |

## Security notes

- Read actions (`list_tasks`, `get_task`, `my_submissions`) never move funds.
- `submit_work` anchors the artifact on-chain and is irreversible; `create_task` escrows the reward amount. Both require the caller to set `confirm: true` explicitly.
- The provider adds no autonomous spending: every paid action originates from an explicit tool call with user-visible parameters.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./taskmarketActionProvider";
65 changes: 65 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { z } from "zod";

/**
* Input schema for listing Taskmarket tasks
*/
export const ListTasksSchema = z
.object({
status: z.string().describe("Task status filter, e.g. open").default("open"),
limit: z.number().int().min(1).max(100).describe("Maximum number of results").default(20),
rewardMin: z
.number()
.nullable()
.optional()
.describe("Optional minimum reward in USDC"),
tags: z.string().nullable().optional().describe("Optional comma-separated tag filter"),
})
.strict();

/**
* Input schema for getting a single Taskmarket task
*/
export const GetTaskSchema = z
.object({
taskId: z.string().describe("The 0x-prefixed Taskmarket task id"),
})
.strict();

/**
* Input schema for listing the wallet's own submissions
*/
export const GetMySubmissionsSchema = z.object({}).strict();

/**
* Input schema for submitting work to a Taskmarket task
*/
export const SubmitWorkSchema = z
.object({
taskId: z.string().describe("The 0x-prefixed Taskmarket task id"),
filePath: z.string().describe("Local path of the deliverable file to submit"),
role: z
.enum(["preview", "source", "final", "attachment"])
.describe("Artifact role applied to the submitted file")
.default("final"),
confirm: z
.boolean()
.describe("Must be true to submit; submission is irreversible and anchors the artifact on-chain")
.default(false),
})
.strict();

/**
* Input schema for creating a Taskmarket task
*/
export const CreateTaskSchema = z
.object({
description: z.string().describe("Full task specification shown to workers"),
rewardUsdc: z.number().positive().describe("Reward in USDC; this amount is escrowed"),
durationHours: z.number().int().positive().describe("Hours until the task expires"),
tags: z.string().nullable().optional().describe("Optional comma-separated tags"),
confirm: z
.boolean()
.describe("Must be true to create the task and escrow the reward")
.default(false),
})
.strict();
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { execFile } from "child_process";
import { taskmarketActionProvider } from "./taskmarketActionProvider";

jest.mock("child_process", () => ({ execFile: jest.fn() }));

const execFileMock = execFile as unknown as jest.Mock;

describe("TaskmarketActionProvider", () => {
const provider = taskmarketActionProvider();

beforeEach(() => {
jest.resetAllMocks();
});

const cliCall = (stdout: string, stderr = "") =>
execFileMock.mockImplementation(
(cmd: string, args: string[], opts: unknown, cb: (e: Error | null, so: string, se: string) => void) =>
cb(null, stdout, stderr),
);

const cliError = (message: string, stderr = "") =>
execFileMock.mockImplementation(
(cmd: string, args: string[], opts: unknown, cb: (e: Error | null, so: string, se: string) => void) =>
cb(new Error(message), "", stderr),
);

describe("listTasks", () => {
it("should invoke the CLI with default filters and return its output", async () => {
const output = JSON.stringify({ ok: true, data: { tasks: [] } });
cliCall(output);

const result = await provider.listTasks({
status: "open",
limit: 20,
rewardMin: null,
tags: null,
});

expect(result).toEqual(output);
expect(execFileMock).toHaveBeenCalledWith(
"taskmarket",
["task", "list", "--status", "open", "--limit", "20"],
expect.anything(),
expect.any(Function),
);
});

it("should pass through reward and tag filters", async () => {
cliCall("{}");

await provider.listTasks({ status: "open", limit: 5, rewardMin: 1, tags: "html" });

expect(execFileMock).toHaveBeenCalledWith(
"taskmarket",
["task", "list", "--status", "open", "--limit", "5", "--reward-min", "1", "--tags", "html"],
expect.anything(),
expect.any(Function),
);
});

it("should surface CLI errors with stderr detail", async () => {
cliError("spawn failed", "CLI not installed");

const result = await provider.listTasks({ status: "open", limit: 20, rewardMin: null, tags: null });

expect(result).toContain("Error listing Taskmarket tasks");
expect(result).toContain("CLI not installed");
});
});

describe("getTask", () => {
it("should fetch a task by id", async () => {
const output = JSON.stringify({ ok: true, data: { id: "0xabc" } });
cliCall(output);

const result = await provider.getTask({ taskId: "0xabc" });

expect(result).toEqual(output);
expect(execFileMock).toHaveBeenCalledWith(
"taskmarket",
["task", "get", "0xabc"],
expect.anything(),
expect.any(Function),
);
});

it("should return an error message for unknown tasks", async () => {
cliError("not found", "Task not found");

const result = await provider.getTask({ taskId: "0xdead" });

expect(result).toContain("Error getting Taskmarket task");
expect(result).toContain("Task not found");
});
});

describe("mySubmissions", () => {
it("should list the wallet's submissions", async () => {
const output = JSON.stringify({ ok: true, data: [] });
cliCall(output);

const result = await provider.mySubmissions();

expect(result).toEqual(output);
expect(execFileMock).toHaveBeenCalledWith(
"taskmarket",
["task", "my-submissions"],
expect.anything(),
expect.any(Function),
);
});
});

describe("submitWork", () => {
it("should refuse to submit without explicit confirmation", async () => {
const result = await provider.submitWork({
taskId: "0xabc",
filePath: "./index.html",
role: "final",
confirm: false,
});

expect(result).toContain("not confirmed");
expect(execFileMock).not.toHaveBeenCalled();
});

it("should submit with the file and role when confirmed", async () => {
const output = JSON.stringify({ ok: true, data: { submissionId: "sub-1" } });
cliCall(output);

const result = await provider.submitWork({
taskId: "0xabc",
filePath: "./index.html",
role: "final",
confirm: true,
});

expect(result).toEqual(output);
expect(execFileMock).toHaveBeenCalledWith(
"taskmarket",
["task", "submit", "0xabc", "--file", "./index.html", "--role", "final"],
expect.anything(),
expect.any(Function),
);
});
});

describe("createTask", () => {
it("should refuse to create a task without explicit confirmation", async () => {
const result = await provider.createTask({
description: "Build a logo",
rewardUsdc: 5,
durationHours: 48,
tags: null,
confirm: false,
});

expect(result).toContain("not confirmed");
expect(result).toContain("5 USDC");
expect(execFileMock).not.toHaveBeenCalled();
});

it("should escrow and create the task when confirmed", async () => {
const output = JSON.stringify({ ok: true, data: { id: "0xnew" } });
cliCall(output);

const result = await provider.createTask({
description: "Build a logo",
rewardUsdc: 5,
durationHours: 48,
tags: "design",
confirm: true,
});

expect(result).toEqual(output);
expect(execFileMock).toHaveBeenCalledWith(
"taskmarket",
[
"task",
"create",
"--description",
"Build a logo",
"--reward",
"5",
"--duration",
"48",
"--tags",
"design",
],
expect.anything(),
expect.any(Function),
);
});
});

describe("supportsNetwork", () => {
it("should always return true", () => {
expect(provider.supportsNetwork()).toBe(true);
});
});
});
Loading
Loading