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
2 changes: 2 additions & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ export * from "./zerion";
export * from "./zerodev";
export * from "./zeroX";
export * from "./zora";

export * from "./taskmarket";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* TaskMarket API constants.
* Public REST API — browse/get are free; paid actions require X402 (not used here without auth).
*/
export const TASKMARKET_API_BASE = "https://api.taskmarket.dev";
export const TASKMARKET_DOCS = "https://docs.taskmarket.dev/";
export const TASKMARKET_SITE = "https://taskmarket.dev/";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./taskmarketActionProvider";
export * from "./schemas";
export * from "./constants";
87 changes: 87 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { z } from "zod";

/**
* List open TaskMarket tasks.
*/
export const ListOpenTasksSchema = z
.object({
limit: z
.number()
.int()
.min(1)
.max(50)
.nullable()
.describe("Max tasks to return (1-50). Defaults to 10."),
mode: z
.string()
.nullable()
.describe("Optional mode filter: bounty, claim, pitch, benchmark, auction"),
tags: z
.string()
.nullable()
.describe("Optional comma-separated tags filter"),
})
.strict();

/**
* Fetch a single task by id.
*/
export const GetTaskSchema = z
.object({
taskId: z
.string()
.describe("TaskMarket task id (0x… hex) or full task URL"),
})
.strict();

/**
* Draft a delegation proposal — does NOT create or fund a task.
*/
export const PrepareDelegationSchema = z
.object({
description: z.string().describe("Concrete deliverable description for the task"),
rewardUsdc: z
.number()
.positive()
.describe("Proposed escrow reward in USDC (human-readable, e.g. 5)"),
durationHours: z
.number()
.positive()
.describe("Proposed task duration in hours"),
mode: z
.string()
.nullable()
.describe("Task mode (default bounty)"),
spendingLimitUsdc: z
.number()
.positive()
.describe("Hard spending ceiling the user must approve before any create/fund call"),
userAuthorized: z
.boolean()
.describe(
"Must be true only after the human explicitly approved this draft. False = draft only.",
),
})
.strict();

/**
* Submit work to an existing task. Requires explicit user authorization.
* Does not create tasks or move funds.
*/
export const SubmitWorkSchema = z
.object({
taskId: z.string().describe("Target task id (0x…)"),
deliverableSummary: z
.string()
.describe("Short summary of the deliverable being submitted"),
artifactPaths: z
.array(z.string())
.nullable()
.describe("Optional local file paths / URLs for evidence artifacts"),
userAuthorized: z
.boolean()
.describe(
"REQUIRED true: human must have authorized this submission. Silent submit is forbidden.",
),
})
.strict();
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { taskmarketActionProvider } from "./taskmarketActionProvider";

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

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

describe("listOpenTasks", () => {
it("returns mapped tasks on success", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({
tasks: [
{
id: "0xabc",
mode: "bounty",
reward: "1500000",
submissionCount: 2,
tags: ["ai"],
description: "Hello world task",
},
],
}),
});
const result = await provider.listOpenTasks({
limit: 5,
mode: null,
tags: null,
});
const parsed = JSON.parse(result);
expect(parsed.count).toBe(1);
expect(parsed.tasks[0].rewardUsdcApprox).toBe(1.5);
});

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

describe("prepareDelegation", () => {
it("returns pending_approval when not authorized", async () => {
const result = await provider.prepareDelegation({
description: "demo",
rewardUsdc: 1,
durationHours: 24,
spendingLimitUsdc: 5,
userAuthorized: false,
mode: null,
});
expect(JSON.parse(result).status).toBe("pending_approval");
});

it("rejects over spending limit", async () => {
const result = await provider.prepareDelegation({
description: "demo",
rewardUsdc: 10,
durationHours: 24,
spendingLimitUsdc: 5,
userAuthorized: true,
mode: "bounty",
});
expect(JSON.parse(result).status).toBe("rejected");
});
});

describe("submitWork", () => {
it("blocks when userAuthorized is false", async () => {
const result = await provider.submitWork({
taskId: "0xabc",
deliverableSummary: "x",
artifactPaths: null,
userAuthorized: false,
});
expect(JSON.parse(result).status).toBe("blocked");
});

it("returns plan when authorized", async () => {
const result = await provider.submitWork({
taskId: "https://taskmarket.dev/tasks/0xdeadbeef",
deliverableSummary: "evidence",
artifactPaths: ["a.md"],
userAuthorized: true,
});
const parsed = JSON.parse(result);
expect(parsed.status).toBe("authorized_submission_plan");
expect(parsed.taskId).toBe("0xdeadbeef");
});
});

describe("supportsNetwork", () => {
it("supports all networks", () => {
expect(
provider.supportsNetwork({ protocolFamily: "evm", networkId: "base-mainnet" }),
).toBe(true);
});
});
});
Loading
Loading