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
6 changes: 6 additions & 0 deletions typescript/agentkit/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# AgentKit Changelog

## Unreleased

### Patch Changes

- Added `taskmarketActionProvider` so agents can browse Taskmarket work, preview a Base (8453) create-task spend, create only after explicit user authorization via the official CLI, and present submissions for human review. Creates never auto-retry when settlement is unknown and the provider never accepts or rejects work.

## 0.11.0

### Minor Changes
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/DEMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Taskmarket Action Provider — Demo Log

Recorded 2026-08-13T23:15Z from the same public API the provider calls (`https://api.taskmarket.dev/api`).

## 1. Browse open tasks (`list_taskmarket_tasks`)

```
GET https://api.taskmarket.dev/api/tasks?status=open&limit=3
```

Returned 3 open bounties, including:

| id prefix | mode | status | reward (base units) |
|---|---|---|---|
| `0xdf65bccc07b3681f` | bounty | open | 8000 (0.008 USDC) |
| `0xfb182f610d57a6c0` | bounty | open | 398000 (0.398 USDC) |
| `0xf41d2979b5765bda` | bounty | open | 100000000 (100 USDC) |

No wallet, key, or spend involved.

## 2. Live status (`get_taskmarket_task`)

```
GET https://api.taskmarket.dev/api/tasks/0xdf65bccc07b3681f4028a45bfb31e2ce49f311c1e549e7a80be6d21915b84e4c
```

Response included `id`, `status=open`, `reward`, `expiryTime`, `tags`, `mode`. Public URL:

https://taskmarket.dev/tasks/0xdf65bccc07b3681f4028a45bfb31e2ce49f311c1e549e7a80be6d21915b84e4c

## 3. Preview then create (authorization)

`preview_taskmarket_task` returns the full spend preview (description, deliverables, reward, 7.5% platform fee, Base / chain 8453, max spend) plus a confirmation token. `create_taskmarket_task` is refused unless:

- `iAuthorizeSpend === true`
- the token matches the exact payload
- `rewardUsdc <= maxSpendUsdc`
- a previous create is not sitting in unknown-settlement

Covered by `taskmarketActionProvider.test.ts`.

## 4. Submissions stay human-reviewed

`list_taskmarket_submissions` returns `{ reviewOnly: true, autoAccept: false, autoReject: false }`. There is no accept/reject action on this provider.
84 changes: 84 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Taskmarket Action Provider

