From 949c8cc8bfff974d98618d45b40d3cb123bb0fab Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:09:57 -0300 Subject: [PATCH 01/11] Add TaskMarket action provider --- .../action-providers/taskmarket/schemas.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/schemas.ts diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts new file mode 100644 index 000000000..6bf6cfdbd --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -0,0 +1,137 @@ +Exit code: 0 +Wall time: 1.5 seconds +Output: +import { z } from "zod"; + +const taskId = z + .string() + .min(1) + .describe("TaskMarket task identifier, usually a 0x-prefixed 32-byte hex value"); + +/** Filters accepted by the TaskMarket task search endpoint. */ +export const TaskMarketListTasksSchema = z + .object({ + status: z.string().optional().default("open").describe("Task status, for example open"), + phase: z + .enum(["active", "in_review", "awaiting_settlement", "resolved"]) + .optional() + .describe("Derived lifecycle phase"), + mode: z + .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) + .optional() + .describe("Task mode"), + tags: z.array(z.string()).optional().describe("Tags to match"), + rewardMin: z.number().nonnegative().optional().describe("Minimum reward in USDC"), + rewardMax: z.number().nonnegative().optional().describe("Maximum reward in USDC"), + deadlineHours: z + .number() + .int() + .positive() + .optional() + .describe("Only tasks expiring within this many hours"), + limit: z.number().int().positive().max(100).optional().default(20), + cursor: z.string().optional().describe("Pagination cursor returned by a previous call"), + }) + .describe("Filters for searching public TaskMarket tasks"); + +/** Input for retrieving a single task. */ +export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket task lookup"); + +/** Input for claiming a task as the current EVM wallet. */ +export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); + +/** Input for retrieving submissions owned by the current EVM wallet. */ +export const TaskMarketMySubmissionsSchema = z.object({ + taskId: taskId.optional().describe("Optionally restrict results to one task"), +}); + +/** + * A single text artifact. Text is intentionally explicit so an agent does not upload + * arbitrary local files without the caller's knowledge. + */ +export const TaskMarketSubmitTextSchema = z + .object({ + taskId, + fileName: z + .string() + .min(1) + .regex(/^[^\\/]+$/, "fileName must not contain path separators") + .describe("Public artifact filename"), + content: z.string().min(1).describe("Text content to submit as the artifact"), + mimeType: z.string().min(1).optional().default("text/plain"), + role: z + .enum(["preview", "source", "final", "attachment"]) + .optional() + .default("final") + .describe("TaskMarket artifact role"), + }) + .describe("Submit one explicitly provided text artifact to TaskMarket"); + + +import { z } from "zod"; + +const taskId = z + .string() + .min(1) + .describe("TaskMarket task identifier, usually a 0x-prefixed 32-byte hex value"); + +/** Filters accepted by the TaskMarket task search endpoint. */ +export const TaskMarketListTasksSchema = z + .object({ + status: z.string().optional().default("open").describe("Task status, for example open"), + phase: z + .enum(["active", "in_review", "awaiting_settlement", "resolved"]) + .optional() + .describe("Derived lifecycle phase"), + mode: z + .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) + .optional() + .describe("Task mode"), + tags: z.array(z.string()).optional().describe("Tags to match"), + rewardMin: z.number().nonnegative().optional().describe("Minimum reward in USDC"), + rewardMax: z.number().nonnegative().optional().describe("Maximum reward in USDC"), + deadlineHours: z + .number() + .int() + .positive() + .optional() + .describe("Only tasks expiring within this many hours"), + limit: z.number().int().positive().max(100).optional().default(20), + cursor: z.string().optional().describe("Pagination cursor returned by a previous call"), + }) + .describe("Filters for searching public TaskMarket tasks"); + +/** Input for retrieving a single task. */ +export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket task lookup"); + +/** Input for claiming a task as the current EVM wallet. */ +export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); + +/** Input for retrieving submissions owned by the current EVM wallet. */ +export const TaskMarketMySubmissionsSchema = z.object({ + taskId: taskId.optional().describe("Optionally restrict results to one task"), +}); + +/** + * A single text artifact. Text is intentionally explicit so an agent does not upload + * arbitrary local files without the caller's knowledge. + */ +export const TaskMarketSubmitTextSchema = z + .object({ + taskId, + fileName: z + .string() + .min(1) + .regex(/^[^\\/]+$/, "fileName must not contain path separators") + .describe("Public artifact filename"), + content: z.string().min(1).describe("Text content to submit as the artifact"), + mimeType: z.string().min(1).optional().default("text/plain"), + role: z + .enum(["preview", "source", "final", "attachment"]) + .optional() + .default("final") + .describe("TaskMarket artifact role"), + }) + .describe("Submit one explicitly provided text artifact to TaskMarket"); + + From bdfc5e8f40fa5ab17cf6a7b511b99e6045aef84a Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:10:33 -0300 Subject: [PATCH 02/11] Update schemas.ts --- .../action-providers/taskmarket/schemas.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts index 6bf6cfdbd..7ecd40d0b 100644 --- a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -1,3 +1,70 @@ +import { z } from "zod"; + +const taskId = z + .string() + .min(1) + .describe("TaskMarket task identifier, usually a 0x-prefixed 32-byte hex value"); + +/** Filters accepted by the TaskMarket task search endpoint. */ +export const TaskMarketListTasksSchema = z + .object({ + status: z.string().optional().default("open").describe("Task status, for example open"), + phase: z + .enum(["active", "in_review", "awaiting_settlement", "resolved"]) + .optional() + .describe("Derived lifecycle phase"), + mode: z + .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) + .optional() + .describe("Task mode"), + tags: z.array(z.string()).optional().describe("Tags to match"), + rewardMin: z.number().nonnegative().optional().describe("Minimum reward in USDC"), + rewardMax: z.number().nonnegative().optional().describe("Maximum reward in USDC"), + deadlineHours: z + .number() + .int() + .positive() + .optional() + .describe("Only tasks expiring within this many hours"), + limit: z.number().int().positive().max(100).optional().default(20), + cursor: z.string().optional().describe("Pagination cursor returned by a previous call"), + }) + .describe("Filters for searching public TaskMarket tasks"); + +/** Input for retrieving a single task. */ +export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket task lookup"); + +/** Input for claiming a task as the current EVM wallet. */ +export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); + +/** Input for retrieving submissions owned by the current EVM wallet. */ +export const TaskMarketMySubmissionsSchema = z.object({ + taskId: taskId.optional().describe("Optionally restrict results to one task"), +}); + +/** + * A single text artifact. Text is intentionally explicit so an agent does not upload + * arbitrary local files without the caller's knowledge. + */ +export const TaskMarketSubmitTextSchema = z + .object({ + taskId, + fileName: z + .string() + .min(1) + .regex(/^[^\\/]+$/, "fileName must not contain path separators") + .describe("Public artifact filename"), + content: z.string().min(1).describe("Text content to submit as the artifact"), + mimeType: z.string().min(1).optional().default("text/plain"), + role: z + .enum(["preview", "source", "final", "attachment"]) + .optional() + .default("final") + .describe("TaskMarket artifact role"), + }) + .describe("Submit one explicitly provided text artifact to TaskMarket"); + + Exit code: 0 Wall time: 1.5 seconds Output: From 98b88f9f66460df58290528b46ecac1a76e444c8 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:11:19 -0300 Subject: [PATCH 03/11] Create taskmarketActionProvider.ts --- .../taskmarket/taskmarketActionProvider.ts | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts new file mode 100644 index 000000000..fab9d3311 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts @@ -0,0 +1,291 @@ +import { createHash } from "node:crypto"; +import { z } from "zod"; +import { keccak256 } from "viem"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { + TaskMarketClaimTaskSchema, + TaskMarketGetTaskSchema, + TaskMarketListTasksSchema, + TaskMarketMySubmissionsSchema, + TaskMarketSubmitTextSchema, +} from "./schemas"; + +const DEFAULT_API_URL = "https://api.taskmarket.dev"; +const READ_AUTH_ADDRESS_HEADER = "X-Taskmarket-Caller-Address"; +const READ_AUTH_SIGNATURE_HEADER = "X-Taskmarket-Caller-Signature"; + +export interface TaskMarketActionProviderConfig { + /** TaskMarket API origin. Defaults to https://api.taskmarket.dev. */ + apiUrl?: string; + /** + * Enables claim and submit actions. It is false by default so a read-only agent + * cannot commit the wallet to a task or incur a relay fee accidentally. + */ + allowWriteActions?: boolean; + /** Request timeout in milliseconds. Defaults to 20 seconds. */ + requestTimeoutMs?: number; +} + +interface TaskMarketArtifact { + artifactKey: string; + fileName: string; + mimeType: string; + role: string; + sizeBytes: number; + sha256Hash: string; + keccak256Hash: string; +} + +type TaskMarketRequestOptions = Omit & { + body?: BodyInit | Record; +}; + +/** + * AgentKit actions for discovering and, when explicitly enabled, working on + * TaskMarket tasks. Reads use the public REST API; wallet-bound actions sign + * TaskMarket's documented intent messages with the configured EVM wallet. + */ +export class TaskMarketActionProvider extends ActionProvider { + private readonly apiUrl: string; + private readonly allowWriteActions: boolean; + private readonly requestTimeoutMs: number; + + constructor(config: TaskMarketActionProviderConfig = {}) { + super("taskmarket", []); + this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, ""); + this.allowWriteActions = config.allowWriteActions ?? false; + this.requestTimeoutMs = config.requestTimeoutMs ?? 20_000; + + if (!/^https?:\/\//i.test(this.apiUrl)) { + throw new Error("TaskMarket apiUrl must be an http(s) URL"); + } + } + + /** Search public TaskMarket tasks without using a wallet. */ + @CreateAction({ + name: "list_tasks", + description: + "List public TaskMarket tasks. Use this to discover work before claiming or submitting anything.", + schema: TaskMarketListTasksSchema, + }) + async listTasks(args: z.infer): Promise { + const params = new URLSearchParams(); + params.set("status", args.status); + if (args.phase) params.set("phase", args.phase); + if (args.mode) params.set("mode", args.mode); + if (args.tags?.length) params.set("tags", args.tags.join(",")); + if (args.rewardMin !== undefined) { + params.set("minReward", String(Math.round(args.rewardMin * 1_000_000))); + } + if (args.rewardMax !== undefined) { + params.set("maxReward", String(Math.round(args.rewardMax * 1_000_000))); + } + if (args.deadlineHours !== undefined) params.set("deadlineHours", String(args.deadlineHours)); + params.set("limit", String(args.limit)); + if (args.cursor) params.set("cursor", args.cursor); + + return this.request(`/api/tasks?${params.toString()}`); + } + + /** Retrieve the complete public record for a task. */ + @CreateAction({ + name: "get_task", + description: "Retrieve one public TaskMarket task by its task identifier.", + schema: TaskMarketGetTaskSchema, + }) + async getTask(args: z.infer): Promise { + return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}`); + } + + /** List submissions for the current wallet, with TaskMarket read authentication. */ + @CreateAction({ + name: "my_submissions", + description: + "List TaskMarket submissions made by the connected EVM wallet. This is read-only.", + schema: TaskMarketMySubmissionsSchema, + }) + async mySubmissions( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + const address = walletProvider.getAddress(); + const signature = await walletProvider.signMessage(`taskmarket:read:${address.toLowerCase()}`); + const result = await this.requestJson( + `/api/submissions/mine?workerAddress=${encodeURIComponent(address)}`, + { + headers: { + [READ_AUTH_ADDRESS_HEADER]: address, + [READ_AUTH_SIGNATURE_HEADER]: signature, + }, + }, + ); + if (args.taskId && Array.isArray(result)) { + const filtered = result.filter( + item => + item && + typeof item === "object" && + "taskId" in item && + item.taskId === args.taskId, + ); + return JSON.stringify(filtered, null, 2); + } + if ( + args.taskId && + result && + typeof result === "object" && + "submissions" in result && + Array.isArray(result.submissions) + ) { + const filtered = result.submissions.filter( + item => + item && + typeof item === "object" && + "taskId" in item && + item.taskId === args.taskId, + ); + return JSON.stringify({ ...result, submissions: filtered }, null, 2); + } + return JSON.stringify(result, null, 2); + } + + /** Claim a task after the caller explicitly enabled write actions. */ + @CreateAction({ + name: "claim_task", + description: + "Claim a TaskMarket task for the connected wallet. This is an external side effect and is disabled unless allowWriteActions is true.", + schema: TaskMarketClaimTaskSchema, + }) + async claimTask( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + this.assertWritesEnabled(); + const signature = await walletProvider.signMessage(`taskmarket:claim:${args.taskId}`); + return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}/claim`, { + method: "POST", + body: { + taskId: args.taskId, + workerAddress: walletProvider.getAddress(), + signature, + }, + }); + } + + /** Submit one explicit text artifact through TaskMarket's upload-key flow. */ + @CreateAction({ + name: "submit_text", + description: + "Submit one explicitly provided text artifact to TaskMarket. This uploads content and may incur a relay fee; it is disabled unless allowWriteActions is true.", + schema: TaskMarketSubmitTextSchema, + }) + async submitText( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + this.assertWritesEnabled(); + const workerAddress = walletProvider.getAddress(); + const content = new TextEncoder().encode(args.content); + const uploadSignature = await walletProvider.signMessage(`taskmarket:submit:${args.taskId}`); + const uploadResponse = await this.requestJson<{ uploadUrl: string; artifactKey: string }>( + `/api/tasks/${encodeURIComponent(args.taskId)}/submissions/request-upload-url`, + { + method: "POST", + body: { + taskId: args.taskId, + workerAddress, + signature: uploadSignature, + fileName: args.fileName, + mimeType: args.mimeType, + role: args.role, + sizeBytes: content.byteLength, + }, + }, + ); + + const upload = uploadResponse; + const uploadResult = await fetch(upload.uploadUrl, { + method: "PUT", + headers: { "Content-Type": args.mimeType }, + body: content, + signal: AbortSignal.timeout(this.requestTimeoutMs), + }); + if (!uploadResult.ok) { + throw new Error(`TaskMarket artifact upload failed with status ${uploadResult.status}`); + } + + const artifact: TaskMarketArtifact = { + artifactKey: upload.artifactKey, + fileName: args.fileName, + mimeType: args.mimeType, + role: args.role, + sizeBytes: content.byteLength, + sha256Hash: createHash("sha256").update(content).digest("hex"), + keccak256Hash: keccak256(content), + }; + const submitSignature = await walletProvider.signMessage( + `taskmarket:submit:${args.taskId}:${artifact.artifactKey}`, + ); + + return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}/submissions/from-keys`, { + method: "POST", + body: { + taskId: args.taskId, + workerAddress, + artifacts: [artifact], + signature: submitSignature, + }, + }); + } + + supportsNetwork(network: Network): boolean { + return network.protocolFamily.toLowerCase() === "evm"; + } + + private assertWritesEnabled(): void { + if (!this.allowWriteActions) { + throw new Error( + "TaskMarket write actions are disabled. Set allowWriteActions: true only after reviewing the task and approving the side effect.", + ); + } + } + + private async request(path: string, options: TaskMarketRequestOptions = {}): Promise { + const result = await this.requestJson(path, options); + return JSON.stringify(result, null, 2); + } + + private async requestJson(path: string, options: TaskMarketRequestOptions = {}): Promise { + const headers = new Headers(options.headers); + headers.set("Accept", "application/json"); + if (options.body !== undefined && !headers.has("Content-Type")) { + headers.set("Content-Type", "application/json"); + } + + const response = await fetch(`${this.apiUrl}${path}`, { + ...options, + method: options.method ?? "GET", + headers, + body: options.body === undefined || typeof options.body === "string" ? options.body : JSON.stringify(options.body), + signal: options.signal ?? AbortSignal.timeout(this.requestTimeoutMs), + }); + + const text = await response.text(); + let payload: unknown = text; + try { + payload = text ? JSON.parse(text) : null; + } catch { + // Preserve non-JSON error bodies for diagnostics. + } + if (!response.ok) { + throw new Error(`TaskMarket request failed with status ${response.status}: ${JSON.stringify(payload)}`); + } + return payload as T; + } +} + +export const taskMarketActionProvider = (config: TaskMarketActionProviderConfig = {}) => + new TaskMarketActionProvider(config); + From d3f575ca2e7e3285be7ea02f0a0ca6dcb7dba416 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:11:32 -0300 Subject: [PATCH 04/11] Create index.ts --- typescript/agentkit/src/action-providers/taskmarket/index.ts | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/index.ts diff --git a/typescript/agentkit/src/action-providers/taskmarket/index.ts b/typescript/agentkit/src/action-providers/taskmarket/index.ts new file mode 100644 index 000000000..7086e3d5a --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export * from "./taskmarketActionProvider"; From 7d21ff4fd07c851fffa08c3a3fcf2a874493a472 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:11:49 -0300 Subject: [PATCH 05/11] Create README.md --- .../src/action-providers/taskmarket/README.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/README.md diff --git a/typescript/agentkit/src/action-providers/taskmarket/README.md b/typescript/agentkit/src/action-providers/taskmarket/README.md new file mode 100644 index 000000000..11e33937a --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/README.md @@ -0,0 +1,33 @@ +# TaskMarket action provider + +`TaskMarketActionProvider` gives AgentKit agents a small, typed integration with +the TaskMarket REST API. + +## What it provides + +- `list_tasks` and `get_task` for public task discovery. +- `my_submissions` for read-only, wallet-authenticated submission history. +- `claim_task` for signing a documented claim intent. +- `submit_text` for uploading one explicit text artifact and submitting it + through TaskMarket's upload-key flow. + +Write actions are disabled by default. Enable them only after reviewing the +task and the intended artifact: + +```ts +import { AgentKit } from "@coinbase/agentkit"; +import { taskMarketActionProvider } from "@coinbase/agentkit"; + +const agentKit = await AgentKit.configureWithWallet({ + walletProvider, + actionProviders: [ + taskMarketActionProvider({ allowWriteActions: true }), + ], +}); +``` + +The provider never stores private keys or API tokens. It uses the configured +`EvmWalletProvider` to sign TaskMarket's read, claim, and submission messages. +It only uploads content supplied directly to `submit_text`; it does not read +local files. + From 0cc81b216e2c05463761569c194b12184d1c0096 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:12:08 -0300 Subject: [PATCH 06/11] Create taskmarketActionProvider.test.ts --- .../taskmarketActionProvider.test.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts new file mode 100644 index 000000000..47135c38c --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts @@ -0,0 +1,97 @@ +jest.mock("../../wallet-providers", () => { + class WalletProvider {} + class EvmWalletProvider extends WalletProvider {} + return { WalletProvider, EvmWalletProvider }; +}); + +import { TaskMarketActionProvider } from "./taskmarketActionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; + +const mockResponse = (body: unknown, status = 200): Response => + ({ + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(body), + }) as Response; + +const mockWallet = { + getAddress: () => "0x1111111111111111111111111111111111111111", + getName: () => "test-wallet", + getNetwork: () => ({ protocolFamily: "evm", networkId: "base-mainnet", chainId: "8453" }), + signMessage: jest.fn(async (message: string) => `signature:${message}`), +} as unknown as jest.Mocked; + +describe("TaskMarketActionProvider", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("lists tasks through the public API", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValue(mockResponse({ tasks: [{ id: "task-1" }], hasMore: false })); + const provider = new TaskMarketActionProvider({ apiUrl: "https://taskmarket.test" }); + + const result = await provider.listTasks({ status: "open", limit: 5 }); + + expect(result).toContain('"task-1"'); + expect(fetchMock).toHaveBeenCalledWith( + "https://taskmarket.test/api/tasks?status=open&limit=5", + expect.objectContaining({ method: "GET" }), + ); + }); + + it("keeps wallet writes disabled by default", async () => { + const provider = new TaskMarketActionProvider({ apiUrl: "https://taskmarket.test" }); + + await expect( + provider.claimTask(mockWallet, { taskId: "0xabc" }), + ).rejects.toThrow("write actions are disabled"); + expect(mockWallet.signMessage).not.toHaveBeenCalled(); + }); + + it("claims only after writes are explicitly enabled", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValue(mockResponse({ claimId: "claim-1" })); + const provider = new TaskMarketActionProvider({ + apiUrl: "https://taskmarket.test", + allowWriteActions: true, + }); + + const result = await provider.claimTask(mockWallet, { taskId: "0xabc" }); + + expect(result).toContain("claim-1"); + expect(mockWallet.signMessage).toHaveBeenCalledWith("taskmarket:claim:0xabc"); + expect(fetchMock).toHaveBeenCalledWith( + "https://taskmarket.test/api/tasks/0xabc/claim", + expect.objectContaining({ method: "POST" }), + ); + }); + + it("uploads and submits one explicit text artifact", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValueOnce(mockResponse({ uploadUrl: "https://upload.test/artifact", artifactKey: "key-1" })) + .mockResolvedValueOnce(mockResponse(null)) + .mockResolvedValueOnce(mockResponse({ submissionId: "submission-1" })); + const provider = new TaskMarketActionProvider({ + apiUrl: "https://taskmarket.test", + allowWriteActions: true, + }); + + const result = await provider.submitText(mockWallet, { + taskId: "0xabc", + fileName: "answer.txt", + content: "hello TaskMarket", + mimeType: "text/plain", + role: "final", + }); + + expect(result).toContain("submission-1"); + expect(mockWallet.signMessage).toHaveBeenCalledWith("taskmarket:submit:0xabc"); + expect(mockWallet.signMessage).toHaveBeenCalledWith("taskmarket:submit:0xabc:key-1"); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); +}); + From 486d851e1b291e619fb53ac0f1d3fe9883a6aadc Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:12:26 -0300 Subject: [PATCH 07/11] Update index.ts --- .../agentkit/src/action-providers/index.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..2364ea763 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -3,6 +3,51 @@ export * from "./actionProvider"; export * from "./customActionProvider"; +export * from "./across"; +export * from "./alchemy"; +export * from "./baseAccount"; +export * from "./basename"; +export * from "./cdp"; +export * from "./clanker"; +export * from "./compound"; +export * from "./defillama"; +export * from "./dtelecom"; +export * from "./enso"; +export * from "./erc20"; +export * from "./erc721"; +export * from "./erc8004"; +export * from "./farcaster"; +export * from "./jupiter"; +export * from "./messari"; +export * from "./pyth"; +export * from "./moonwell"; +export * from "./morpho"; +export * from "./opensea"; +export * from "./spl"; +export * from "./superfluid"; +export * from "./taskmarket"; +export * from "./sushi"; +export * from "./truemarkets"; +export * from "./twitter"; +export * from "./wallet"; +export * from "./weth"; +export * from "./wow"; +export * from "./allora"; +export * from "./flaunch"; +export * from "./onramp"; +export * from "./vaultsfyi"; +export * from "./x402"; +export * from "./yelay"; +export * from "./zerion"; +export * from "./zerodev"; +export * from "./zeroX"; +export * from "./zora"; + +export * from "./actionDecorator"; +export * from "./actionProvider"; + +export * from "./customActionProvider"; + export * from "./across"; export * from "./alchemy"; export * from "./baseAccount"; From d9063f4e68b6ceecc4a83f9f444b3ab689dea32b Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Thu, 13 Aug 2026 20:14:17 -0300 Subject: [PATCH 08/11] Update schemas.ts --- .../action-providers/taskmarket/schemas.ts | 137 ------------------ 1 file changed, 137 deletions(-) diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts index 7ecd40d0b..d7fe932cd 100644 --- a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -65,140 +65,3 @@ export const TaskMarketSubmitTextSchema = z .describe("Submit one explicitly provided text artifact to TaskMarket"); -Exit code: 0 -Wall time: 1.5 seconds -Output: -import { z } from "zod"; - -const taskId = z - .string() - .min(1) - .describe("TaskMarket task identifier, usually a 0x-prefixed 32-byte hex value"); - -/** Filters accepted by the TaskMarket task search endpoint. */ -export const TaskMarketListTasksSchema = z - .object({ - status: z.string().optional().default("open").describe("Task status, for example open"), - phase: z - .enum(["active", "in_review", "awaiting_settlement", "resolved"]) - .optional() - .describe("Derived lifecycle phase"), - mode: z - .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) - .optional() - .describe("Task mode"), - tags: z.array(z.string()).optional().describe("Tags to match"), - rewardMin: z.number().nonnegative().optional().describe("Minimum reward in USDC"), - rewardMax: z.number().nonnegative().optional().describe("Maximum reward in USDC"), - deadlineHours: z - .number() - .int() - .positive() - .optional() - .describe("Only tasks expiring within this many hours"), - limit: z.number().int().positive().max(100).optional().default(20), - cursor: z.string().optional().describe("Pagination cursor returned by a previous call"), - }) - .describe("Filters for searching public TaskMarket tasks"); - -/** Input for retrieving a single task. */ -export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket task lookup"); - -/** Input for claiming a task as the current EVM wallet. */ -export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); - -/** Input for retrieving submissions owned by the current EVM wallet. */ -export const TaskMarketMySubmissionsSchema = z.object({ - taskId: taskId.optional().describe("Optionally restrict results to one task"), -}); - -/** - * A single text artifact. Text is intentionally explicit so an agent does not upload - * arbitrary local files without the caller's knowledge. - */ -export const TaskMarketSubmitTextSchema = z - .object({ - taskId, - fileName: z - .string() - .min(1) - .regex(/^[^\\/]+$/, "fileName must not contain path separators") - .describe("Public artifact filename"), - content: z.string().min(1).describe("Text content to submit as the artifact"), - mimeType: z.string().min(1).optional().default("text/plain"), - role: z - .enum(["preview", "source", "final", "attachment"]) - .optional() - .default("final") - .describe("TaskMarket artifact role"), - }) - .describe("Submit one explicitly provided text artifact to TaskMarket"); - - -import { z } from "zod"; - -const taskId = z - .string() - .min(1) - .describe("TaskMarket task identifier, usually a 0x-prefixed 32-byte hex value"); - -/** Filters accepted by the TaskMarket task search endpoint. */ -export const TaskMarketListTasksSchema = z - .object({ - status: z.string().optional().default("open").describe("Task status, for example open"), - phase: z - .enum(["active", "in_review", "awaiting_settlement", "resolved"]) - .optional() - .describe("Derived lifecycle phase"), - mode: z - .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) - .optional() - .describe("Task mode"), - tags: z.array(z.string()).optional().describe("Tags to match"), - rewardMin: z.number().nonnegative().optional().describe("Minimum reward in USDC"), - rewardMax: z.number().nonnegative().optional().describe("Maximum reward in USDC"), - deadlineHours: z - .number() - .int() - .positive() - .optional() - .describe("Only tasks expiring within this many hours"), - limit: z.number().int().positive().max(100).optional().default(20), - cursor: z.string().optional().describe("Pagination cursor returned by a previous call"), - }) - .describe("Filters for searching public TaskMarket tasks"); - -/** Input for retrieving a single task. */ -export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket task lookup"); - -/** Input for claiming a task as the current EVM wallet. */ -export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); - -/** Input for retrieving submissions owned by the current EVM wallet. */ -export const TaskMarketMySubmissionsSchema = z.object({ - taskId: taskId.optional().describe("Optionally restrict results to one task"), -}); - -/** - * A single text artifact. Text is intentionally explicit so an agent does not upload - * arbitrary local files without the caller's knowledge. - */ -export const TaskMarketSubmitTextSchema = z - .object({ - taskId, - fileName: z - .string() - .min(1) - .regex(/^[^\\/]+$/, "fileName must not contain path separators") - .describe("Public artifact filename"), - content: z.string().min(1).describe("Text content to submit as the artifact"), - mimeType: z.string().min(1).optional().default("text/plain"), - role: z - .enum(["preview", "source", "final", "attachment"]) - .optional() - .default("final") - .describe("TaskMarket artifact role"), - }) - .describe("Submit one explicitly provided text artifact to TaskMarket"); - - From c1611c50db42259c689ebbccb2cb697ed3894c78 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Fri, 14 Aug 2026 09:52:50 -0300 Subject: [PATCH 09/11] feat(agentkit): add TaskMarket action provider --- .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/taskmarket/README.md | 33 ++ .../src/action-providers/taskmarket/index.ts | 2 + .../action-providers/taskmarket/schemas.ts | 65 +++ .../taskmarketActionProvider.test.ts | 115 +++++ .../taskmarket/taskmarketActionProvider.ts | 428 ++++++++++++++++++ 6 files changed, 644 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/taskmarket/README.md create mode 100644 typescript/agentkit/src/action-providers/taskmarket/index.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/schemas.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..c012dc414 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -25,6 +25,7 @@ export * from "./morpho"; export * from "./opensea"; export * from "./spl"; export * from "./superfluid"; +export * from "./taskmarket"; export * from "./sushi"; export * from "./truemarkets"; export * from "./twitter"; diff --git a/typescript/agentkit/src/action-providers/taskmarket/README.md b/typescript/agentkit/src/action-providers/taskmarket/README.md new file mode 100644 index 000000000..ede674a11 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/README.md @@ -0,0 +1,33 @@ +# TaskMarket action provider + +`TaskMarketActionProvider` gives AgentKit agents a small, typed integration with +the TaskMarket REST API. + +## What it provides + +- `list_tasks` and `get_task` for public task discovery. +- `my_submissions` for read-only, wallet-authenticated submission history. +- `claim_task` for signing a documented claim intent. +- `submit_text` for uploading one explicit text artifact and submitting it + through TaskMarket's upload-key flow with x402 payment handling. + +Write actions are disabled by default. Enable them only after reviewing the +task and the intended artifact: + +```ts +import { AgentKit } from "@coinbase/agentkit"; +import { taskMarketActionProvider } from "@coinbase/agentkit"; + +const agentKit = await AgentKit.configureWithWallet({ + walletProvider, + actionProviders: [ + taskMarketActionProvider({ allowWriteActions: true }), + ], +}); +``` + +The provider never stores private keys or API tokens. It uses the configured +`EvmWalletProvider` to sign TaskMarket's read, claim, and submission messages. +It only uploads content supplied directly to `submit_text`; it does not read +local files. The final submission relay payment is capped at 1 USDC by default; +configure `maxPaymentUsdc` explicitly if a different limit is appropriate. diff --git a/typescript/agentkit/src/action-providers/taskmarket/index.ts b/typescript/agentkit/src/action-providers/taskmarket/index.ts new file mode 100644 index 000000000..7086e3d5a --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export * from "./taskmarketActionProvider"; diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts new file mode 100644 index 000000000..d11c204bd --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +const taskId = z + .string() + .min(1) + .describe("TaskMarket task identifier, usually a 0x-prefixed 32-byte hex value"); + +/** Filters accepted by the TaskMarket task search endpoint. */ +export const TaskMarketListTasksSchema = z + .object({ + status: z.string().optional().default("open").describe("Task status, for example open"), + phase: z + .enum(["active", "in_review", "awaiting_settlement", "resolved"]) + .optional() + .describe("Derived lifecycle phase"), + mode: z + .enum(["bounty", "claim", "pitch", "benchmark", "auction"]) + .optional() + .describe("Task mode"), + tags: z.array(z.string()).optional().describe("Tags to match"), + rewardMin: z.number().nonnegative().optional().describe("Minimum reward in USDC"), + rewardMax: z.number().nonnegative().optional().describe("Maximum reward in USDC"), + deadlineHours: z + .number() + .int() + .positive() + .optional() + .describe("Only tasks expiring within this many hours"), + limit: z.number().int().positive().max(100).optional().default(20), + cursor: z.string().optional().describe("Pagination cursor returned by a previous call"), + }) + .describe("Filters for searching public TaskMarket tasks"); + +/** Input for retrieving a single task. */ +export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket task lookup"); + +/** Input for claiming a task as the current EVM wallet. */ +export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); + +/** Input for retrieving submissions owned by the current EVM wallet. */ +export const TaskMarketMySubmissionsSchema = z.object({ + taskId: taskId.optional().describe("Optionally restrict results to one task"), +}); + +/** + * A single text artifact. Text is intentionally explicit so an agent does not upload + * arbitrary local files without the caller's knowledge. + */ +export const TaskMarketSubmitTextSchema = z + .object({ + taskId, + fileName: z + .string() + .min(1) + .regex(/^[^\\/]+$/, "fileName must not contain path separators") + .describe("Public artifact filename"), + content: z.string().min(1).describe("Text content to submit as the artifact"), + mimeType: z.string().min(1).optional().default("text/plain"), + role: z + .enum(["preview", "source", "final", "attachment"]) + .optional() + .default("final") + .describe("TaskMarket artifact role"), + }) + .describe("Submit one explicitly provided text artifact to TaskMarket"); diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts new file mode 100644 index 000000000..01ae72347 --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts @@ -0,0 +1,115 @@ +import { TaskMarketActionProvider } from "./taskmarketActionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; + +jest.mock("../../wallet-providers", () => { + /** Minimal wallet-provider base for the isolated provider tests. */ + class WalletProvider {} + + /** Minimal EVM wallet provider for the isolated provider tests. */ + class EvmWalletProvider extends WalletProvider {} + return { WalletProvider, EvmWalletProvider }; +}); + +const mockResponse = (body: unknown, status = 200): Response => + ({ + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(body), + }) as Response; + +const mockWallet = { + getAddress: () => "0x1111111111111111111111111111111111111111", + getName: () => "test-wallet", + getNetwork: () => ({ protocolFamily: "evm", networkId: "base-mainnet", chainId: "8453" }), + toSigner: () => ({ + address: "0x1111111111111111111111111111111111111111", + sign: jest.fn(), + signMessage: jest.fn(), + signTransaction: jest.fn(), + signTypedData: jest.fn(), + }), + readContract: jest.fn(), + signMessage: jest.fn(async (message: string) => `signature:${message}`), +} as unknown as jest.Mocked; + +describe("TaskMarketActionProvider", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("rejects an invalid x402 payment cap", () => { + expect(() => new TaskMarketActionProvider({ maxPaymentUsdc: -1 })).toThrow( + "maxPaymentUsdc must be a non-negative finite number", + ); + }); + + it("lists tasks through the public API", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValue(mockResponse({ tasks: [{ id: "task-1" }], hasMore: false })); + const provider = new TaskMarketActionProvider({ apiUrl: "https://taskmarket.test" }); + + const result = await provider.listTasks({ status: "open", limit: 5 }); + + expect(result).toContain('"task-1"'); + expect(fetchMock).toHaveBeenCalledWith( + "https://taskmarket.test/api/tasks?status=open&limit=5", + expect.objectContaining({ method: "GET" }), + ); + }); + + it("keeps wallet writes disabled by default", async () => { + const provider = new TaskMarketActionProvider({ apiUrl: "https://taskmarket.test" }); + + await expect(provider.claimTask(mockWallet, { taskId: "0xabc" })).rejects.toThrow( + "write actions are disabled", + ); + expect(mockWallet.signMessage).not.toHaveBeenCalled(); + }); + + it("claims only after writes are explicitly enabled", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValue(mockResponse({ claimId: "claim-1" })); + const provider = new TaskMarketActionProvider({ + apiUrl: "https://taskmarket.test", + allowWriteActions: true, + }); + + const result = await provider.claimTask(mockWallet, { taskId: "0xabc" }); + + expect(result).toContain("claim-1"); + expect(mockWallet.signMessage).toHaveBeenCalledWith("taskmarket:claim:0xabc"); + expect(fetchMock).toHaveBeenCalledWith( + "https://taskmarket.test/api/tasks/0xabc/claim", + expect.objectContaining({ method: "POST" }), + ); + }); + + it("uploads and submits one explicit text artifact", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValueOnce( + mockResponse({ uploadUrl: "https://upload.test/artifact", artifactKey: "key-1" }), + ) + .mockResolvedValueOnce(mockResponse(null)) + .mockResolvedValueOnce(mockResponse({ submissionId: "submission-1" })); + const provider = new TaskMarketActionProvider({ + apiUrl: "https://taskmarket.test", + allowWriteActions: true, + }); + + const result = await provider.submitText(mockWallet, { + taskId: "0xabc", + fileName: "answer.txt", + content: "hello TaskMarket", + mimeType: "text/plain", + role: "final", + }); + + expect(result).toContain("submission-1"); + expect(mockWallet.signMessage).toHaveBeenCalledWith("taskmarket:submit:0xabc"); + expect(mockWallet.signMessage).toHaveBeenCalledWith("taskmarket:submit:0xabc:key-1"); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); +}); diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts new file mode 100644 index 000000000..e2c0dc00c --- /dev/null +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts @@ -0,0 +1,428 @@ +import { createHash } from "node:crypto"; +import { z } from "zod"; +import { keccak256 } from "viem"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { + TaskMarketClaimTaskSchema, + TaskMarketGetTaskSchema, + TaskMarketListTasksSchema, + TaskMarketMySubmissionsSchema, + TaskMarketSubmitTextSchema, +} from "./schemas"; + +const DEFAULT_API_URL = "https://api.taskmarket.dev"; +const READ_AUTH_ADDRESS_HEADER = "X-Taskmarket-Caller-Address"; +const READ_AUTH_SIGNATURE_HEADER = "X-Taskmarket-Caller-Signature"; + +export interface TaskMarketActionProviderConfig { + /** TaskMarket API origin. Defaults to https://api.taskmarket.dev. */ + apiUrl?: string; + /** + * Enables claim and submit actions. It is false by default so a read-only agent + * cannot commit the wallet to a task or incur a relay fee accidentally. + */ + allowWriteActions?: boolean; + /** Request timeout in milliseconds. Defaults to 20 seconds. */ + requestTimeoutMs?: number; + /** Maximum x402 payment in USDC for a write action. Defaults to 1 USDC. */ + maxPaymentUsdc?: number; +} + +interface TaskMarketArtifact { + artifactKey: string; + fileName: string; + mimeType: string; + role: string; + sizeBytes: number; + sha256Hash: string; + keccak256Hash: string; +} + +type TaskMarketRequestOptions = Omit & { + body?: BodyInit | Record; +}; + +/** + * AgentKit actions for discovering and, when explicitly enabled, working on + * TaskMarket tasks. Reads use the public REST API; wallet-bound actions sign + * TaskMarket's documented intent messages with the configured EVM wallet. + */ +export class TaskMarketActionProvider extends ActionProvider { + private readonly apiUrl: string; + private readonly allowWriteActions: boolean; + private readonly requestTimeoutMs: number; + private readonly maxPaymentUsdc: number; + + /** + * Create a TaskMarket provider with read-only actions enabled by default. + * + * @param config - Provider configuration. + */ + constructor(config: TaskMarketActionProviderConfig = {}) { + super("taskmarket", []); + this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, ""); + this.allowWriteActions = config.allowWriteActions ?? false; + this.requestTimeoutMs = config.requestTimeoutMs ?? 20_000; + this.maxPaymentUsdc = config.maxPaymentUsdc ?? 1; + + if (!/^https?:\/\//i.test(this.apiUrl)) { + throw new Error("TaskMarket apiUrl must be an http(s) URL"); + } + if (!Number.isFinite(this.maxPaymentUsdc) || this.maxPaymentUsdc < 0) { + throw new Error("TaskMarket maxPaymentUsdc must be a non-negative finite number"); + } + } + + /** + * Search public TaskMarket tasks without using a wallet. + * + * @param args - Task search filters. + * @returns The API response as formatted JSON. + */ + @CreateAction({ + name: "list_tasks", + description: + "List public TaskMarket tasks. Use this to discover work before claiming or submitting anything.", + schema: TaskMarketListTasksSchema, + }) + async listTasks(args: z.infer): Promise { + const params = new URLSearchParams(); + params.set("status", args.status); + if (args.phase) params.set("phase", args.phase); + if (args.mode) params.set("mode", args.mode); + if (args.tags?.length) params.set("tags", args.tags.join(",")); + if (args.rewardMin !== undefined) { + params.set("minReward", String(Math.round(args.rewardMin * 1_000_000))); + } + if (args.rewardMax !== undefined) { + params.set("maxReward", String(Math.round(args.rewardMax * 1_000_000))); + } + if (args.deadlineHours !== undefined) params.set("deadlineHours", String(args.deadlineHours)); + params.set("limit", String(args.limit)); + if (args.cursor) params.set("cursor", args.cursor); + + return this.request(`/api/tasks?${params.toString()}`); + } + + /** + * Retrieve the complete public record for a task. + * + * @param args - Task identifier. + * @returns The API response as formatted JSON. + */ + @CreateAction({ + name: "get_task", + description: "Retrieve one public TaskMarket task by its task identifier.", + schema: TaskMarketGetTaskSchema, + }) + async getTask(args: z.infer): Promise { + return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}`); + } + + /** + * List submissions for the current wallet, with TaskMarket read authentication. + * + * @param walletProvider - Wallet used to sign the read request. + * @param args - Optional task filter. + * @returns The filtered API response as formatted JSON. + */ + @CreateAction({ + name: "my_submissions", + description: "List TaskMarket submissions made by the connected EVM wallet. This is read-only.", + schema: TaskMarketMySubmissionsSchema, + }) + async mySubmissions( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + const address = walletProvider.getAddress(); + const signature = await walletProvider.signMessage(`taskmarket:read:${address.toLowerCase()}`); + const result = await this.requestJson( + `/api/submissions/mine?workerAddress=${encodeURIComponent(address)}`, + { + headers: { + [READ_AUTH_ADDRESS_HEADER]: address, + [READ_AUTH_SIGNATURE_HEADER]: signature, + }, + }, + ); + if (args.taskId && Array.isArray(result)) { + const filtered = result.filter( + item => item && typeof item === "object" && "taskId" in item && item.taskId === args.taskId, + ); + return JSON.stringify(filtered, null, 2); + } + if ( + args.taskId && + result && + typeof result === "object" && + "submissions" in result && + Array.isArray(result.submissions) + ) { + const filtered = result.submissions.filter( + item => item && typeof item === "object" && "taskId" in item && item.taskId === args.taskId, + ); + return JSON.stringify({ ...result, submissions: filtered }, null, 2); + } + return JSON.stringify(result, null, 2); + } + + /** + * Claim a task after the caller explicitly enabled write actions. + * + * @param walletProvider - Wallet submitting the claim. + * @param args - Task identifier. + * @returns The API response as formatted JSON. + */ + @CreateAction({ + name: "claim_task", + description: + "Claim a TaskMarket task for the connected wallet. This is an external side effect and is disabled unless allowWriteActions is true.", + schema: TaskMarketClaimTaskSchema, + }) + async claimTask( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + this.assertWritesEnabled(); + const signature = await walletProvider.signMessage(`taskmarket:claim:${args.taskId}`); + return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}/claim`, { + method: "POST", + body: { + taskId: args.taskId, + workerAddress: walletProvider.getAddress(), + signature, + }, + }); + } + + /** + * Submit one explicit text artifact through TaskMarket's upload-key flow. + * + * @param walletProvider - Wallet submitting the artifact. + * @param args - Explicit artifact contents and task metadata. + * @returns The API response as formatted JSON. + */ + @CreateAction({ + name: "submit_text", + description: + "Submit one explicitly provided text artifact to TaskMarket. This uploads content and may incur a relay fee; it is disabled unless allowWriteActions is true.", + schema: TaskMarketSubmitTextSchema, + }) + async submitText( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + this.assertWritesEnabled(); + const workerAddress = walletProvider.getAddress(); + const content = new TextEncoder().encode(args.content); + const uploadSignature = await walletProvider.signMessage(`taskmarket:submit:${args.taskId}`); + const uploadResponse = await this.requestJson<{ uploadUrl: string; artifactKey: string }>( + `/api/tasks/${encodeURIComponent(args.taskId)}/submissions/request-upload-url`, + { + method: "POST", + body: { + taskId: args.taskId, + workerAddress, + signature: uploadSignature, + fileName: args.fileName, + mimeType: args.mimeType, + role: args.role, + sizeBytes: content.byteLength, + }, + }, + ); + + const upload = uploadResponse; + const uploadResult = await fetch(upload.uploadUrl, { + method: "PUT", + headers: { "Content-Type": args.mimeType }, + body: content, + signal: AbortSignal.timeout(this.requestTimeoutMs), + }); + if (!uploadResult.ok) { + throw new Error(`TaskMarket artifact upload failed with status ${uploadResult.status}`); + } + + const artifact: TaskMarketArtifact = { + artifactKey: upload.artifactKey, + fileName: args.fileName, + mimeType: args.mimeType, + role: args.role, + sizeBytes: content.byteLength, + sha256Hash: createHash("sha256").update(content).digest("hex"), + keccak256Hash: keccak256(content), + }; + const submitSignature = await walletProvider.signMessage( + `taskmarket:submit:${args.taskId}:${artifact.artifactKey}`, + ); + + return this.requestWithX402( + `/api/tasks/${encodeURIComponent(args.taskId)}/submissions/from-keys`, + walletProvider, + { + method: "POST", + body: { + taskId: args.taskId, + workerAddress, + artifacts: [artifact], + signature: submitSignature, + }, + }, + ); + } + + /** + * Check whether this provider can run with an EVM network. + * + * @param network - Network descriptor supplied by AgentKit. + * @returns Whether the network uses the EVM protocol family. + */ + supportsNetwork(network: Network): boolean { + return network.protocolFamily.toLowerCase() === "evm"; + } + + /** + * Throw before any wallet-bound write action when writes are not approved. + */ + private assertWritesEnabled(): void { + if (!this.allowWriteActions) { + throw new Error( + "TaskMarket write actions are disabled. Set allowWriteActions: true only after reviewing the task and approving the side effect.", + ); + } + } + + /** + * Request JSON and return it in the string format expected by AgentKit actions. + * + * @param path - API path. + * @param options - Request options. + * @returns Formatted JSON response. + */ + private async request(path: string, options: TaskMarketRequestOptions = {}): Promise { + const result = await this.requestJson(path, options); + return JSON.stringify(result, null, 2); + } + + /** + * Request JSON with x402 payment handling for the final submission call. + * + * @param path - API path. + * @param walletProvider - Wallet used to authorize the x402 payment. + * @param options - Request options. + * @returns Formatted JSON response. + */ + private async requestWithX402( + path: string, + walletProvider: EvmWalletProvider, + options: TaskMarketRequestOptions = {}, + ): Promise { + const client = await this.createX402Client(walletProvider); + const result = await this.requestJson( + path, + options, + wrapFetchWithPayment(fetch, client), + ); + return JSON.stringify(result, null, 2); + } + + /** + * Perform a TaskMarket request with timeout and structured error handling. + * + * @param path - API path. + * @param options - Request options. + * @param fetchImplementation - Fetch implementation, optionally wrapped for x402 payments. + * @returns Parsed response payload. + */ + private async requestJson( + path: string, + options: TaskMarketRequestOptions = {}, + fetchImplementation: typeof fetch = fetch, + ): Promise { + const headers = new Headers(options.headers); + headers.set("Accept", "application/json"); + if (options.body !== undefined && !headers.has("Content-Type")) { + headers.set("Content-Type", "application/json"); + } + + const response = await fetchImplementation(`${this.apiUrl}${path}`, { + ...options, + method: options.method ?? "GET", + headers, + body: + options.body === undefined || typeof options.body === "string" + ? options.body + : JSON.stringify(options.body), + signal: options.signal ?? AbortSignal.timeout(this.requestTimeoutMs), + }); + + const text = await response.text(); + let payload: unknown = text; + try { + payload = text ? JSON.parse(text) : null; + } catch { + // Preserve non-JSON error bodies for diagnostics. + } + if (!response.ok) { + throw new Error( + `TaskMarket request failed with status ${response.status}: ${JSON.stringify(payload)}`, + ); + } + return payload as T; + } + + /** + * Create an x402 client for the connected EVM wallet and apply the payment cap. + * + * @param walletProvider - Wallet used to sign the payment authorization. + * @returns Configured x402 client. + */ + private async createX402Client(walletProvider: EvmWalletProvider): Promise { + const client = new x402Client(); + const account = walletProvider.toSigner(); + const signer = { + ...account, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + walletProvider.readContract({ + address: args.address, + abi: args.abi as never, + functionName: args.functionName as never, + args: args.args as never, + }), + }; + registerExactEvmScheme(client, { signer }); + + const maxPaymentBaseUnits = BigInt(Math.round(this.maxPaymentUsdc * 1_000_000)); + client.registerPolicy((_version, requirements) => { + const affordable = requirements.filter(requirement => { + try { + return BigInt(requirement.amount) <= maxPaymentBaseUnits; + } catch { + return false; + } + }); + if (affordable.length === 0) { + throw new Error( + `TaskMarket x402 payment exceeds the configured ${this.maxPaymentUsdc} USDC limit`, + ); + } + return affordable; + }); + + return client; + } +} + +export const taskMarketActionProvider = (config: TaskMarketActionProviderConfig = {}) => + new TaskMarketActionProvider(config); From 601a4cf203d19e69c8d257590190ae0953e96bc2 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Fri, 14 Aug 2026 09:55:31 -0300 Subject: [PATCH 10/11] chore: add TaskMarket changeset --- .changeset/tidy-wombats-add.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-wombats-add.md diff --git a/.changeset/tidy-wombats-add.md b/.changeset/tidy-wombats-add.md new file mode 100644 index 000000000..dcd523a83 --- /dev/null +++ b/.changeset/tidy-wombats-add.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Added a TaskMarket action provider for typed task discovery, wallet-authenticated submissions, and capped x402 relay payments. From a1189ce69bbcdedf0fc3d1b2db044a51256dfac3 Mon Sep 17 00:00:00 2001 From: frando0924-oss Date: Fri, 14 Aug 2026 10:02:13 -0300 Subject: [PATCH 11/11] feat(agentkit): support requester TaskMarket workflow --- .../src/action-providers/taskmarket/README.md | 7 ++ .../action-providers/taskmarket/schemas.ts | 30 +++++ .../taskmarketActionProvider.test.ts | 74 ++++++++++++ .../taskmarket/taskmarketActionProvider.ts | 106 ++++++++++++++++++ 4 files changed, 217 insertions(+) diff --git a/typescript/agentkit/src/action-providers/taskmarket/README.md b/typescript/agentkit/src/action-providers/taskmarket/README.md index ede674a11..560c70bc1 100644 --- a/typescript/agentkit/src/action-providers/taskmarket/README.md +++ b/typescript/agentkit/src/action-providers/taskmarket/README.md @@ -6,7 +6,9 @@ the TaskMarket REST API. ## What it provides - `list_tasks` and `get_task` for public task discovery. +- `create_task` for an explicitly confirmed, Base-mainnet bounty workflow. - `my_submissions` for read-only, wallet-authenticated submission history. +- `task_submissions` for read-only requester review of a task's submissions. - `claim_task` for signing a documented claim intent. - `submit_text` for uploading one explicit text artifact and submitting it through TaskMarket's upload-key flow with x402 payment handling. @@ -31,3 +33,8 @@ The provider never stores private keys or API tokens. It uses the configured It only uploads content supplied directly to `submit_text`; it does not read local files. The final submission relay payment is capped at 1 USDC by default; configure `maxPaymentUsdc` explicitly if a different limit is appropriate. + +`create_task` requires `confirmed: true`, validates that the reward does not +exceed `maxSpendUsdc`, embeds the exact deadline, deliverables, Base network, +and spend cap in the task description, and uses x402 to fund the escrow. The +provider never accepts or rejects submissions automatically. diff --git a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts index d11c204bd..58755588d 100644 --- a/typescript/agentkit/src/action-providers/taskmarket/schemas.ts +++ b/typescript/agentkit/src/action-providers/taskmarket/schemas.ts @@ -37,11 +37,41 @@ export const TaskMarketGetTaskSchema = z.object({ taskId }).describe("TaskMarket /** Input for claiming a task as the current EVM wallet. */ export const TaskMarketClaimTaskSchema = z.object({ taskId }).describe("TaskMarket claim request"); +/** Inputs for creating and funding a new Base-mainnet TaskMarket bounty. */ +export const TaskMarketCreateTaskSchema = z + .object({ + description: z.string().trim().min(1).describe("Human-readable task description"), + deliverables: z + .array(z.string().trim().min(1)) + .min(1) + .max(20) + .describe("Concrete deliverables the worker must provide"), + rewardUsdc: z.number().positive().describe("Escrowed reward in USDC"), + deadlineIso: z + .string() + .min(1) + .refine(value => Number.isFinite(Date.parse(value)), "deadlineIso must be an ISO date"), + network: z.literal("base-mainnet").describe("TaskMarket settlement network"), + maxSpendUsdc: z.number().nonnegative().describe("Maximum USDC the caller authorizes"), + confirmed: z + .literal(true) + .describe("Fresh explicit confirmation that the displayed task details may be funded"), + tags: z.array(z.string().trim().min(1)).max(20).optional().describe("Task tags"), + }) + .describe( + "Create a TaskMarket bounty only after showing description, reward, deadline, deliverables, Base network, and maximum spend to the user", + ); + /** Input for retrieving submissions owned by the current EVM wallet. */ export const TaskMarketMySubmissionsSchema = z.object({ taskId: taskId.optional().describe("Optionally restrict results to one task"), }); +/** Input for retrieving every submission to a requester-owned task. */ +export const TaskMarketTaskSubmissionsSchema = z + .object({ taskId }) + .describe("TaskMarket requester submission review request"); + /** * A single text artifact. Text is intentionally explicit so an agent does not upload * arbitrary local files without the caller's knowledge. diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts index 01ae72347..e909a4b80 100644 --- a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.test.ts @@ -33,6 +33,10 @@ const mockWallet = { } as unknown as jest.Mocked; describe("TaskMarketActionProvider", () => { + beforeEach(() => { + mockWallet.signMessage.mockClear(); + }); + afterEach(() => { jest.restoreAllMocks(); }); @@ -58,6 +62,58 @@ describe("TaskMarketActionProvider", () => { ); }); + it("requires explicit confirmation before creating a funded task", async () => { + const provider = new TaskMarketActionProvider({ + apiUrl: "https://taskmarket.test", + allowWriteActions: true, + maxPaymentUsdc: 1, + }); + + await expect( + provider.createTask(mockWallet, { + description: "Ship a tested integration", + deliverables: ["Public pull request"], + rewardUsdc: 0.5, + deadlineIso: new Date(Date.now() + 3_600_000).toISOString(), + network: "base-mainnet", + maxSpendUsdc: 0.5, + confirmed: false as never, + }), + ).rejects.toThrow("fresh explicit confirmation"); + }); + + it("creates a confirmed task with the requested guardrails", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValue(mockResponse({ taskId: "task-created" })); + const provider = new TaskMarketActionProvider({ + apiUrl: "https://taskmarket.test", + allowWriteActions: true, + maxPaymentUsdc: 1, + }); + + const result = await provider.createTask(mockWallet, { + description: "Ship a tested integration", + deliverables: ["Public pull request", "Reproduction logs"], + rewardUsdc: 0.5, + deadlineIso: new Date(Date.now() + 3_600_000).toISOString(), + network: "base-mainnet", + maxSpendUsdc: 0.5, + confirmed: true, + tags: ["integration"], + }); + + expect(result).toContain("task-created"); + const [input, init] = fetchMock.mock.calls[0] ?? []; + const serializedBody = + init?.body ?? (input instanceof Request ? await input.clone().text() : undefined); + const body = JSON.parse(String(serializedBody)); + expect(body.reward).toBe("500000"); + expect(body.description).toContain("Settlement network: Base mainnet (eip155:8453)"); + expect(body.description).toContain("Maximum authorized spend: 0.5 USDC"); + expect(body.description).toContain("- Public pull request"); + }); + it("keeps wallet writes disabled by default", async () => { const provider = new TaskMarketActionProvider({ apiUrl: "https://taskmarket.test" }); @@ -86,6 +142,24 @@ describe("TaskMarketActionProvider", () => { ); }); + it("retrieves task submissions with read authentication", async () => { + const fetchMock = jest + .spyOn(global, "fetch") + .mockResolvedValue(mockResponse({ submissions: [{ id: "submission-1" }] })); + const provider = new TaskMarketActionProvider({ apiUrl: "https://taskmarket.test" }); + + const result = await provider.taskSubmissions(mockWallet, { taskId: "0xabc" }); + + expect(result).toContain("submission-1"); + expect(fetchMock).toHaveBeenCalledWith( + "https://taskmarket.test/api/tasks/0xabc/submissions", + expect.objectContaining({ method: "GET" }), + ); + expect(mockWallet.signMessage).toHaveBeenCalledWith( + "taskmarket:read:0x1111111111111111111111111111111111111111", + ); + }); + it("uploads and submits one explicit text artifact", async () => { const fetchMock = jest .spyOn(global, "fetch") diff --git a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts index e2c0dc00c..042317bf2 100644 --- a/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts +++ b/typescript/agentkit/src/action-providers/taskmarket/taskmarketActionProvider.ts @@ -9,10 +9,12 @@ import { Network } from "../../network"; import { EvmWalletProvider } from "../../wallet-providers"; import { TaskMarketClaimTaskSchema, + TaskMarketCreateTaskSchema, TaskMarketGetTaskSchema, TaskMarketListTasksSchema, TaskMarketMySubmissionsSchema, TaskMarketSubmitTextSchema, + TaskMarketTaskSubmissionsSchema, } from "./schemas"; const DEFAULT_API_URL = "https://api.taskmarket.dev"; @@ -124,6 +126,69 @@ export class TaskMarketActionProvider extends ActionProvider return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}`); } + /** + * Create and fund a bounty after fresh confirmation of its exact parameters. + * + * @param walletProvider - Wallet used to fund the escrow through x402. + * @param args - Description, deliverables, deadline, network, and spend cap. + * @returns The TaskMarket response containing the created task identifier. + */ + @CreateAction({ + name: "create_task", + description: + "Create and fund a TaskMarket bounty on Base mainnet only after showing the user the exact description, reward, deadline, deliverables, network, and maximum spend. Requires confirmed=true and never creates a task silently.", + schema: TaskMarketCreateTaskSchema, + }) + async createTask( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + this.assertWritesEnabled(); + if (!args.confirmed) { + throw new Error("TaskMarket task creation requires fresh explicit confirmation"); + } + if (args.rewardUsdc > args.maxSpendUsdc) { + throw new Error("TaskMarket reward exceeds the caller's maximum authorized spend"); + } + if (args.maxSpendUsdc > this.maxPaymentUsdc) { + throw new Error( + `TaskMarket maxSpendUsdc exceeds the provider's configured ${this.maxPaymentUsdc} USDC payment cap`, + ); + } + + const deadlineMs = Date.parse(args.deadlineIso); + const durationHours = (deadlineMs - Date.now()) / (60 * 60 * 1_000); + if (!Number.isFinite(durationHours) || durationHours <= 0) { + throw new Error("TaskMarket deadlineIso must be in the future"); + } + + const description = [ + args.description.trim(), + "", + "Deliverables:", + ...args.deliverables.map(deliverable => `- ${deliverable.trim()}`), + "", + `Deadline (ISO): ${args.deadlineIso}`, + "Settlement network: Base mainnet (eip155:8453)", + `Maximum authorized spend: ${args.maxSpendUsdc} USDC`, + ].join("\n"); + + return this.requestWithX402("/api/tasks", walletProvider, { + method: "POST", + body: { + description, + reward: this.toUsdcBaseUnits(args.rewardUsdc), + duration: durationHours, + mode: "bounty", + taskVisibility: "public", + submissionVisibility: "public", + tags: args.tags ?? [], + stakeRequired: false, + stakeBps: 0, + }, + }); + } + /** * List submissions for the current wallet, with TaskMarket read authentication. * @@ -172,6 +237,33 @@ export class TaskMarketActionProvider extends ActionProvider return JSON.stringify(result, null, 2); } + /** + * Retrieve submissions for a task so a human can review them. + * + * @param walletProvider - Wallet used for TaskMarket read authentication. + * @param args - Task identifier. + * @returns The submission list as formatted JSON. + */ + @CreateAction({ + name: "task_submissions", + description: + "Retrieve all submissions for a TaskMarket task for human review. This action is read-only and never accepts or rejects work.", + schema: TaskMarketTaskSubmissionsSchema, + }) + async taskSubmissions( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + const address = walletProvider.getAddress(); + const signature = await walletProvider.signMessage(`taskmarket:read:${address.toLowerCase()}`); + return this.request(`/api/tasks/${encodeURIComponent(args.taskId)}/submissions`, { + headers: { + [READ_AUTH_ADDRESS_HEADER]: address, + [READ_AUTH_SIGNATURE_HEADER]: signature, + }, + }); + } + /** * Claim a task after the caller explicitly enabled write actions. * @@ -377,6 +469,20 @@ export class TaskMarketActionProvider extends ActionProvider return payload as T; } + /** + * Convert a finite USDC amount to six-decimal base units without accepting an unsafe integer. + * + * @param amount - Human-readable USDC amount. + * @returns Base-unit amount for the TaskMarket API. + */ + private toUsdcBaseUnits(amount: number): string { + const baseUnits = Math.round(amount * 1_000_000); + if (!Number.isSafeInteger(baseUnits) || baseUnits <= 0) { + throw new Error("TaskMarket rewardUsdc must convert to a positive safe USDC amount"); + } + return String(baseUnits); + } + /** * Create an x402 client for the connected EVM wallet and apply the payment cap. *