@bountyagents/bountyagents-task 2026.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.js ADDED
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ import { responseStatusSchema, taskStatusSchema } from '@bountyagents/task-db';
3
+ import { getAddress, isAddress } from 'viem';
4
+ const addressSchema = z
5
+ .string()
6
+ .refine((value) => isAddress(value), {
7
+ message: 'Invalid address'
8
+ })
9
+ .transform((value) => getAddress(value));
10
+ const tokenSchema = z.string().regex(/^[a-z0-9-]+:0x[a-fA-F0-9]{40}$/);
11
+ const priceStringSchema = z.string().regex(/^[0-9]+$/).refine((val) => BigInt(val) > 0n, {
12
+ message: 'price must be greater than zero'
13
+ });
14
+ const signatureSchema = z.string().regex(/^0x[a-fA-F0-9]{130}$/, { message: 'Invalid signature' });
15
+ export const createTaskPayloadSchema = z.object({
16
+ id: z.string().uuid(),
17
+ title: z.string().min(4),
18
+ content: z.string().min(1)
19
+ });
20
+ export const fundTaskPayloadSchema = z.object({
21
+ taskId: z.string().uuid(),
22
+ price: priceStringSchema,
23
+ token: tokenSchema
24
+ });
25
+ export const submitResponsePayloadSchema = z.object({
26
+ id: z.string().uuid().optional(),
27
+ taskId: z.string().uuid(),
28
+ payload: z.string().min(1)
29
+ });
30
+ export const decisionPayloadSchema = z
31
+ .object({
32
+ responseId: z.string().uuid(),
33
+ workerAddress: addressSchema,
34
+ price: z.string(),
35
+ status: responseStatusSchema,
36
+ settlementSignature: signatureSchema.optional()
37
+ })
38
+ .refine((value) => value.status !== 'pending', {
39
+ message: 'Status must be approved or rejected'
40
+ })
41
+ .refine((value) => (value.status === 'approved' ? Boolean(value.settlementSignature) : true), {
42
+ message: 'settlementSignature required when approving'
43
+ });
44
+ export const cancelTaskPayloadSchema = z.object({
45
+ taskId: z.string().uuid()
46
+ });
47
+ export const settleTaskPayloadSchema = z.object({
48
+ taskId: z.string().uuid(),
49
+ responseId: z.string().uuid()
50
+ });
51
+ const createdRangeSchema = z.tuple([z.number().nonnegative(), z.number().nonnegative()]).optional();
52
+ export const taskQueryPayloadSchema = z.object({
53
+ filter: z
54
+ .object({
55
+ publisher: addressSchema.optional(),
56
+ created_at: createdRangeSchema,
57
+ status: taskStatusSchema.optional(),
58
+ minPrice: z.number().int().nonnegative().optional(),
59
+ keyword: z.string().min(2).max(256).optional()
60
+ })
61
+ .default({}),
62
+ sortBy: z.enum(['price', 'created_at']).optional().default('created_at'),
63
+ pageSize: z.number().int().min(1).max(200).optional().default(50),
64
+ pageNum: z.number().int().min(0).optional().default(0)
65
+ });
66
+ export const taskResponsesQueryPayloadSchema = z.object({
67
+ taskId: z.string().uuid(),
68
+ workerAddress: addressSchema.optional(),
69
+ pageSize: z.number().int().min(1).max(200).optional().default(50),
70
+ pageNum: z.number().int().min(0).optional().default(0)
71
+ });
72
+ export const workerResponsesQueryPayloadSchema = z.object({
73
+ taskId: z.string().uuid().optional(),
74
+ pageSize: z.number().int().min(1).max(200).optional().default(50),
75
+ pageNum: z.number().int().min(0).optional().default(0)
76
+ });
77
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAE7C,MAAM,aAAa,GAAG,CAAC;KACpB,MAAM,EAAE;KACR,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,KAAsB,CAAC,EAAE;IACpD,OAAO,EAAE,iBAAiB;CAC3B,CAAC;KACD,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAsB,CAAC,CAAC,CAAC;AAE5D,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;AAEvE,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE;IACvF,OAAO,EAAE,iCAAiC;CAC3C,CAAC,CAAC;AAEH,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,sBAAsB,EAAE,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAC;AAEnG,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACrB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACzB,KAAK,EAAE,iBAAiB;IACxB,KAAK,EAAE,WAAW;CACnB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACzB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC;KACnC,MAAM,CAAC;IACN,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC7B,aAAa,EAAE,aAAa;IAC5B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,oBAAoB;IAC5B,mBAAmB,EAAE,eAAe,CAAC,QAAQ,EAAE;CAChD,CAAC;KACD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE;IAC7C,OAAO,EAAE,qCAAqC;CAC/C,CAAC;KACD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;IAC5F,OAAO,EAAE,6CAA6C;CACvD,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;CAC1B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACzB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;CAC9B,CAAC,CAAC;AAEH,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AAEpG,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,MAAM,EAAE,CAAC;SACN,MAAM,CAAC;QACN,SAAS,EAAE,aAAa,CAAC,QAAQ,EAAE;QACnC,UAAU,EAAE,kBAAkB;QAC9B,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE;QACnC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;QACnD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;KAC/C,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;IACd,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IACxE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACjE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;CACvD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACzB,aAAa,EAAE,aAAa,CAAC,QAAQ,EAAE;IACvC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACjE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;CACvD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IACpC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACjE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;CACvD,CAAC,CAAC"}
package/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { BountyAgentsPublisherPlugin } from "./src/index.js";
2
+ import { PrivateKeySigner } from "./src/signers.js";
3
+ import crypto from "node:crypto";
4
+
5
+ export default function (api: any) {
6
+ api.registerCli(
7
+ ({ program }: { program: any }) => {
8
+ program.command("createtask").action(async () => {
9
+ const signer = new PrivateKeySigner(
10
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
11
+ );
12
+ const plugin = new BountyAgentsPublisherPlugin(signer, {
13
+ serviceUrl: "http://localhost:3000",
14
+ contractAddress: "0x55D45aFA265d0381C8A81328FfeA408D2Dd45F40",
15
+ });
16
+
17
+ try {
18
+ const task = await plugin.executeTool(
19
+ "bountyagents.publisher.task.create",
20
+ {
21
+ id: crypto.randomUUID(),
22
+ title: "Test Task from CLI",
23
+ content: "This is a test task created via the CLI tool.",
24
+ }
25
+ );
26
+ console.log("Task created successfully:", task);
27
+ } catch (error) {
28
+ console.error("Failed to create task:", error);
29
+ }
30
+ });
31
+ },
32
+ { commands: ["createtask"] }
33
+ );
34
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "bountyagents-task",
3
+ "main": "./dist/index.js",
4
+ "skills": ["./skills"],
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {}
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@bountyagents/bountyagents-task",
3
+ "version": "2026.2.26",
4
+ "description": "BountyAgents Task Plugin for OpenClaw",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "bun build ./index.ts --outdir ./dist --target node --format esm --external openclaw/plugin-sdk",
9
+ "watch": "bun build ./index.ts --outdir ./dist --target node --format esm --external openclaw/plugin-sdk --watch",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "openclaw",
15
+ "openclaw-plugin",
16
+ "task",
17
+ "bountyagents"
18
+ ],
19
+ "dependencies": {
20
+ "@sinclair/typebox": "^0.34.48",
21
+ "zod": "^4.3.6"
22
+ },
23
+ "devDependencies": {
24
+ "openclaw": "*"
25
+ },
26
+ "peerDependencies": {
27
+ "openclaw": "*"
28
+ },
29
+ "openclaw": {
30
+ "extensions": [
31
+ "./dist/index.js"
32
+ ],
33
+ "install": {
34
+ "npmSpec": "@bountyagents/bountyagents-task",
35
+ "defaultChoice": "npm"
36
+ }
37
+ }
38
+ }
package/src/escrow.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { Hex, encodePacked, getAddress, keccak256, stringToBytes } from 'viem';
2
+
3
+ export const taskDepositKey = (taskId: string): Hex => keccak256(stringToBytes(taskId));
4
+
5
+ export const buildSettleDataHash = (
6
+ contractAddress: string,
7
+ key: Hex,
8
+ owner: string,
9
+ token: string,
10
+ worker: string,
11
+ amount: bigint
12
+ ): Hex => {
13
+ return keccak256(
14
+ encodePacked(
15
+ ['string', 'address', 'bytes32', 'address', 'address', 'address', 'uint256'],
16
+ [
17
+ 'SETTLE',
18
+ getAddress(contractAddress as `0x${string}`),
19
+ key,
20
+ getAddress(owner as `0x${string}`),
21
+ getAddress(token as `0x${string}`),
22
+ getAddress(worker as `0x${string}`),
23
+ amount
24
+ ]
25
+ )
26
+ );
27
+ };
package/src/index.ts ADDED
@@ -0,0 +1,413 @@
1
+ import { fetch } from "undici";
2
+ import { z } from "zod";
3
+ import {
4
+ TaskRecord,
5
+ ResponseRecord,
6
+ taskRecordSchema,
7
+ responseRecordSchema,
8
+ } from "@bountyagents/task-db";
9
+ import { getAddress, Hex } from "viem";
10
+ import { Signer } from "./signers.js";
11
+ import {
12
+ CancelTaskPayload,
13
+ CreateTaskPayload,
14
+ DecisionPayload,
15
+ FundTaskPayload,
16
+ SettleTaskPayload,
17
+ SubmitResponsePayload,
18
+ TaskQueryPayload,
19
+ TaskResponsesQueryPayload,
20
+ WorkerResponsesQueryPayload,
21
+ cancelTaskPayloadSchema,
22
+ createTaskPayloadSchema,
23
+ decisionPayloadSchema,
24
+ fundTaskPayloadSchema,
25
+ settleTaskPayloadSchema,
26
+ submitResponsePayloadSchema,
27
+ taskQueryPayloadSchema,
28
+ taskResponsesQueryPayloadSchema,
29
+ workerResponsesQueryPayloadSchema,
30
+ } from "./types.js";
31
+ import {
32
+ cancelTaskSignaturePayload,
33
+ decisionSignaturePayload,
34
+ responseSignaturePayload,
35
+ taskFundSignaturePayload,
36
+ taskResponsesQuerySignaturePayload,
37
+ taskSettleSignaturePayload,
38
+ taskSignaturePayload,
39
+ workerResponsesQuerySignaturePayload,
40
+ } from "./signing.js";
41
+ import { parseTokenIdentifier } from "./token.js";
42
+ import { buildSettleDataHash, taskDepositKey } from "./escrow.js";
43
+
44
+ export interface BountyAgentsPluginOptions {
45
+ serviceUrl: string;
46
+ contractAddress: string;
47
+ }
48
+
49
+ export interface PluginTool {
50
+ name: string;
51
+ description: string;
52
+ inputSchema: z.ZodTypeAny;
53
+ execute: (input: unknown) => Promise<unknown>;
54
+ }
55
+
56
+ const normalizeUrl = (url: string) => url.replace(/\/$/, "");
57
+
58
+ const taskResponseSchema = z.object({ task: taskRecordSchema });
59
+ const submissionResponseSchema = z.object({ response: responseRecordSchema });
60
+ const tasksListSchema = z.object({ tasks: z.array(taskRecordSchema) });
61
+ const responsesListSchema = z.object({
62
+ responses: z.array(responseRecordSchema),
63
+ });
64
+ const settleResponseSchema = z.object({
65
+ task: taskRecordSchema,
66
+ settlementSignature: z.string(),
67
+ });
68
+
69
+ abstract class BaseBountyPlugin {
70
+ protected readonly baseUrl: string;
71
+ protected readonly contractAddress: `0x${string}`;
72
+ private readonly tools: PluginTool[] = [];
73
+
74
+ constructor(
75
+ protected readonly signer: Signer,
76
+ options: BountyAgentsPluginOptions
77
+ ) {
78
+ this.baseUrl = normalizeUrl(options.serviceUrl);
79
+ this.contractAddress = getAddress(options.contractAddress as `0x${string}`);
80
+ }
81
+
82
+ getTools(): PluginTool[] {
83
+ return this.tools;
84
+ }
85
+
86
+ async executeTool(name: string, input: unknown): Promise<unknown> {
87
+ const tool = this.tools.find((t) => t.name === name);
88
+ if (!tool) {
89
+ throw new Error(`Tool ${name} not found`);
90
+ }
91
+ return tool.execute(input);
92
+ }
93
+
94
+ protected registerTool<TSchema extends z.ZodTypeAny>(
95
+ name: string,
96
+ description: string,
97
+ schema: TSchema,
98
+ executor: (input: z.infer<TSchema>) => Promise<unknown>
99
+ ) {
100
+ this.tools.push({
101
+ name,
102
+ description,
103
+ inputSchema: schema,
104
+ execute: (rawInput: unknown) => executor(schema.parse(rawInput)),
105
+ });
106
+ }
107
+
108
+ protected async request(
109
+ path: string,
110
+ body?: unknown,
111
+ method: "GET" | "POST" = "POST"
112
+ ): Promise<unknown> {
113
+ const response = await fetch(`${this.baseUrl}${path}`, {
114
+ method,
115
+ headers: {
116
+ "content-type": "application/json",
117
+ },
118
+ body: body ? JSON.stringify(body) : undefined,
119
+ });
120
+ const payload = await response.text();
121
+ const data = payload ? JSON.parse(payload) : {};
122
+ if (!response.ok) {
123
+ const errorMessage =
124
+ typeof data.message === "string" ? data.message : response.statusText;
125
+ throw new Error(`Service error: ${errorMessage}`);
126
+ }
127
+ return data;
128
+ }
129
+
130
+ protected signPayload(payload: string): Promise<Hex> {
131
+ return this.signer.signMessage(payload);
132
+ }
133
+
134
+ protected signDigest(digest: Hex): Promise<Hex> {
135
+ return this.signer.signDigest(digest);
136
+ }
137
+
138
+ protected async fetchTask(taskId: string): Promise<TaskRecord> {
139
+ const response = await this.request(`/tasks/${taskId}`, undefined, "GET");
140
+ return taskResponseSchema.parse(response).task;
141
+ }
142
+
143
+ protected async fetchResponse(responseId: string): Promise<ResponseRecord> {
144
+ const response = await this.request(
145
+ `/responses/${responseId}`,
146
+ undefined,
147
+ "GET"
148
+ );
149
+ return submissionResponseSchema.parse(response).response;
150
+ }
151
+ }
152
+
153
+ export class BountyAgentsPublisherPlugin extends BaseBountyPlugin {
154
+ constructor(signer: Signer, options: BountyAgentsPluginOptions) {
155
+ super(signer, options);
156
+ this.registerTool(
157
+ "bountyagents.publisher.task.create",
158
+ "Create a draft bounty task on the remote service",
159
+ createTaskPayloadSchema,
160
+ (input) => this.createTask(input)
161
+ );
162
+ this.registerTool(
163
+ "bountyagents.publisher.task.fund",
164
+ "Attach price/token metadata after depositing escrow funds",
165
+ fundTaskPayloadSchema,
166
+ (input) => this.fundTask(input)
167
+ );
168
+ this.registerTool(
169
+ "bountyagents.publisher.task.decision",
170
+ "Approve or reject a response and cache the settlement signature",
171
+ decisionPayloadSchema,
172
+ (input) => this.decideOnResponse(input)
173
+ );
174
+ this.registerTool(
175
+ "bountyagents.publisher.task.cancel",
176
+ "Cancel an active task and retrieve the admin withdraw signature",
177
+ cancelTaskPayloadSchema,
178
+ (input) => this.cancelTask(input)
179
+ );
180
+ this.registerTool(
181
+ "bountyagents.publisher.task.query",
182
+ "Search tasks with keyword, publisher and price filters",
183
+ taskQueryPayloadSchema,
184
+ (input) => this.queryTasks(input)
185
+ );
186
+ this.registerTool(
187
+ "bountyagents.publisher.task.response.query",
188
+ "List responses for a task (owner signature required)",
189
+ taskResponsesQueryPayloadSchema,
190
+ (input) => this.queryTaskResponses(input)
191
+ );
192
+ }
193
+
194
+ private async createTask(payload: CreateTaskPayload): Promise<TaskRecord> {
195
+ const body = {
196
+ ...payload,
197
+ ownerAddress: this.signer.address,
198
+ signature: await this.signPayload(taskSignaturePayload(payload)),
199
+ };
200
+ const response = await this.request("/tasks", body);
201
+ return taskResponseSchema.parse(response).task;
202
+ }
203
+
204
+ private async fundTask(payload: FundTaskPayload): Promise<TaskRecord> {
205
+ const body = {
206
+ ...payload,
207
+ ownerAddress: this.signer.address,
208
+ signature: await this.signPayload(taskFundSignaturePayload(payload)),
209
+ };
210
+ const response = await this.request(`/tasks/${payload.taskId}/fund`, body);
211
+ return taskResponseSchema.parse(response).task;
212
+ }
213
+
214
+ private async decideOnResponse(
215
+ payload: DecisionPayload
216
+ ): Promise<ResponseRecord> {
217
+ const responseRecord = await this.fetchResponse(payload.responseId);
218
+ const task = await this.fetchTask(responseRecord.task_id);
219
+ if (!task.token || task.price === "0") {
220
+ throw new Error("Task is not funded yet");
221
+ }
222
+ if (payload.price !== task.price) {
223
+ throw new Error("Price does not match funded amount");
224
+ }
225
+ const workerAddress = getAddress(payload.workerAddress as `0x${string}`);
226
+ if (workerAddress !== getAddress(responseRecord.worker as `0x${string}`)) {
227
+ throw new Error("Worker address mismatch");
228
+ }
229
+
230
+ const settlementSignature =
231
+ payload.status === "approved"
232
+ ? await this.createSettlementSignature(
233
+ responseRecord.task_id,
234
+ workerAddress,
235
+ payload.price,
236
+ task.token
237
+ )
238
+ : undefined;
239
+
240
+ const canonicalPayload: DecisionPayload = {
241
+ responseId: payload.responseId,
242
+ workerAddress,
243
+ price: payload.price,
244
+ status: payload.status,
245
+ settlementSignature,
246
+ };
247
+ const body = {
248
+ ...canonicalPayload,
249
+ ownerAddress: this.signer.address,
250
+ signature: await this.signPayload(
251
+ decisionSignaturePayload(canonicalPayload)
252
+ ),
253
+ };
254
+ const response = await this.request(
255
+ `/responses/${payload.responseId}/decision`,
256
+ body
257
+ );
258
+ return submissionResponseSchema.parse(response).response;
259
+ }
260
+
261
+ private async cancelTask(payload: CancelTaskPayload): Promise<TaskRecord> {
262
+ const body = {
263
+ ...payload,
264
+ ownerAddress: this.signer.address,
265
+ signature: await this.signPayload(cancelTaskSignaturePayload(payload)),
266
+ };
267
+ const response = await this.request(
268
+ `/tasks/${payload.taskId}/cancel`,
269
+ body
270
+ );
271
+ return taskResponseSchema.parse(response).task;
272
+ }
273
+
274
+ private async queryTasks(payload: TaskQueryPayload): Promise<TaskRecord[]> {
275
+ const canonicalFilter = (payload.filter ?? {}) as NonNullable<
276
+ TaskQueryPayload["filter"]
277
+ >;
278
+ const pageSize = payload.pageSize ?? 50;
279
+ const pageNum = payload.pageNum ?? 0;
280
+ const body = {
281
+ filter: canonicalFilter,
282
+ sortBy: payload.sortBy ?? "created_at",
283
+ pageSize,
284
+ pageNum,
285
+ };
286
+ const response = await this.request("/tasks/query", body);
287
+ return tasksListSchema.parse(response).tasks;
288
+ }
289
+
290
+ private async queryTaskResponses(
291
+ payload: TaskResponsesQueryPayload
292
+ ): Promise<ResponseRecord[]> {
293
+ const pageSize = payload.pageSize ?? 50;
294
+ const pageNum = payload.pageNum ?? 0;
295
+ const canonicalPayload = {
296
+ taskId: payload.taskId,
297
+ workerAddress: payload.workerAddress,
298
+ pageSize,
299
+ pageNum,
300
+ };
301
+ const body = {
302
+ ...canonicalPayload,
303
+ ownerAddress: this.signer.address,
304
+ signature: await this.signPayload(
305
+ taskResponsesQuerySignaturePayload({
306
+ ...canonicalPayload,
307
+ ownerAddress: this.signer.address,
308
+ })
309
+ ),
310
+ };
311
+ const response = await this.request("/tasks/responses/query", body);
312
+ return responsesListSchema.parse(response).responses;
313
+ }
314
+
315
+ private async createSettlementSignature(
316
+ taskId: string,
317
+ workerAddress: `0x${string}`,
318
+ price: string,
319
+ tokenIdentifier: string
320
+ ): Promise<Hex> {
321
+ const token = parseTokenIdentifier(tokenIdentifier);
322
+ const depositKey = taskDepositKey(taskId);
323
+ const digest = buildSettleDataHash(
324
+ this.contractAddress,
325
+ depositKey,
326
+ this.signer.address,
327
+ token.address,
328
+ workerAddress,
329
+ BigInt(price)
330
+ );
331
+ return this.signDigest(digest);
332
+ }
333
+ }
334
+
335
+ export class BountyAgentsWorkerPlugin extends BaseBountyPlugin {
336
+ constructor(signer: Signer, options: BountyAgentsPluginOptions) {
337
+ super(signer, options);
338
+ this.registerTool(
339
+ "bountyagents.worker.task.respond",
340
+ "Submit a response for an active task",
341
+ submitResponsePayloadSchema,
342
+ (input) => this.submitResponse(input)
343
+ );
344
+ this.registerTool(
345
+ "bountyagents.worker.response.query",
346
+ "List responses submitted by this worker",
347
+ workerResponsesQueryPayloadSchema,
348
+ (input) => this.queryWorkerResponses(input)
349
+ );
350
+ this.registerTool(
351
+ "bountyagents.worker.task.settle",
352
+ "Fetch settlement signature for an approved response",
353
+ settleTaskPayloadSchema,
354
+ (input) => this.settleTask(input)
355
+ );
356
+ }
357
+
358
+ private async submitResponse(
359
+ payload: SubmitResponsePayload
360
+ ): Promise<ResponseRecord> {
361
+ const body = {
362
+ ...payload,
363
+ workerAddress: this.signer.address,
364
+ signature: await this.signPayload(responseSignaturePayload(payload)),
365
+ };
366
+ const response = await this.request(
367
+ `/tasks/${payload.taskId}/responses`,
368
+ body
369
+ );
370
+ return submissionResponseSchema.parse(response).response;
371
+ }
372
+
373
+ private async queryWorkerResponses(
374
+ payload: WorkerResponsesQueryPayload
375
+ ): Promise<ResponseRecord[]> {
376
+ const pageSize = payload.pageSize ?? 50;
377
+ const pageNum = payload.pageNum ?? 0;
378
+ const canonicalPayload = {
379
+ taskId: payload.taskId,
380
+ pageSize,
381
+ pageNum,
382
+ };
383
+ const body = {
384
+ ...canonicalPayload,
385
+ workerAddress: this.signer.address,
386
+ signature: await this.signPayload(
387
+ workerResponsesQuerySignaturePayload({
388
+ ...canonicalPayload,
389
+ workerAddress: this.signer.address,
390
+ })
391
+ ),
392
+ };
393
+ const response = await this.request("/workers/responses/query", body);
394
+ return responsesListSchema.parse(response).responses;
395
+ }
396
+
397
+ private async settleTask(
398
+ payload: SettleTaskPayload
399
+ ): Promise<{ task: TaskRecord; settlementSignature: string }> {
400
+ const canonicalPayload = { ...payload, workerAddress: this.signer.address };
401
+ const body = {
402
+ ...canonicalPayload,
403
+ signature: await this.signPayload(
404
+ taskSettleSignaturePayload(canonicalPayload)
405
+ ),
406
+ };
407
+ const response = await this.request(
408
+ `/tasks/${payload.taskId}/settle`,
409
+ body
410
+ );
411
+ return settleResponseSchema.parse(response);
412
+ }
413
+ }
package/src/signers.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { Account, Address, Hex, hexToBytes } from 'viem';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+
4
+ export interface Signer {
5
+ readonly address: Address;
6
+ signMessage(message: string): Promise<Hex>;
7
+ signDigest(digest: Hex): Promise<Hex>;
8
+ }
9
+
10
+ export class PrivateKeySigner implements Signer {
11
+ private readonly account: Account;
12
+
13
+ constructor(privateKey: Hex) {
14
+ this.account = privateKeyToAccount(privateKey);
15
+ }
16
+
17
+ get address(): Address {
18
+ return this.account.address;
19
+ }
20
+
21
+ signMessage(message: string): Promise<Hex> {
22
+ const signMessageFn = this.account.signMessage;
23
+ if (!signMessageFn) {
24
+ throw new Error('Account does not support signMessage');
25
+ }
26
+ return signMessageFn({ message });
27
+ }
28
+
29
+ signDigest(digest: Hex): Promise<Hex> {
30
+ const signMessageFn = this.account.signMessage;
31
+ if (!signMessageFn) {
32
+ throw new Error('Account does not support signDigest');
33
+ }
34
+ return signMessageFn({ message: { raw: hexToBytes(digest) } });
35
+ }
36
+ }