diff --git a/bin/acp.ts b/bin/acp.ts index 2c37bc9..aac5cab 100755 --- a/bin/acp.ts +++ b/bin/acp.ts @@ -477,6 +477,18 @@ async function main(): Promise { if (subcommand === "status") return bounty.status(rest[0]); if (subcommand === "select") return bounty.select(rest[0]); if (subcommand === "cleanup") return bounty.cleanup(rest[0]); + if (subcommand === "apply") { + const offering = getFlagValue(args, "--offering"); + const priceRaw = getFlagValue(args, "--price"); + const priceType = getFlagValue(args, "--price-type"); + const note = getFlagValue(args, "--note"); + return bounty.apply(rest[0], { + offering: offering || undefined, + price: priceRaw ? parseFloat(priceRaw) : undefined, + priceType: priceType || undefined, + note: note || undefined, + }); + } console.log(buildCommandHelp("bounty")); return; } diff --git a/src/commands/bounty.ts b/src/commands/bounty.ts index 4184d20..215603e 100644 --- a/src/commands/bounty.ts +++ b/src/commands/bounty.ts @@ -22,7 +22,7 @@ import { syncBountyJobStatus, confirmMatch, } from "../lib/bounty.js"; -import { ROOT } from "../lib/config.js"; +import { ROOT, loadApiKey } from "../lib/config.js"; import { ensureBountyPollCron, removeBountyPollCronIfUnused, @@ -433,7 +433,7 @@ export async function poll(): Promise { result.checked += 1; try { // --- Claimed bounties: track ACP job status --- - if (b.status === "claimed" && b.acpJobId) { + if (b.status === "matched" && b.acpJobId) { let jobPhase = ""; let deliverable: string | undefined; try { @@ -491,7 +491,7 @@ export async function poll(): Promise { } const isNewPendingMatch = - status === "pending_match" && + status === "open" && Array.isArray(remote.candidates) && remote.candidates.length > 0 && !b.notifiedPendingMatch; @@ -633,7 +633,7 @@ export async function status(bountyId: string): Promise { } ); - if (!output.isJsonMode() && String(remote.status).toLowerCase() === "pending_match") { + if (!output.isJsonMode() && ["open","pending_match"].includes(String(remote.status).toLowerCase())) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -665,7 +665,7 @@ export async function select(bountyId: string): Promise { } const match = await getMatchStatus(bountyId); - if (String(match.status).toLowerCase() !== "pending_match") { + if (!["open","matched","pending_match"].includes(String(match.status).toLowerCase())) { output.fatal(`Bounty is not pending_match. Current status: ${match.status}`); } if (!Array.isArray(match.candidates) || match.candidates.length === 0) { @@ -797,7 +797,7 @@ export async function select(bountyId: string): Promise { const next: ActiveBounty = { ...active, - status: "claimed", + status: "matched", selectedCandidateId: candidateId, acpJobId, }; @@ -808,7 +808,7 @@ export async function select(bountyId: string): Promise { bountyId, candidateId, acpJobId, - status: "claimed", + status: "matched", }, (data) => { output.heading("Bounty Claimed"); @@ -845,3 +845,38 @@ export async function cleanup(bountyId: string): Promise { output.log(` Cleaned up bounty ${bountyId}\n`); } + +export async function apply(bountyId: string, flags: { + offering?: string; + price?: number; + priceType?: string; + note?: string; +}): Promise { + if (!bountyId) output.fatal("Usage: acp bounty apply --offering [--price 100] [--note 'why you']"); + + const agent = await requireActiveAgent(); + const apiKey = await loadApiKey(); + + const offering = flags.offering?.trim(); + if (!offering) output.fatal("--offering is required. Describe what you will deliver."); + + const input: BountyApplyInput = { + agent_wallet: agent.walletAddress, + agent_name: agent.name, + job_offering: offering, + ...(flags.price != null ? { price: flags.price } : {}), + ...(flags.priceType ? { price_type: flags.priceType as "fixed" | "percentage" } : {}), + ...(flags.note ? { note: flags.note } : {}), + }; + + const candidate = await applyToBounty(bountyId, input, apiKey); + + output.output({ bountyId, candidate }, (data) => { + output.heading("Applied to Bounty"); + output.field("Bounty ID", data.bountyId); + output.field("Your offering", offering); + if (flags.price != null) output.field("Proposed price", `${flags.price} USDC`); + if (flags.note) output.field("Note", flags.note); + output.log("\n Application submitted. The poster will see you in their candidate list.\n"); + }); +} diff --git a/src/lib/bounty.ts b/src/lib/bounty.ts index 0a715c0..90e1547 100644 --- a/src/lib/bounty.ts +++ b/src/lib/bounty.ts @@ -8,11 +8,15 @@ import * as path from "path"; import { ROOT, loadApiKey } from "./config.js"; export type BountyStatus = + // Active statuses (no ACP contract needed) | "open" - | "pending_match" - | "claimed" + | "matched" | "fulfilled" + | "cancelled" | "expired" + // Legacy aliases (old DB rows — still returned by API) + | "pending_match" + | "claimed" | "rejected"; export interface BountyCreateInput { @@ -29,9 +33,20 @@ export interface BountyMatchCandidate { name?: string; walletAddress?: string; offeringName?: string; + source?: "auto_match" | "applied"; // auto_match = ACP registry, applied = agent self-nominated + note?: string; // agent pitch (only for applied) [key: string]: unknown; } +export interface BountyApplyInput { + agent_wallet: string; + agent_name: string; + job_offering: string; + price?: number; + price_type?: "fixed" | "percentage"; + note?: string; +} + export interface BountyMatchStatusResponse { status: BountyStatus | string; candidates: BountyMatchCandidate[]; @@ -226,3 +241,16 @@ export async function syncBountyJobStatus(params: { return extractData(res.data); } + +export async function applyToBounty( + bountyId: string, + input: BountyApplyInput, + apiKey: string +): Promise { + const { data } = await axios.post<{ data?: BountyMatchCandidate }>( + `${process.env.ACP_BOUNTY_API_URL || "https://bounty.virtuals.io/api/v1"}/bounties/${bountyId}/apply`, + { ...input, api_key: apiKey }, + { headers: { "x-api-key": apiKey } } + ); + return extractData(data); +} diff --git a/src/lib/openclawCron.ts b/src/lib/openclawCron.ts index a396ecc..4b94437 100644 --- a/src/lib/openclawCron.ts +++ b/src/lib/openclawCron.ts @@ -19,7 +19,7 @@ const DEFAULT_SCHEDULE = "*/10 * * * *"; const POLL_SYSTEM_EVENT = [ `[ACP Bounty Poll] This is an automated bounty check. You MUST:`, `1. Run this command: cd "${ROOT}" && npx acp bounty poll --json`, - `2. Parse the JSON output and check the pendingMatch, claimedJobs, cleaned, and errors arrays.`, + `2. Parse the JSON output and check the pendingMatch, cleaned, and errors arrays.`, ``, `3. IF anything needs attention (non-empty arrays), you MUST use the "message" tool`, ` (action: "send") to proactively notify the user. Do NOT just reply in conversation —`, @@ -27,8 +27,8 @@ const POLL_SYSTEM_EVENT = [ ``, ` For pendingMatch: list bounty IDs, candidate agent names, offerings, and prices.`, ` Filter out irrelevant or malicious candidates. Ask which candidate to select.`, - ` For claimedJobs: report job phase/status.`, - ` For cleaned (completed/fulfilled/expired): inform user and share deliverables.`, + ` Note: claimedJobs tracking is disabled until the ACP contract is deployed.`, + ` For cleaned (fulfilled/expired/cancelled): inform user of outcome.`, ` For errors: report them.`, ``, `4. IF everything is empty (all arrays are empty or zero), reply HEARTBEAT_OK.`,