From 05824ae24de126aedee8a55e9c84464dad9c3f78 Mon Sep 17 00:00:00 2001
From: pbtc21
Date: Fri, 6 Mar 2026 16:39:30 +0000
Subject: [PATCH 1/4] feat(bounty-scanner): add autonomous bounty hunting skill
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Scan open bounties, match them to installed skills by confidence score,
claim work, and track progress. Designed for dispatch loops — run `match`
every cycle to find new opportunities automatically.
Co-Authored-By: Claude Opus 4.6
---
bounty-scanner/AGENT.md | 41 +++++
bounty-scanner/SKILL.md | 67 +++++++
bounty-scanner/bounty-scanner.ts | 292 +++++++++++++++++++++++++++++++
3 files changed, 400 insertions(+)
create mode 100644 bounty-scanner/AGENT.md
create mode 100644 bounty-scanner/SKILL.md
create mode 100644 bounty-scanner/bounty-scanner.ts
diff --git a/bounty-scanner/AGENT.md b/bounty-scanner/AGENT.md
new file mode 100644
index 0000000..c1e3ad0
--- /dev/null
+++ b/bounty-scanner/AGENT.md
@@ -0,0 +1,41 @@
+---
+name: bounty-scanner
+skill: bounty-scanner
+description: Decision rules for autonomous bounty hunting
+---
+
+# Bounty Scanner — Agent Guide
+
+## When to Use
+
+- **Every dispatch cycle**: Run `match` to check for new bounties matching your skills
+- **After installing new skills**: Re-run `match` — your capabilities changed
+- **When idle**: If your task queue is empty, `scan` for work
+
+## Decision Logic
+
+1. Run `match` to get ranked bounty suggestions
+2. If a bounty scores above 0.7 confidence AND reward >= 1000 sats → consider claiming
+3. Before claiming, check if you have the prerequisites (wallet, signing, relevant protocol skills)
+4. After claiming, begin work immediately — unclaimed bounties go to faster agents
+
+## Safety Checks
+
+- Never claim a bounty you can't complete — reputation damage is permanent
+- Check if someone else already claimed it (status != "open")
+- Don't claim more than 2 bounties simultaneously — finish what you start
+
+## Error Handling
+
+| Error | Action |
+|-------|--------|
+| Bounty board unreachable | Retry once, then skip this cycle |
+| Bounty already claimed | Move to next match |
+| No matching bounties | Log and wait for next cycle |
+
+## Integration
+
+Pairs well with:
+- `ceo` — strategic decision on which bounties align with your thesis
+- `business-dev` — bounty completion builds reputation for future partnerships
+- `reputation` — completed bounties generate on-chain validation opportunities
diff --git a/bounty-scanner/SKILL.md b/bounty-scanner/SKILL.md
new file mode 100644
index 0000000..d97413d
--- /dev/null
+++ b/bounty-scanner/SKILL.md
@@ -0,0 +1,67 @@
+---
+name: bounty-scanner
+description: Autonomous bounty hunting — scan open bounties, match to your skills, claim and track work
+user-invocable: true
+arguments: scan | match | claim | status | my-claims
+entry: bounty-scanner/bounty-scanner.ts
+requires: [wallet]
+tags: [l2, read-only, infrastructure]
+---
+
+# Bounty Scanner
+
+Autonomous bounty discovery and tracking. Scans the AIBTC bounty board, matches open bounties to your installed skills, and helps you claim and track work.
+
+## Why This Skill Exists
+
+Most agents check in and wait. This skill makes you **hunt**. It connects the bounty board to your capabilities and tells you exactly what to build next.
+
+## Commands
+
+### `scan`
+
+List all open bounties with rewards.
+
+```bash
+bun run bounty-scanner/bounty-scanner.ts scan
+```
+
+Returns: array of open bounties with id, title, reward, and posting date.
+
+### `match`
+
+Match open bounties to your installed skills and suggest the best fit.
+
+```bash
+bun run bounty-scanner/bounty-scanner.ts match
+```
+
+Returns: ranked list of bounties you're most likely to complete, based on keyword matching against your installed skills and their descriptions.
+
+### `claim `
+
+Mark a bounty as claimed by your agent.
+
+```bash
+bun run bounty-scanner/bounty-scanner.ts claim
+```
+
+### `status`
+
+Check the overall bounty board health — open, claimed, completed counts.
+
+```bash
+bun run bounty-scanner/bounty-scanner.ts status
+```
+
+### `my-claims`
+
+List bounties you've claimed or completed.
+
+```bash
+bun run bounty-scanner/bounty-scanner.ts my-claims --address
+```
+
+## Autonomous Use
+
+This skill is designed for dispatch loops. Run `match` every cycle to find new opportunities. When confidence is high, auto-claim and begin work.
diff --git a/bounty-scanner/bounty-scanner.ts b/bounty-scanner/bounty-scanner.ts
new file mode 100644
index 0000000..f9811ec
--- /dev/null
+++ b/bounty-scanner/bounty-scanner.ts
@@ -0,0 +1,292 @@
+#!/usr/bin/env bun
+/**
+ * Bounty Scanner skill CLI
+ * Autonomous bounty hunting — scan, match, claim, and track bounties
+ *
+ * Usage: bun run bounty-scanner/bounty-scanner.ts [options]
+ */
+
+import { Command } from "commander";
+import { printJson, handleError } from "../src/lib/utils/cli.js";
+import { getWalletManager } from "../src/lib/services/wallet-manager.js";
+import { readFileSync, existsSync } from "fs";
+import { join } from "path";
+
+const BOUNTY_API = "https://1btc-news-api.p-d07.workers.dev";
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+async function fetchBounties(): Promise {
+ const res = await fetch(`${BOUNTY_API}/bounties`);
+ if (!res.ok) throw new Error(`Bounty API returned ${res.status}`);
+ const data = await res.json();
+ return (data as any).bounties ?? [];
+}
+
+function getStxAddress(address?: string): string {
+ if (address) return address;
+ const walletManager = getWalletManager();
+ const session = walletManager.getSessionInfo();
+ if (session?.stxAddress) return session.stxAddress;
+ throw new Error(
+ "No STX address provided and wallet is not unlocked. " +
+ "Either provide --address or unlock your wallet first."
+ );
+}
+
+/**
+ * Load installed skill names and descriptions from local SKILL.md files.
+ */
+function getInstalledSkills(): Array<{ name: string; description: string; tags: string[] }> {
+ const skills: Array<{ name: string; description: string; tags: string[] }> = [];
+ const repoRoot = join(import.meta.dir, "..");
+
+ // Read skills.json if it exists
+ const manifestPath = join(repoRoot, "skills.json");
+ if (existsSync(manifestPath)) {
+ try {
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
+ for (const skill of manifest.skills ?? []) {
+ skills.push({
+ name: skill.name ?? "",
+ description: skill.description ?? "",
+ tags: skill.tags ?? [],
+ });
+ }
+ return skills;
+ } catch {
+ // fall through to directory scan
+ }
+ }
+
+ return skills;
+}
+
+/**
+ * Score how well a bounty matches the agent's installed skills.
+ * Returns 0-1 confidence score.
+ */
+function scoreBountyMatch(
+ bounty: { title: string; description: string },
+ skills: Array<{ name: string; description: string; tags: string[] }>
+): { score: number; matchedSkills: string[]; reason: string } {
+ const bountyText = `${bounty.title} ${bounty.description}`.toLowerCase();
+ const matchedSkills: string[] = [];
+ let score = 0;
+
+ // Keyword matching against skill names and descriptions
+ for (const skill of skills) {
+ const skillWords = `${skill.name} ${skill.description} ${skill.tags.join(" ")}`.toLowerCase();
+ const skillTokens = skillWords.split(/[\s\-_,./]+/).filter((t) => t.length > 2);
+
+ let hits = 0;
+ for (const token of skillTokens) {
+ if (bountyText.includes(token)) hits++;
+ }
+
+ if (hits >= 2) {
+ matchedSkills.push(skill.name);
+ score += Math.min(hits * 0.15, 0.5);
+ }
+ }
+
+ // Bonus for having wallet/signing (most bounties need them)
+ const hasWallet = skills.some((s) => s.name === "wallet");
+ const hasSigning = skills.some((s) => s.name === "signing");
+ if (hasWallet) score += 0.1;
+ if (hasSigning) score += 0.1;
+
+ // Cap at 1.0
+ score = Math.min(score, 1.0);
+
+ const reason =
+ matchedSkills.length > 0
+ ? `Matches skills: ${matchedSkills.join(", ")}`
+ : "No direct skill match — may require new capabilities";
+
+ return { score: Math.round(score * 100) / 100, matchedSkills, reason };
+}
+
+// ---------------------------------------------------------------------------
+// CLI
+// ---------------------------------------------------------------------------
+
+const program = new Command()
+ .name("bounty-scanner")
+ .description("Autonomous bounty hunting — scan, match, claim, and track bounties");
+
+// -- scan -------------------------------------------------------------------
+program
+ .command("scan")
+ .description("List all open bounties with rewards")
+ .action(async () => {
+ try {
+ const bounties = await fetchBounties();
+ const open = bounties
+ .filter((b: any) => b.status === "open")
+ .map((b: any) => ({
+ id: b.id,
+ title: b.title,
+ reward: b.reward,
+ posted: b.created_at,
+ }));
+
+ printJson({
+ success: true,
+ openBounties: open.length,
+ bounties: open,
+ });
+ } catch (err) {
+ handleError(err);
+ }
+ });
+
+// -- match ------------------------------------------------------------------
+program
+ .command("match")
+ .description("Match open bounties to your installed skills")
+ .action(async () => {
+ try {
+ const bounties = await fetchBounties();
+ const skills = getInstalledSkills();
+ const open = bounties.filter((b: any) => b.status === "open");
+
+ const matches = open
+ .map((b: any) => {
+ const match = scoreBountyMatch(
+ { title: b.title, description: b.description ?? "" },
+ skills
+ );
+ return {
+ id: b.id,
+ title: b.title,
+ reward: b.reward,
+ confidence: match.score,
+ matchedSkills: match.matchedSkills,
+ reason: match.reason,
+ };
+ })
+ .sort((a, b) => b.confidence - a.confidence);
+
+ const recommended = matches.filter((m) => m.confidence >= 0.3);
+
+ printJson({
+ success: true,
+ installedSkills: skills.length,
+ openBounties: open.length,
+ recommendedBounties: recommended.length,
+ matches: matches.slice(0, 10),
+ action:
+ recommended.length > 0
+ ? `Top match: "${recommended[0].title}" (${recommended[0].confidence * 100}% confidence, ${recommended[0].reward} sats)`
+ : "No strong matches found. Install more skills or check back later.",
+ });
+ } catch (err) {
+ handleError(err);
+ }
+ });
+
+// -- claim ------------------------------------------------------------------
+program
+ .command("claim")
+ .argument("", "Bounty ID to claim")
+ .description("Claim a bounty for your agent")
+ .action(async (bountyId: string) => {
+ try {
+ const stxAddress = getStxAddress();
+
+ const res = await fetch(`${BOUNTY_API}/bounties/${bountyId}/claim`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ claimer: stxAddress }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ printJson({
+ success: false,
+ error: (data as any).error ?? `HTTP ${res.status}`,
+ bountyId,
+ });
+ return;
+ }
+
+ printJson({
+ success: true,
+ bountyId,
+ claimer: stxAddress,
+ message: "Bounty claimed. Start building and submit your PR.",
+ ...(data as object),
+ });
+ } catch (err) {
+ handleError(err);
+ }
+ });
+
+// -- status -----------------------------------------------------------------
+program
+ .command("status")
+ .description("Bounty board health — open, claimed, completed counts")
+ .action(async () => {
+ try {
+ const bounties = await fetchBounties();
+
+ const stats = {
+ total: bounties.length,
+ open: bounties.filter((b: any) => b.status === "open").length,
+ claimed: bounties.filter((b: any) => b.status === "claimed").length,
+ completed: bounties.filter((b: any) => b.status === "completed").length,
+ cancelled: bounties.filter((b: any) => b.status === "cancelled").length,
+ totalRewardsOpen: bounties
+ .filter((b: any) => b.status === "open")
+ .reduce((sum: number, b: any) => sum + (b.reward ?? 0), 0),
+ };
+
+ printJson({
+ success: true,
+ ...stats,
+ summary: `${stats.open} open bounties worth ${stats.totalRewardsOpen.toLocaleString()} sats`,
+ });
+ } catch (err) {
+ handleError(err);
+ }
+ });
+
+// -- my-claims --------------------------------------------------------------
+program
+ .command("my-claims")
+ .description("List bounties you have claimed or completed")
+ .option("--address ", "Your STX address")
+ .action(async (opts: { address?: string }) => {
+ try {
+ const stxAddress = getStxAddress(opts.address);
+ const bounties = await fetchBounties();
+
+ const mine = bounties.filter(
+ (b: any) =>
+ b.claimer === stxAddress ||
+ b.poster === stxAddress
+ );
+
+ printJson({
+ success: true,
+ agent: stxAddress,
+ claimed: mine.filter((b: any) => b.claimer === stxAddress).length,
+ posted: mine.filter((b: any) => b.poster === stxAddress).length,
+ bounties: mine.map((b: any) => ({
+ id: b.id,
+ title: b.title,
+ status: b.status,
+ reward: b.reward,
+ role: b.claimer === stxAddress ? "claimer" : "poster",
+ })),
+ });
+ } catch (err) {
+ handleError(err);
+ }
+ });
+
+program.parse();
From b1c562c629a2fda150f5aa8f71fcf0512f996f97 Mon Sep 17 00:00:00 2001
From: Jason Schrader
Date: Fri, 6 Mar 2026 12:03:48 -0700
Subject: [PATCH 2/4] fix(bounty-scanner): resolve all blocking review issues
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Implement getInstalledSkills() directory scan fallback (globs */SKILL.md
when skills.json is absent) so match command actually finds installed skills
- Add signed proof of identity to claim POST using Stacks message signing
(signMessageHashRsv) — prevents address spoofing
- Fix user-invocable: true → false per repo convention
- Fix tags: read-only → write (claim is a write operation)
- Add requires: [wallet, signing] since claim needs both
- Define Bounty interface to replace pervasive `any` types
- Rename my-claims → my-bounties (shows both claimed and posted)
- Clarify threshold semantics: 0.3 display, 0.7 auto-claim (AGENT.md + code)
Co-Authored-By: Claude Opus 4.6
---
bounty-scanner/AGENT.md | 7 +-
bounty-scanner/SKILL.md | 14 +-
bounty-scanner/bounty-scanner.ts | 226 ++++++++++++++++++++++++++-----
3 files changed, 201 insertions(+), 46 deletions(-)
diff --git a/bounty-scanner/AGENT.md b/bounty-scanner/AGENT.md
index c1e3ad0..dfe83d2 100644
--- a/bounty-scanner/AGENT.md
+++ b/bounty-scanner/AGENT.md
@@ -15,9 +15,10 @@ description: Decision rules for autonomous bounty hunting
## Decision Logic
1. Run `match` to get ranked bounty suggestions
-2. If a bounty scores above 0.7 confidence AND reward >= 1000 sats → consider claiming
-3. Before claiming, check if you have the prerequisites (wallet, signing, relevant protocol skills)
-4. After claiming, begin work immediately — unclaimed bounties go to faster agents
+2. Bounties with confidence >= 0.3 are shown as "recommended" in match output
+3. Only auto-claim if confidence >= 0.7 AND reward >= 1000 sats — lower scores need manual review
+4. Before claiming, check if you have the prerequisites (wallet must be unlocked, signing is used automatically)
+5. After claiming, begin work immediately — unclaimed bounties go to faster agents
## Safety Checks
diff --git a/bounty-scanner/SKILL.md b/bounty-scanner/SKILL.md
index d97413d..d0cbf7e 100644
--- a/bounty-scanner/SKILL.md
+++ b/bounty-scanner/SKILL.md
@@ -1,11 +1,11 @@
---
name: bounty-scanner
description: Autonomous bounty hunting — scan open bounties, match to your skills, claim and track work
-user-invocable: true
-arguments: scan | match | claim | status | my-claims
+user-invocable: false
+arguments: scan | match | claim | status | my-bounties
entry: bounty-scanner/bounty-scanner.ts
-requires: [wallet]
-tags: [l2, read-only, infrastructure]
+requires: [wallet, signing]
+tags: [l2, write, infrastructure]
---
# Bounty Scanner
@@ -54,12 +54,12 @@ Check the overall bounty board health — open, claimed, completed counts.
bun run bounty-scanner/bounty-scanner.ts status
```
-### `my-claims`
+### `my-bounties`
-List bounties you've claimed or completed.
+List bounties you've claimed or posted.
```bash
-bun run bounty-scanner/bounty-scanner.ts my-claims --address
+bun run bounty-scanner/bounty-scanner.ts my-bounties --address
```
## Autonomous Use
diff --git a/bounty-scanner/bounty-scanner.ts b/bounty-scanner/bounty-scanner.ts
index f9811ec..81051a5 100644
--- a/bounty-scanner/bounty-scanner.ts
+++ b/bounty-scanner/bounty-scanner.ts
@@ -9,20 +9,44 @@
import { Command } from "commander";
import { printJson, handleError } from "../src/lib/utils/cli.js";
import { getWalletManager } from "../src/lib/services/wallet-manager.js";
-import { readFileSync, existsSync } from "fs";
+import { signMessageHashRsv } from "@stacks/transactions";
+import { hashMessage } from "@stacks/encryption";
+import { bytesToHex } from "@stacks/common";
+import { readFileSync, existsSync, readdirSync } from "fs";
import { join } from "path";
const BOUNTY_API = "https://1btc-news-api.p-d07.workers.dev";
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface Bounty {
+ id: string;
+ title: string;
+ description?: string;
+ reward: number;
+ status: string;
+ claimer?: string;
+ poster?: string;
+ created_at: number;
+}
+
+interface SkillInfo {
+ name: string;
+ description: string;
+ tags: string[];
+}
+
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
-async function fetchBounties(): Promise {
+async function fetchBounties(): Promise {
const res = await fetch(`${BOUNTY_API}/bounties`);
if (!res.ok) throw new Error(`Bounty API returned ${res.status}`);
- const data = await res.json();
- return (data as any).bounties ?? [];
+ const data = (await res.json()) as { bounties?: Bounty[] };
+ return data.bounties ?? [];
}
function getStxAddress(address?: string): string {
@@ -37,17 +61,101 @@ function getStxAddress(address?: string): string {
}
/**
- * Load installed skill names and descriptions from local SKILL.md files.
+ * Get the active wallet account or throw a consistent error.
+ */
+function requireUnlockedWallet() {
+ const walletManager = getWalletManager();
+ const account = walletManager.getActiveAccount();
+ if (!account) {
+ throw new Error(
+ "Wallet is not unlocked. Use wallet/wallet.ts unlock first."
+ );
+ }
+ return account;
+}
+
+/**
+ * Sign a claim message proving control of the STX address.
+ * Uses the Stacks message signing format (same as signing skill's stacks-sign).
+ */
+function signClaimMessage(
+ bountyId: string,
+ stxAddress: string,
+ privateKey: string
+): string {
+ const message = `claim:${bountyId}:${stxAddress}:${Date.now()}`;
+ const msgHash = hashMessage(message);
+ const msgHashHex = bytesToHex(msgHash);
+ const signature = signMessageHashRsv({
+ messageHash: msgHashHex,
+ privateKey,
+ });
+ return signature;
+}
+
+/**
+ * Parse simple YAML frontmatter from a SKILL.md file.
+ * Extracts name, description, and tags fields.
*/
-function getInstalledSkills(): Array<{ name: string; description: string; tags: string[] }> {
- const skills: Array<{ name: string; description: string; tags: string[] }> = [];
+function parseFrontmatter(content: string): SkillInfo | null {
+ const lines = content.split("\n");
+ let inFrontmatter = false;
+ const fields: Record = {};
+
+ for (const line of lines) {
+ if (line.trim() === "---") {
+ if (!inFrontmatter) {
+ inFrontmatter = true;
+ continue;
+ } else {
+ break;
+ }
+ }
+ if (inFrontmatter) {
+ const colonIdx = line.indexOf(":");
+ if (colonIdx > 0) {
+ const key = line.slice(0, colonIdx).trim();
+ const value = line.slice(colonIdx + 1).trim();
+ fields[key] = value;
+ }
+ }
+ }
+
+ if (!fields.name) return null;
+
+ // Parse bracket list for tags: [l2, write, infrastructure]
+ let tags: string[] = [];
+ if (fields.tags) {
+ const raw = fields.tags;
+ if (raw.startsWith("[") && raw.endsWith("]")) {
+ tags = raw
+ .slice(1, -1)
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0);
+ }
+ }
+
+ return {
+ name: fields.name,
+ description: fields.description ?? "",
+ tags,
+ };
+}
+
+/**
+ * Load installed skill names and descriptions.
+ * First tries skills.json manifest, then falls back to scanning SKILL.md files.
+ */
+function getInstalledSkills(): SkillInfo[] {
const repoRoot = join(import.meta.dir, "..");
- // Read skills.json if it exists
+ // Try skills.json first (faster)
const manifestPath = join(repoRoot, "skills.json");
if (existsSync(manifestPath)) {
try {
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
+ const skills: SkillInfo[] = [];
for (const skill of manifest.skills ?? []) {
skills.push({
name: skill.name ?? "",
@@ -55,12 +163,43 @@ function getInstalledSkills(): Array<{ name: string; description: string; tags:
tags: skill.tags ?? [],
});
}
- return skills;
+ if (skills.length > 0) return skills;
} catch {
// fall through to directory scan
}
}
+ // Directory scan fallback: find all */SKILL.md files
+ const skills: SkillInfo[] = [];
+ try {
+ const entries = readdirSync(repoRoot, { withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ // Skip non-skill directories
+ if (
+ entry.name.startsWith(".") ||
+ entry.name === "node_modules" ||
+ entry.name === "src" ||
+ entry.name === "scripts" ||
+ entry.name === "dist"
+ ) {
+ continue;
+ }
+ const skillMdPath = join(repoRoot, entry.name, "SKILL.md");
+ if (existsSync(skillMdPath)) {
+ try {
+ const content = readFileSync(skillMdPath, "utf-8");
+ const info = parseFrontmatter(content);
+ if (info) skills.push(info);
+ } catch {
+ // skip unreadable files
+ }
+ }
+ }
+ } catch {
+ // repo root unreadable — return empty
+ }
+
return skills;
}
@@ -70,7 +209,7 @@ function getInstalledSkills(): Array<{ name: string; description: string; tags:
*/
function scoreBountyMatch(
bounty: { title: string; description: string },
- skills: Array<{ name: string; description: string; tags: string[] }>
+ skills: SkillInfo[]
): { score: number; matchedSkills: string[]; reason: string } {
const bountyText = `${bounty.title} ${bounty.description}`.toLowerCase();
const matchedSkills: string[] = [];
@@ -78,8 +217,11 @@ function scoreBountyMatch(
// Keyword matching against skill names and descriptions
for (const skill of skills) {
- const skillWords = `${skill.name} ${skill.description} ${skill.tags.join(" ")}`.toLowerCase();
- const skillTokens = skillWords.split(/[\s\-_,./]+/).filter((t) => t.length > 2);
+ const skillWords =
+ `${skill.name} ${skill.description} ${skill.tags.join(" ")}`.toLowerCase();
+ const skillTokens = skillWords
+ .split(/[\s\-_,./]+/)
+ .filter((t) => t.length > 2);
let hits = 0;
for (const token of skillTokens) {
@@ -115,7 +257,9 @@ function scoreBountyMatch(
const program = new Command()
.name("bounty-scanner")
- .description("Autonomous bounty hunting — scan, match, claim, and track bounties");
+ .description(
+ "Autonomous bounty hunting — scan, match, claim, and track bounties"
+ );
// -- scan -------------------------------------------------------------------
program
@@ -125,8 +269,8 @@ program
try {
const bounties = await fetchBounties();
const open = bounties
- .filter((b: any) => b.status === "open")
- .map((b: any) => ({
+ .filter((b) => b.status === "open")
+ .map((b) => ({
id: b.id,
title: b.title,
reward: b.reward,
@@ -151,10 +295,10 @@ program
try {
const bounties = await fetchBounties();
const skills = getInstalledSkills();
- const open = bounties.filter((b: any) => b.status === "open");
+ const open = bounties.filter((b) => b.status === "open");
const matches = open
- .map((b: any) => {
+ .map((b) => {
const match = scoreBountyMatch(
{ title: b.title, description: b.description ?? "" },
skills
@@ -170,6 +314,8 @@ program
})
.sort((a, b) => b.confidence - a.confidence);
+ // Display threshold: 0.3 for showing recommendations
+ // Agent auto-claim threshold: 0.7 (see AGENT.md decision logic)
const recommended = matches.filter((m) => m.confidence >= 0.3);
printJson({
@@ -178,6 +324,7 @@ program
openBounties: open.length,
recommendedBounties: recommended.length,
matches: matches.slice(0, 10),
+ note: "Display threshold: 0.3 (recommended). Auto-claim threshold: 0.7 (see AGENT.md).",
action:
recommended.length > 0
? `Top match: "${recommended[0].title}" (${recommended[0].confidence * 100}% confidence, ${recommended[0].reward} sats)`
@@ -192,15 +339,24 @@ program
program
.command("claim")
.argument("", "Bounty ID to claim")
- .description("Claim a bounty for your agent")
+ .description("Claim a bounty for your agent (requires unlocked wallet)")
.action(async (bountyId: string) => {
try {
- const stxAddress = getStxAddress();
+ const account = requireUnlockedWallet();
+ const stxAddress = account.address;
+ const signature = signClaimMessage(
+ bountyId,
+ stxAddress,
+ account.privateKey
+ );
const res = await fetch(`${BOUNTY_API}/bounties/${bountyId}/claim`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ claimer: stxAddress }),
+ body: JSON.stringify({
+ claimer: stxAddress,
+ signature,
+ }),
});
const data = await res.json();
@@ -208,7 +364,7 @@ program
if (!res.ok) {
printJson({
success: false,
- error: (data as any).error ?? `HTTP ${res.status}`,
+ error: (data as Record).error ?? `HTTP ${res.status}`,
bountyId,
});
return;
@@ -236,13 +392,13 @@ program
const stats = {
total: bounties.length,
- open: bounties.filter((b: any) => b.status === "open").length,
- claimed: bounties.filter((b: any) => b.status === "claimed").length,
- completed: bounties.filter((b: any) => b.status === "completed").length,
- cancelled: bounties.filter((b: any) => b.status === "cancelled").length,
+ open: bounties.filter((b) => b.status === "open").length,
+ claimed: bounties.filter((b) => b.status === "claimed").length,
+ completed: bounties.filter((b) => b.status === "completed").length,
+ cancelled: bounties.filter((b) => b.status === "cancelled").length,
totalRewardsOpen: bounties
- .filter((b: any) => b.status === "open")
- .reduce((sum: number, b: any) => sum + (b.reward ?? 0), 0),
+ .filter((b) => b.status === "open")
+ .reduce((sum, b) => sum + (b.reward ?? 0), 0),
};
printJson({
@@ -255,10 +411,10 @@ program
}
});
-// -- my-claims --------------------------------------------------------------
+// -- my-bounties ------------------------------------------------------------
program
- .command("my-claims")
- .description("List bounties you have claimed or completed")
+ .command("my-bounties")
+ .description("List bounties you have claimed or posted")
.option("--address ", "Your STX address")
.action(async (opts: { address?: string }) => {
try {
@@ -266,17 +422,15 @@ program
const bounties = await fetchBounties();
const mine = bounties.filter(
- (b: any) =>
- b.claimer === stxAddress ||
- b.poster === stxAddress
+ (b) => b.claimer === stxAddress || b.poster === stxAddress
);
printJson({
success: true,
agent: stxAddress,
- claimed: mine.filter((b: any) => b.claimer === stxAddress).length,
- posted: mine.filter((b: any) => b.poster === stxAddress).length,
- bounties: mine.map((b: any) => ({
+ claimed: mine.filter((b) => b.claimer === stxAddress).length,
+ posted: mine.filter((b) => b.poster === stxAddress).length,
+ bounties: mine.map((b) => ({
id: b.id,
title: b.title,
status: b.status,
From 69ab7fa31331efab55cb748c0177c5dcca994376 Mon Sep 17 00:00:00 2001
From: Jason Schrader
Date: Fri, 6 Mar 2026 12:05:27 -0700
Subject: [PATCH 3/4] feat(bounty-scanner): add author attribution to
frontmatter
Co-Authored-By: Claude Opus 4.6
---
bounty-scanner/SKILL.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/bounty-scanner/SKILL.md b/bounty-scanner/SKILL.md
index d0cbf7e..69ece7c 100644
--- a/bounty-scanner/SKILL.md
+++ b/bounty-scanner/SKILL.md
@@ -1,6 +1,8 @@
---
name: bounty-scanner
description: Autonomous bounty hunting — scan open bounties, match to your skills, claim and track work
+author: pbtc21
+author_agent: Tiny Marten
user-invocable: false
arguments: scan | match | claim | status | my-bounties
entry: bounty-scanner/bounty-scanner.ts
From 576d57df756cbc1027df3947fe28c6fa8fec319e Mon Sep 17 00:00:00 2001
From: Jason Schrader
Date: Fri, 6 Mar 2026 12:58:26 -0700
Subject: [PATCH 4/4] =?UTF-8?q?fix(bounty-scanner):=20address=20review=20f?=
=?UTF-8?q?eedback=20=E2=80=94=20scoring,=20signing,=20parser?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix scoring inflation: wallet/signing bonus only applies when bounty
text mentions payment or signing keywords (was +0.2 unconditional)
- Fix claim signature: include message and timestamp in POST body so
server can reconstruct and verify the signed message
- Replace custom frontmatter parser with logic matching
scripts/generate-manifest.ts (parseBracketList, colonIdx=-1 guard)
- Add BOUNTY_API_URL env var override for staging/testing
Co-Authored-By: Claude Opus 4.6
---
bounty-scanner/bounty-scanner.ts | 84 +++++++++++++++++++-------------
1 file changed, 51 insertions(+), 33 deletions(-)
diff --git a/bounty-scanner/bounty-scanner.ts b/bounty-scanner/bounty-scanner.ts
index 81051a5..bb973f7 100644
--- a/bounty-scanner/bounty-scanner.ts
+++ b/bounty-scanner/bounty-scanner.ts
@@ -15,7 +15,8 @@ import { bytesToHex } from "@stacks/common";
import { readFileSync, existsSync, readdirSync } from "fs";
import { join } from "path";
-const BOUNTY_API = "https://1btc-news-api.p-d07.workers.dev";
+const BOUNTY_API =
+ process.env.BOUNTY_API_URL ?? "https://1btc-news-api.p-d07.workers.dev";
// ---------------------------------------------------------------------------
// Types
@@ -77,30 +78,54 @@ function requireUnlockedWallet() {
/**
* Sign a claim message proving control of the STX address.
* Uses the Stacks message signing format (same as signing skill's stacks-sign).
+ * Returns both the signature and the signed message so the server can verify.
+ *
+ * NOTE: The upstream bounty API at bounty.drx4.xyz uses BIP-322/BIP-137 BTC
+ * signatures with format: "agent-bounties | claim-bounty | {btc_address} |
+ * bounties/{uuid} | {timestamp}". This skill currently uses Stacks message
+ * signing against a different API. Full alignment is tracked upstream.
*/
function signClaimMessage(
bountyId: string,
stxAddress: string,
privateKey: string
-): string {
- const message = `claim:${bountyId}:${stxAddress}:${Date.now()}`;
+): { signature: string; message: string; timestamp: string } {
+ const timestamp = new Date().toISOString();
+ const message = `claim:${bountyId}:${stxAddress}:${timestamp}`;
const msgHash = hashMessage(message);
const msgHashHex = bytesToHex(msgHash);
const signature = signMessageHashRsv({
messageHash: msgHashHex,
privateKey,
});
- return signature;
+ return { signature, message, timestamp };
}
/**
- * Parse simple YAML frontmatter from a SKILL.md file.
- * Extracts name, description, and tags fields.
+ * Parse a bracket-list value like "[]" or "[wallet]" or "[l2, defi, write]".
+ * Matches the logic in scripts/generate-manifest.ts.
+ */
+function parseBracketList(raw: string): string[] {
+ const trimmed = raw.trim();
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
+ return trimmed.length > 0 ? [trimmed] : [];
+ }
+ const inner = trimmed.slice(1, -1).trim();
+ if (inner.length === 0) return [];
+ return inner
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0);
+}
+
+/**
+ * Parse YAML frontmatter from a SKILL.md file.
+ * Matches the parsing logic in scripts/generate-manifest.ts.
*/
function parseFrontmatter(content: string): SkillInfo | null {
const lines = content.split("\n");
let inFrontmatter = false;
- const fields: Record = {};
+ const frontmatterLines: string[] = [];
for (const line of lines) {
if (line.trim() === "---") {
@@ -112,34 +137,25 @@ function parseFrontmatter(content: string): SkillInfo | null {
}
}
if (inFrontmatter) {
- const colonIdx = line.indexOf(":");
- if (colonIdx > 0) {
- const key = line.slice(0, colonIdx).trim();
- const value = line.slice(colonIdx + 1).trim();
- fields[key] = value;
- }
+ frontmatterLines.push(line);
}
}
- if (!fields.name) return null;
-
- // Parse bracket list for tags: [l2, write, infrastructure]
- let tags: string[] = [];
- if (fields.tags) {
- const raw = fields.tags;
- if (raw.startsWith("[") && raw.endsWith("]")) {
- tags = raw
- .slice(1, -1)
- .split(",")
- .map((s) => s.trim())
- .filter((s) => s.length > 0);
- }
+ const fields: Record = {};
+ for (const line of frontmatterLines) {
+ const colonIdx = line.indexOf(":");
+ if (colonIdx === -1) continue;
+ const key = line.slice(0, colonIdx).trim();
+ const value = line.slice(colonIdx + 1).trim();
+ fields[key] = value;
}
+ if (!fields.name) return null;
+
return {
name: fields.name,
description: fields.description ?? "",
- tags,
+ tags: parseBracketList(fields.tags ?? "[]"),
};
}
@@ -234,11 +250,11 @@ function scoreBountyMatch(
}
}
- // Bonus for having wallet/signing (most bounties need them)
- const hasWallet = skills.some((s) => s.name === "wallet");
- const hasSigning = skills.some((s) => s.name === "signing");
- if (hasWallet) score += 0.1;
- if (hasSigning) score += 0.1;
+ // Bonus for wallet/signing only when bounty mentions payment or signing
+ const mentionsPayment = /pay|transfer|send|sats|btc|stx|sbtc|escrow|fund/i.test(bountyText);
+ const mentionsSigning = /sign|signature|verify|auth/i.test(bountyText);
+ if (mentionsPayment && skills.some((s) => s.name === "wallet")) score += 0.1;
+ if (mentionsSigning && skills.some((s) => s.name === "signing")) score += 0.1;
// Cap at 1.0
score = Math.min(score, 1.0);
@@ -344,7 +360,7 @@ program
try {
const account = requireUnlockedWallet();
const stxAddress = account.address;
- const signature = signClaimMessage(
+ const { signature, message, timestamp } = signClaimMessage(
bountyId,
stxAddress,
account.privateKey
@@ -356,6 +372,8 @@ program
body: JSON.stringify({
claimer: stxAddress,
signature,
+ message,
+ timestamp,
}),
});