This directory contains the **TaskmarketActionProvider**, which lets an AgentKit agent treat [Taskmarket](https://taskmarket.dev/) as a delegated worker market on **Base mainnet (chain 8453)**.

## Directory Structure

```
taskmarket/
├── taskmarketActionProvider.ts # Provider implementation
├── taskmarketActionProvider.test.ts # Unit tests
├── schemas.ts # Zod action schemas
├── confirmation.ts # Preview confirmation tokens
├── api.ts # Public Taskmarket REST client
├── cli.ts # Official Taskmarket CLI wrapper
├── index.ts # Package exports
└── README.md # This file
```

## Actions

| Action | Spends? | Purpose |
|---|---|---|
| `list_taskmarket_tasks` | No | Browse open Taskmarket work |
| `get_taskmarket_task` | No | Live status, reward, deadline, URL |
| `preview_taskmarket_task` | No | Show description, reward, fee, Base network, max spend; issue confirmation token |
| `create_taskmarket_task` | Yes, via official CLI | Create/fund only after preview + `iAuthorizeSpend=true` |
| `list_taskmarket_submissions` | No | Present submissions for **human** review |

There is **no** accept, reject, or auto-pay action. Review stays with the user.

## Safety

- Default `maxSpendUsdc` is `0`. Creates are blocked until the operator sets a limit.
- Create requires a confirmation token from `preview_taskmarket_task` for the **exact** payload.
- Create requires `iAuthorizeSpend: true` from a fresh user authorization.
- Reward must be `<= maxSpendUsdc`.
- If the official CLI times out, settlement is treated as unknown and the provider **refuses to retry**.
- The provider never asks for, stores, or logs private keys. Creates go through the first-party [`taskmarket`](https://docs.taskmarket.dev/reference/cli) CLI and the user's existing keystore.

## Setup

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

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

const provider = taskmarketActionProvider({
maxSpendUsdc: 5, // hard cap per create
});
```

Optional env:

- `TASKMARKET_MAX_SPEND_USDC`
- `TASKMARKET_API_BASE` (default `https://api.taskmarket.dev/api`)
- `TASKMARKET_CLI_PATH` (default `taskmarket`)

## Reproduction

```bash
# from typescript/agentkit
pnpm test -- taskmarketActionProvider.test.ts
```

Browse live tasks without a wallet:

```ts
await provider.listTasks({ status: "open", limit: 5 });
```

## Network Support

Creates settle on Base mainnet only. `supportsNetwork` returns true for `base-mainnet` / chain `8453`.

## Docs

- https://taskmarket.dev/
- https://docs.taskmarket.dev/
- https://docs.taskmarket.dev/concepts/task-modes
- https://docs.taskmarket.dev/reference/cli
- https://api.taskmarket.dev/openapi.json
66 changes: 66 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export const DEFAULT_TASKMARKET_API_BASE = "https://api.taskmarket.dev/api";

export interface TaskmarketApiClient {
getJson(path: string): Promise<unknown>;
}

export class FetchTaskmarketApiClient implements TaskmarketApiClient {
constructor(private readonly apiBase: string = DEFAULT_TASKMARKET_API_BASE) {}

async getJson(path: string): Promise<unknown> {
const url = `${this.apiBase.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "coinbase-agentkit-taskmarket/0.1",
},
});

const text = await response.text();
let parsed: unknown = text;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = { raw: text };
}

if (!response.ok) {
throw new Error(`Taskmarket API ${response.status} for ${path}: ${text.slice(0, 400)}`);
}
return parsed;
}
}

export function toUsdc(rewardBaseUnits: string | number | undefined): number | null {
if (rewardBaseUnits === undefined || rewardBaseUnits === null) {
return null;
}
const asNumber = typeof rewardBaseUnits === "number" ? rewardBaseUnits : Number(rewardBaseUnits);
if (!Number.isFinite(asNumber)) {
return null;
}
return asNumber / 1_000_000;
}

export function summarizeTask(task: Record<string, unknown>): Record<string, unknown> {
const reward = toUsdc(task.reward as string | number | undefined);
const netReward = toUsdc(task.netReward as string | number | undefined);
return {
id: task.id,
status: task.status,
phase: task.phase,
mode: task.mode,
rewardUsdc: reward,
netRewardUsdc: netReward,
submissionCount: task.submissionCount,
expiryTime: task.expiryTime,
createdAt: task.createdAt,
tags: task.tags,
network: "Base",
chainId: 8453,
url: task.id ? `https://taskmarket.dev/tasks/${task.id}` : undefined,
descriptionPreview:
typeof task.description === "string" ? task.description.slice(0, 280) : undefined,
};
}
85 changes: 85 additions & 0 deletions typescript/agentkit/src/action-providers/taskmarket/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { spawn } from "child_process";

export interface CliResult {
exitCode: number | null;
timedOut: boolean;
stdout: string;
stderr: string;
}

export interface TaskmarketCli {
run(args: string[]): Promise<CliResult>;
}

export interface SpawnCliOptions {
command?: string;
timeoutMs?: number;
}

/**
* Runs the first-party Taskmarket CLI. Callers must not retry when timedOut is true
* or when exitCode is null — settlement status is unknown.
*/
export class SpawnTaskmarketCli implements TaskmarketCli {
private readonly command: string;
private readonly timeoutMs: number;

constructor(options: SpawnCliOptions = {}) {
this.command = options.command ?? process.env.TASKMARKET_CLI_PATH ?? "taskmarket";
this.timeoutMs = options.timeoutMs ?? 60_000;
}

run(args: string[]): Promise<CliResult> {
return new Promise(resolve => {
const child = spawn(this.command, args, {
shell: false,
windowsHide: true,
});

let stdout = "";
let stderr = "";
let settled = false;
const timer = setTimeout(() => {
child.kill();
finish({
exitCode: null,
timedOut: true,
stdout,
stderr: stderr + "\nCLI timed out; settlement status unknown. Do not retry.",
});
}, this.timeoutMs);

const finish = (result: CliResult) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve(result);
};

child.stdout.on("data", chunk => {
stdout += String(chunk);
});
child.stderr.on("data", chunk => {
stderr += String(chunk);
});
child.on("error", error => {
finish({
exitCode: null,
timedOut: false,
stdout,
stderr: error.message,
});
});
child.on("close", code => {
finish({
exitCode: code,
timedOut: false,
stdout,
stderr,
});
});
});
}
}
Loading
Loading