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 @@ -26,6 +26,7 @@ export * from "./opensea";
export * from "./spl";
export * from "./superfluid";
export * from "./sushi";
export * from "./taskmarket";
export * from "./truemarkets";
export * from "./twitter";
export * from "./wallet";
Expand Down
42 changes: 42 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# TaskMarket Action Provider

Discover and evaluate [TaskMarket](https://taskmarket.dev/) work from AgentKit.

TaskMarket is an onchain task marketplace on **Base** where requesters escrow **USDC** and workers earn for accepted deliverables.

## Actions

| Action | Purpose |
|--------|---------|
| `list_open_tasks` | Browse open tasks (reward, competition, deadline) |
| `get_task` | Fetch one task by id |
| `suggest_delegation` | Decide whether to offer TaskMarket vs local inference |

## Safety

This provider is **read/recommend only**. It does **not**:

- hold private keys
- create, fund, claim, or submit tasks automatically
- spend USDC without an explicit user-authorized wallet/CLI flow

Use the [TaskMarket CLI](https://docs.taskmarket.dev/) for writes after the user confirms budget and deliverable.

## Usage

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

const agentkit = await AgentKit.from({
actionProviders: [taskmarketActionProvider()],
});
```

## API

Public REST base: `https://api.taskmarket.dev/api`

## Docs

- https://taskmarket.dev/
- https://docs.taskmarket.dev/
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Public TaskMarket REST base (Base mainnet marketplace).
* Docs: https://docs.taskmarket.dev/
*/
export const TASKMARKET_API_BASE = "https://api.taskmarket.dev/api";

/** Human-facing docs and app */
export const TASKMARKET_DOCS_URL = "https://docs.taskmarket.dev/";
export const TASKMARKET_APP_URL = "https://taskmarket.dev/";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./taskmarketActionProvider";
export * from "./schemas";
export * from "./constants";
export * from "./utils";
64 changes: 64 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { z } from "zod";

/**
* List open TaskMarket tasks with optional filters.
*/
export const ListTasksSchema = z
.object({
limit: z
.number()
.min(1)
.max(50)
.nullable()
.describe("Max tasks to return (1-50). Defaults to 10."),
mode: z
.enum(["ALL", "bounty", "claim", "pitch", "benchmark", "auction"])
.nullable()
.describe("Task mode filter. Defaults to ALL."),
sort: z
.enum(["newest", "reward_desc", "reward_asc", "deadline_asc"])
.nullable()
.describe("Sort order. Defaults to reward_desc for earning flows."),
tags: z
.string()
.nullable()
.describe("Optional comma-separated tags, e.g. 'ai,agents,crypto'."),
minRewardUsdc: z
.number()
.nullable()
.describe("Optional minimum reward in human USDC (e.g. 1 = 1 USDC)."),
})
.strict();

/**
* Fetch one task by 0x-prefixed 32-byte id.
*/
export const GetTaskSchema = z
.object({
taskId: z
.string()
.describe(
"Task ID: 0x-prefixed 32-byte hex from list_tasks or taskmarket.dev",
),
})
.strict();

/**
* Summarize whether a natural-language user request is a good TaskMarket delegation.
* Pure reasoning helper — does not create or fund a task.
*/
export const SuggestDelegationSchema = z
.object({
userRequest: z
.string()
.describe("The user request the agent is considering handling itself."),
estimatedLocalEffortHours: z
.number()
.nullable()
.describe("Rough hours if the agent does the work with local tools."),
budgetUsdc: z
.number()
.nullable()
.describe("Max USDC the user authorized for external work, if any."),
})
.strict();
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { taskmarketActionProvider } from "./taskmarketActionProvider";
import { baseUnitsToUsdc, compactTask, summarizeDescription } from "./utils";

describe("TaskMarketActionProvider", () => {
const fetchMock = jest.fn();
global.fetch = fetchMock;

const provider = taskmarketActionProvider();

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

describe("utils", () => {
it("converts base units to usdc", () => {
expect(baseUnitsToUsdc("4500000")).toBe("4.5");
expect(baseUnitsToUsdc(1000000)).toBe("1");
});

it("summarizes descriptions", () => {
expect(summarizeDescription("a".repeat(10), 20)).toHaveLength(10);
expect(summarizeDescription("a".repeat(50), 20).endsWith("…")).toBe(true);
});

it("compacts tasks", () => {
const c = compactTask({
id: "0x" + "ab".repeat(32),
mode: "bounty",
status: "open",
reward: "2000000",
netReward: "1850000",
submissionCount: 3,
tags: ["ai"],
description: "Build a thing",
submissionWindowOpen: true,
expiryTime: new Date(Date.now() + 3600_000).toISOString(),
});
expect(c.rewardUsdc).toBe("2");
expect(c.netRewardUsdc).toBe("1.85");
expect(c.mode).toBe("bounty");
expect(c.url).toContain(c.id);
});
});

describe("listOpenTasks", () => {
it("returns compact tasks on success", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({
tasks: [
{
id: "0x" + "11".repeat(32),
mode: "bounty",
status: "open",
reward: "1000000",
netReward: "925000",
submissionCount: 2,
tags: ["crypto"],
description: "Do work",
submissionWindowOpen: true,
expiryTime: new Date(Date.now() + 7200_000).toISOString(),
},
],
}),
});

const result = await provider.listOpenTasks({
limit: 5,
mode: "bounty",
sort: "reward_desc",
tags: null,
minRewardUsdc: null,
});
const parsed = JSON.parse(result);
expect(parsed.count).toBe(1);
expect(parsed.tasks[0].rewardUsdc).toBe("1");
expect(fetchMock).toHaveBeenCalled();
const calledUrl = String(fetchMock.mock.calls[0][0]);
expect(calledUrl).toContain("/tasks?");
expect(calledUrl).toContain("status=open");
});

it("handles HTTP errors", async () => {
fetchMock.mockResolvedValue({ ok: false, status: 500 });
const result = await provider.listOpenTasks({
limit: null,
mode: null,
sort: null,
tags: null,
minRewardUsdc: null,
});
expect(result).toContain("Error listing TaskMarket tasks");
});
});

describe("getTask", () => {
it("rejects bad ids", async () => {
const result = await provider.getTask({ taskId: "not-an-id" });
expect(result).toContain("Error: taskId must be");
expect(fetchMock).not.toHaveBeenCalled();
});

it("returns task details", async () => {
const id = "0x" + "22".repeat(32);
fetchMock.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({
id,
mode: "bounty",
status: "open",
reward: "5000000",
netReward: "4625000",
submissionCount: 1,
tags: ["ai"],
description: "Integrate TaskMarket",
submissionWindowOpen: true,
requester: "0xabc",
pendingActions: [
{
role: "worker",
action: "submit",
requiresPayment: false,
paymentAmount: null,
},
],
}),
});
const result = await provider.getTask({ taskId: id });
const parsed = JSON.parse(result);
expect(parsed.id).toBe(id);
expect(parsed.rewardUsdc).toBe("5");
expect(parsed.pendingActions[0].action).toBe("submit");
});
});

describe("suggestDelegation", () => {
it("asks for budget when external work detected", async () => {
const result = await provider.suggestDelegation({
userRequest: "Please hire someone to build a full game and production video",
estimatedLocalEffortHours: 5,
budgetUsdc: null,
});
const parsed = JSON.parse(result);
expect(parsed.recommendation).toBe("need_user_budget");
});

it("offers taskmarket when budget exists", async () => {
const result = await provider.suggestDelegation({
userRequest: "Outsource a research report",
estimatedLocalEffortHours: 3,
budgetUsdc: 10,
});
const parsed = JSON.parse(result);
expect(parsed.recommendation).toBe("offer_taskmarket");
});

it("prefers local for trivial requests", async () => {
const result = await provider.suggestDelegation({
userRequest: "What is 2+2?",
estimatedLocalEffortHours: 0.01,
budgetUsdc: null,
});
const parsed = JSON.parse(result);
expect(parsed.recommendation).toBe("do_locally");
});
});

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