spendos 0.1.0
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/.dockerignore +4 -0
- package/.env.example +30 -0
- package/AGENTS.md +212 -0
- package/BOOTSTRAP.md +55 -0
- package/Dockerfile +52 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SOUL.md +202 -0
- package/SUBMISSION.md +128 -0
- package/TOOLS.md +40 -0
- package/USER.md +17 -0
- package/acp-seller/bin/acp.ts +807 -0
- package/acp-seller/config.json +34 -0
- package/acp-seller/package.json +55 -0
- package/acp-seller/src/commands/agent.ts +328 -0
- package/acp-seller/src/commands/bounty.ts +1189 -0
- package/acp-seller/src/commands/deploy.ts +414 -0
- package/acp-seller/src/commands/job.ts +217 -0
- package/acp-seller/src/commands/profile.ts +71 -0
- package/acp-seller/src/commands/resource.ts +91 -0
- package/acp-seller/src/commands/search.ts +327 -0
- package/acp-seller/src/commands/sell.ts +883 -0
- package/acp-seller/src/commands/serve.ts +258 -0
- package/acp-seller/src/commands/setup.ts +399 -0
- package/acp-seller/src/commands/token.ts +88 -0
- package/acp-seller/src/commands/wallet.ts +123 -0
- package/acp-seller/src/lib/api.ts +118 -0
- package/acp-seller/src/lib/auth.ts +291 -0
- package/acp-seller/src/lib/bounty.ts +257 -0
- package/acp-seller/src/lib/client.ts +42 -0
- package/acp-seller/src/lib/config.ts +240 -0
- package/acp-seller/src/lib/open.ts +41 -0
- package/acp-seller/src/lib/openclawCron.ts +138 -0
- package/acp-seller/src/lib/output.ts +104 -0
- package/acp-seller/src/lib/wallet.ts +81 -0
- package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
- package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
- package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
- package/acp-seller/src/seller/runtime/logger.ts +36 -0
- package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
- package/acp-seller/src/seller/runtime/offerings.ts +277 -0
- package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
- package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
- package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
- package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
- package/acp-seller/src/seller/runtime/seller.ts +1041 -0
- package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
- package/acp-seller/src/seller/runtime/startup.ts +270 -0
- package/acp-seller/src/seller/runtime/types.ts +62 -0
- package/acp-seller/tsconfig.json +20 -0
- package/bin/spendos.js +23 -0
- package/contracts/SpendOSAudit.sol +29 -0
- package/dist/mcp-server.mjs +153 -0
- package/jobs/translate.json +7 -0
- package/jobs/tweet-gen.json +7 -0
- package/openclaw.json +41 -0
- package/package.json +49 -0
- package/plugins/spendos-events/index.ts +78 -0
- package/plugins/spendos-events/package.json +14 -0
- package/policies/enforce-bounds.mjs +71 -0
- package/public/index.html +509 -0
- package/public/landing.html +241 -0
- package/railway.json +12 -0
- package/railway.toml +12 -0
- package/scripts/deploy.ts +48 -0
- package/scripts/test-x402-mainnet.ts +30 -0
- package/scripts/xmtp-listener.ts +61 -0
- package/setup.sh +278 -0
- package/skills/spendos/skill.md +26 -0
- package/src/agent.ts +152 -0
- package/src/audit.ts +166 -0
- package/src/governance.ts +367 -0
- package/src/job-registry.ts +306 -0
- package/src/mcp-public.ts +145 -0
- package/src/mcp-server.ts +171 -0
- package/src/opportunity-scanner.ts +138 -0
- package/src/server.ts +870 -0
- package/src/venice-x402.ts +234 -0
- package/src/xmtp.ts +109 -0
- package/src/zerion.ts +58 -0
- package/start.sh +168 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OpenClaw cron integration for bounty polling.
|
|
3
|
+
//
|
|
4
|
+
// Uses the OpenClaw Gateway CLI (`openclaw cron add/remove`) to manage a
|
|
5
|
+
// recurring cron job that polls bounty status.
|
|
6
|
+
//
|
|
7
|
+
// The Gateway cron system runs *agent turns* (not arbitrary shell commands).
|
|
8
|
+
// We create an isolated agentTurn job whose prompt instructs the agent to
|
|
9
|
+
// run `acp bounty poll --json` and act on the results.
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
import { ROOT, readConfig, writeConfig } from "./config.js";
|
|
14
|
+
import { listActiveBounties } from "./bounty.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_JOB_ID = "openclaw-acp-bounty-poll";
|
|
17
|
+
const DEFAULT_SCHEDULE = "*/10 * * * *";
|
|
18
|
+
|
|
19
|
+
const POLL_SYSTEM_EVENT = [
|
|
20
|
+
`[ACP Bounty Poll] This is an automated bounty check. You MUST:`,
|
|
21
|
+
`1. Run this command: cd "${ROOT}" && npx acp bounty poll --json`,
|
|
22
|
+
`2. Parse the JSON output and check the pendingMatch, claimedJobs, cleaned, and errors arrays.`,
|
|
23
|
+
``,
|
|
24
|
+
`3. IF anything needs attention (non-empty arrays), you MUST use the "message" tool`,
|
|
25
|
+
` (action: "send") to proactively notify the user. Do NOT just reply in conversation —`,
|
|
26
|
+
` use the message tool so the notification is pushed even if the user is not actively chatting.`,
|
|
27
|
+
``,
|
|
28
|
+
` For pendingMatch: list bounty IDs, candidate agent names, offerings, and prices.`,
|
|
29
|
+
` Filter out irrelevant or malicious candidates. Ask which candidate to select.`,
|
|
30
|
+
` For claimedJobs: report job phase/status.`,
|
|
31
|
+
` For cleaned (completed/fulfilled/expired): inform user and share deliverables.`,
|
|
32
|
+
` For errors: report them.`,
|
|
33
|
+
``,
|
|
34
|
+
`4. IF everything is empty (all arrays are empty or zero), reply HEARTBEAT_OK.`,
|
|
35
|
+
` Do NOT message the user when there is nothing to report.`,
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
function runCli(args: string[]): string {
|
|
39
|
+
return execSync(`openclaw cron ${args.join(" ")}`, {
|
|
40
|
+
cwd: ROOT,
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
}).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getBountyPollCronJobId(): string {
|
|
47
|
+
const cfg = readConfig();
|
|
48
|
+
return (
|
|
49
|
+
cfg.OPENCLAW_BOUNTY_CRON_JOB_ID ||
|
|
50
|
+
process.env.OPENCLAW_BOUNTY_CRON_JOB_ID ||
|
|
51
|
+
DEFAULT_JOB_ID
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ensureBountyPollCron(): { enabled: boolean; created: boolean } {
|
|
56
|
+
if (process.env.OPENCLAW_BOUNTY_CRON_DISABLED === "1") {
|
|
57
|
+
return { enabled: false, created: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cfg = readConfig();
|
|
61
|
+
if (cfg.OPENCLAW_BOUNTY_CRON_JOB_ID) {
|
|
62
|
+
// Cron job already registered — nothing to do.
|
|
63
|
+
return { enabled: true, created: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const schedule =
|
|
67
|
+
process.env.OPENCLAW_BOUNTY_CRON_SCHEDULE?.trim() || DEFAULT_SCHEDULE;
|
|
68
|
+
|
|
69
|
+
// Create a main-session systemEvent cron job via the OpenClaw CLI.
|
|
70
|
+
// This injects the poll instruction into the main session so the agent
|
|
71
|
+
// can present candidates interactively and ask the user to pick.
|
|
72
|
+
const result = runCli([
|
|
73
|
+
"add",
|
|
74
|
+
"--name",
|
|
75
|
+
JSON.stringify("ACP Bounty Poll"),
|
|
76
|
+
"--cron",
|
|
77
|
+
JSON.stringify(schedule),
|
|
78
|
+
"--session",
|
|
79
|
+
"main",
|
|
80
|
+
"--system-event",
|
|
81
|
+
JSON.stringify(POLL_SYSTEM_EVENT),
|
|
82
|
+
"--wake",
|
|
83
|
+
"now",
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Parse the job id from CLI output (JSON object with "id" field).
|
|
87
|
+
let jobId: string | undefined;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(result);
|
|
90
|
+
jobId = parsed.id || parsed.jobId;
|
|
91
|
+
} catch {
|
|
92
|
+
// If output isn't JSON, try to extract a UUID-like id.
|
|
93
|
+
const match = result.match(
|
|
94
|
+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
|
|
95
|
+
);
|
|
96
|
+
if (match) jobId = match[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!jobId) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Failed to parse cron job id from OpenClaw CLI output: ${result}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writeConfig({ ...cfg, OPENCLAW_BOUNTY_CRON_JOB_ID: jobId });
|
|
106
|
+
return { enabled: true, created: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function removeBountyPollCronIfUnused(): {
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
removed: boolean;
|
|
112
|
+
} {
|
|
113
|
+
if (process.env.OPENCLAW_BOUNTY_CRON_DISABLED === "1") {
|
|
114
|
+
return { enabled: false, removed: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const active = listActiveBounties();
|
|
118
|
+
if (active.length > 0) {
|
|
119
|
+
return { enabled: true, removed: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cfg = readConfig();
|
|
123
|
+
const jobId = cfg.OPENCLAW_BOUNTY_CRON_JOB_ID;
|
|
124
|
+
if (!jobId) {
|
|
125
|
+
return { enabled: true, removed: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
runCli(["remove", jobId]);
|
|
130
|
+
} catch {
|
|
131
|
+
// Job may already be gone — that's fine.
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const next = readConfig();
|
|
135
|
+
delete next.OPENCLAW_BOUNTY_CRON_JOB_ID;
|
|
136
|
+
writeConfig(next);
|
|
137
|
+
return { enabled: true, removed: true };
|
|
138
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Dual-mode output: human-friendly (default) vs JSON (--json flag / ACP_JSON=1)
|
|
3
|
+
// With ANSI color support for TTY terminals.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
let jsonMode = false;
|
|
7
|
+
|
|
8
|
+
export function setJsonMode(enabled: boolean): void {
|
|
9
|
+
jsonMode = enabled;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isJsonMode(): boolean {
|
|
13
|
+
return jsonMode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// -- ANSI colors (only when stdout is a TTY and not in JSON mode) --
|
|
17
|
+
|
|
18
|
+
const isTTY = process.stdout.isTTY === true;
|
|
19
|
+
|
|
20
|
+
const c = {
|
|
21
|
+
bold: (s: string) => (isTTY && !jsonMode ? `\x1b[1m${s}\x1b[0m` : s),
|
|
22
|
+
dim: (s: string) => (isTTY && !jsonMode ? `\x1b[2m${s}\x1b[0m` : s),
|
|
23
|
+
green: (s: string) => (isTTY && !jsonMode ? `\x1b[32m${s}\x1b[0m` : s),
|
|
24
|
+
red: (s: string) => (isTTY && !jsonMode ? `\x1b[31m${s}\x1b[0m` : s),
|
|
25
|
+
yellow: (s: string) => (isTTY && !jsonMode ? `\x1b[33m${s}\x1b[0m` : s),
|
|
26
|
+
cyan: (s: string) => (isTTY && !jsonMode ? `\x1b[36m${s}\x1b[0m` : s),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { c as colors };
|
|
30
|
+
|
|
31
|
+
// -- Output functions --
|
|
32
|
+
|
|
33
|
+
/** Print JSON to stdout (for --json mode or agent consumption). */
|
|
34
|
+
export function json(data: unknown): void {
|
|
35
|
+
console.log(JSON.stringify(data, null, jsonMode ? undefined : 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Print a line to stdout (human mode). Suppressed in JSON mode. */
|
|
39
|
+
export function log(msg: string): void {
|
|
40
|
+
if (!jsonMode) console.log(msg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Print an error line to stderr. Always shown. */
|
|
44
|
+
export function error(msg: string): void {
|
|
45
|
+
if (jsonMode) {
|
|
46
|
+
console.error(JSON.stringify({ error: msg }));
|
|
47
|
+
} else {
|
|
48
|
+
console.error(c.red(`Error: ${msg}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Print a success line (human mode). Suppressed in JSON mode. */
|
|
53
|
+
export function success(msg: string): void {
|
|
54
|
+
if (!jsonMode) console.log(c.green(` ${msg}`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Print a warning line (human mode). Suppressed in JSON mode. */
|
|
58
|
+
export function warn(msg: string): void {
|
|
59
|
+
if (!jsonMode) console.log(c.yellow(` Warning: ${msg}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Print a section heading. */
|
|
63
|
+
export function heading(title: string): void {
|
|
64
|
+
if (!jsonMode) {
|
|
65
|
+
console.log(`\n${c.bold(title)}`);
|
|
66
|
+
console.log(c.dim("-".repeat(50)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Print a key-value pair. */
|
|
71
|
+
export function field(
|
|
72
|
+
label: string,
|
|
73
|
+
value: string | number | boolean | null | undefined,
|
|
74
|
+
): void {
|
|
75
|
+
if (!jsonMode) {
|
|
76
|
+
console.log(` ${c.dim(label.padEnd(18))} ${value ?? "-"}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Output data in the appropriate mode.
|
|
82
|
+
* In JSON mode: prints JSON to stdout.
|
|
83
|
+
* In human mode: calls the formatter function.
|
|
84
|
+
*/
|
|
85
|
+
export function output(
|
|
86
|
+
data: unknown,
|
|
87
|
+
humanFormatter: (data: any) => void,
|
|
88
|
+
): void {
|
|
89
|
+
if (jsonMode) {
|
|
90
|
+
json(data);
|
|
91
|
+
} else {
|
|
92
|
+
humanFormatter(data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Fatal error — print and exit. */
|
|
97
|
+
export function fatal(msg: string): never {
|
|
98
|
+
error(msg);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatSymbol(symbol: string): string {
|
|
103
|
+
return symbol[0].startsWith("$") ? symbol : `$${symbol}`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Wallet / agent info retrieval.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import client from "./client.js";
|
|
6
|
+
import { readConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
export interface ActiveAgentInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
walletAddress: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Require an active agent. Returns { name, walletAddress }.
|
|
15
|
+
* If no active agent is found, exits with a helpful message telling the
|
|
16
|
+
* user/agent to run `acp agent list` or `acp agent switch` or `acp setup`.
|
|
17
|
+
*/
|
|
18
|
+
export async function requireActiveAgent(): Promise<ActiveAgentInfo> {
|
|
19
|
+
// First try the API
|
|
20
|
+
try {
|
|
21
|
+
const me = await getMyAgentInfo();
|
|
22
|
+
const name = me.name?.trim();
|
|
23
|
+
const walletAddress = me.walletAddress?.trim();
|
|
24
|
+
if (name && walletAddress) return { name, walletAddress };
|
|
25
|
+
} catch {
|
|
26
|
+
// fall through to local config check
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check local config for guidance
|
|
30
|
+
const config = readConfig();
|
|
31
|
+
const agents = config.agents ?? [];
|
|
32
|
+
const active = agents.find((a) => a.active);
|
|
33
|
+
|
|
34
|
+
if (active) {
|
|
35
|
+
// There IS an active agent but the API call failed (likely expired session/key)
|
|
36
|
+
console.error(
|
|
37
|
+
`Error: Active agent "${active.name}" found but API call failed. ` +
|
|
38
|
+
`Session may have expired. Run \`acp login\` to re-authenticate.`,
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (agents.length > 0) {
|
|
44
|
+
// Agents exist but none is active — tell them to pick one
|
|
45
|
+
const names = agents.map((a) => a.name).join(", ");
|
|
46
|
+
console.error(
|
|
47
|
+
`Error: No active agent selected. Available agents: ${names}\n` +
|
|
48
|
+
`Run \`acp agent switch <agent-name>\` to select one.`,
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// No agents at all
|
|
54
|
+
console.error("Error: No agents configured. Run `acp setup` to create one.");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getMyAgentInfo(): Promise<{
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
tokenAddress: string;
|
|
62
|
+
token: {
|
|
63
|
+
name: string;
|
|
64
|
+
symbol: string;
|
|
65
|
+
};
|
|
66
|
+
walletAddress: string;
|
|
67
|
+
jobs: {
|
|
68
|
+
name: string;
|
|
69
|
+
priceV2: {
|
|
70
|
+
type: string;
|
|
71
|
+
value: number;
|
|
72
|
+
};
|
|
73
|
+
slaMinutes: number;
|
|
74
|
+
requiredFunds: boolean;
|
|
75
|
+
deliverable: string;
|
|
76
|
+
requirement: Record<string, any>;
|
|
77
|
+
}[];
|
|
78
|
+
}> {
|
|
79
|
+
const agent = await client.get("/acp/me");
|
|
80
|
+
return agent.data.data;
|
|
81
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-transaction wallet scan helper.
|
|
3
|
+
*
|
|
4
|
+
* Calls the x402janus Guardian API to perform a lightweight wallet risk check
|
|
5
|
+
* before executing on-chain operations (revokes, approvals, etc.).
|
|
6
|
+
*
|
|
7
|
+
* This is an opt-in, non-blocking safety check:
|
|
8
|
+
* - Returns `null` on any error (network, timeout, bad response).
|
|
9
|
+
* - Logs a warning when the wallet's health score is below 50.
|
|
10
|
+
* - Never throws — callers can always ignore the result.
|
|
11
|
+
*
|
|
12
|
+
* Enable via environment variable: ACP_PRE_SCAN_ENABLED=true
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const GUARDIAN_API_URL =
|
|
16
|
+
process.env.GUARDIAN_API_URL || "https://x402janus.com";
|
|
17
|
+
|
|
18
|
+
const SCAN_TIMEOUT_MS = 20_000;
|
|
19
|
+
|
|
20
|
+
export interface ScanResult {
|
|
21
|
+
/** 0-100 health score. Lower means higher risk. */
|
|
22
|
+
healthScore: number;
|
|
23
|
+
/** Severity label returned by Guardian (e.g. "low", "medium", "high", "critical"). */
|
|
24
|
+
severity: string | null;
|
|
25
|
+
/** Raw analysis payload from Guardian for logging/audit. */
|
|
26
|
+
raw: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check whether pre-transaction scanning is enabled.
|
|
31
|
+
*
|
|
32
|
+
* Reads `ACP_PRE_SCAN_ENABLED` at call time so the value can change
|
|
33
|
+
* between invocations without restarting the process.
|
|
34
|
+
*/
|
|
35
|
+
export function isPreScanEnabled(): boolean {
|
|
36
|
+
return process.env.ACP_PRE_SCAN_ENABLED === "true";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run a lightweight Guardian scan on `walletAddress` before executing
|
|
41
|
+
* an on-chain operation.
|
|
42
|
+
*
|
|
43
|
+
* @param walletAddress - The 0x-prefixed wallet address to scan.
|
|
44
|
+
* @param tier - Scan tier to use. Defaults to `"free"` (the quick tier
|
|
45
|
+
* endpoint, which does not require payment).
|
|
46
|
+
* @returns A {@link ScanResult} on success, or `null` if the scan could
|
|
47
|
+
* not be completed for any reason.
|
|
48
|
+
*/
|
|
49
|
+
export async function runPreTransactionScan(
|
|
50
|
+
walletAddress: string,
|
|
51
|
+
tier: "free" | "quick" = "free",
|
|
52
|
+
): Promise<ScanResult | null> {
|
|
53
|
+
try {
|
|
54
|
+
const scanTier = tier === "free" ? "quick" : tier;
|
|
55
|
+
const url = `${GUARDIAN_API_URL}/api/guardian/scan/${encodeURIComponent(walletAddress)}?tier=${scanTier}`;
|
|
56
|
+
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeout = setTimeout(() => controller.abort(), SCAN_TIMEOUT_MS);
|
|
59
|
+
|
|
60
|
+
let response: Response;
|
|
61
|
+
try {
|
|
62
|
+
response = await fetch(url, {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: { Accept: "application/json" },
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`[preTransactionScan] Guardian API returned ${response.status} for ${walletAddress} — skipping pre-scan`,
|
|
74
|
+
);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const body: unknown = await response.json();
|
|
79
|
+
|
|
80
|
+
if (typeof body !== "object" || body === null) {
|
|
81
|
+
console.warn(
|
|
82
|
+
"[preTransactionScan] Unexpected response shape — skipping pre-scan",
|
|
83
|
+
);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const data = body as Record<string, unknown>;
|
|
88
|
+
const analysis = (data.analysis ?? data) as Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
const healthScore =
|
|
91
|
+
typeof analysis.overall_score === "number"
|
|
92
|
+
? analysis.overall_score
|
|
93
|
+
: typeof analysis.healthScore === "number"
|
|
94
|
+
? analysis.healthScore
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
if (healthScore === null) {
|
|
98
|
+
console.warn(
|
|
99
|
+
"[preTransactionScan] No health score in response — skipping pre-scan",
|
|
100
|
+
);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const severity =
|
|
105
|
+
typeof analysis.severity === "string" ? analysis.severity : null;
|
|
106
|
+
|
|
107
|
+
const result: ScanResult = { healthScore, severity, raw: data };
|
|
108
|
+
|
|
109
|
+
if (healthScore < 50) {
|
|
110
|
+
console.warn(
|
|
111
|
+
`[preTransactionScan] HIGH RISK wallet ${walletAddress}: healthScore=${healthScore}, severity=${severity ?? "unknown"}`,
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(
|
|
115
|
+
`[preTransactionScan] Wallet ${walletAddress}: healthScore=${healthScore}, severity=${severity ?? "unknown"}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
+
console.warn(
|
|
123
|
+
`[preTransactionScan] Pre-scan failed for ${walletAddress}: ${message} — continuing without scan`,
|
|
124
|
+
);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Canonical offering catalog — single source of truth for ACP offering names,
|
|
3
|
+
// prices, and required fields.
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
export interface CanonicalOffering {
|
|
7
|
+
/** Offering name — must match directory name and offering.json name exactly. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Fixed job fee in USDC. */
|
|
10
|
+
jobFee: number;
|
|
11
|
+
/** Human-readable price label for logs/docs. */
|
|
12
|
+
priceLabel: string;
|
|
13
|
+
/** Fee type for all canonical offerings. */
|
|
14
|
+
jobFeeType: "fixed";
|
|
15
|
+
/** Whether additional funds are required beyond the fixed fee. */
|
|
16
|
+
requiredFunds: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function roundUsd(value: number): number {
|
|
20
|
+
return Math.round(value * 1_000_000) / 1_000_000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatPriceLabel(jobFee: number): string {
|
|
24
|
+
const decimals = jobFee < 0.01 ? 4 : 2;
|
|
25
|
+
return `$${jobFee.toFixed(decimals)} USDC`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parsePositiveNumber(value: string | undefined): number | undefined {
|
|
29
|
+
if (!value) return undefined;
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function priceOverrideEnvKey(offeringName: string): string {
|
|
36
|
+
return `ACP_PRICE_OVERRIDE_${offeringName.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CANONICAL_CATALOG: Record<string, CanonicalOffering> = {
|
|
40
|
+
// -- Active ACP offerings (live) ----------------------------------------
|
|
41
|
+
x402janus_approvals: {
|
|
42
|
+
name: "x402janus_approvals",
|
|
43
|
+
jobFee: 0.01,
|
|
44
|
+
priceLabel: "$0.01 USDC",
|
|
45
|
+
jobFeeType: "fixed",
|
|
46
|
+
requiredFunds: false,
|
|
47
|
+
},
|
|
48
|
+
x402janus_revoke: {
|
|
49
|
+
name: "x402janus_revoke",
|
|
50
|
+
jobFee: 0.05,
|
|
51
|
+
priceLabel: "$0.05 USDC",
|
|
52
|
+
jobFeeType: "fixed",
|
|
53
|
+
requiredFunds: false,
|
|
54
|
+
},
|
|
55
|
+
x402janus_revoke_batch: {
|
|
56
|
+
name: "x402janus_revoke_batch",
|
|
57
|
+
jobFee: 0.1,
|
|
58
|
+
priceLabel: "$0.10 USDC",
|
|
59
|
+
jobFeeType: "fixed",
|
|
60
|
+
requiredFunds: false,
|
|
61
|
+
},
|
|
62
|
+
// -- Guardian scan tiers ------------------------------------------------
|
|
63
|
+
x402janus_scan_quick: {
|
|
64
|
+
name: "x402janus_scan_quick",
|
|
65
|
+
jobFee: 0.01,
|
|
66
|
+
priceLabel: "$0.01 USDC",
|
|
67
|
+
jobFeeType: "fixed",
|
|
68
|
+
requiredFunds: false,
|
|
69
|
+
},
|
|
70
|
+
x402janus_scan: {
|
|
71
|
+
name: "x402janus_scan",
|
|
72
|
+
jobFee: 0.05,
|
|
73
|
+
priceLabel: "$0.05 USDC",
|
|
74
|
+
jobFeeType: "fixed",
|
|
75
|
+
requiredFunds: false,
|
|
76
|
+
},
|
|
77
|
+
x402janus_scan_standard: {
|
|
78
|
+
name: "x402janus_scan_standard",
|
|
79
|
+
jobFee: 0.05,
|
|
80
|
+
priceLabel: "$0.05 USDC",
|
|
81
|
+
jobFeeType: "fixed",
|
|
82
|
+
requiredFunds: false,
|
|
83
|
+
},
|
|
84
|
+
x402janus_scan_deep: {
|
|
85
|
+
name: "x402janus_scan_deep",
|
|
86
|
+
jobFee: 0.25,
|
|
87
|
+
priceLabel: "$0.25 USDC",
|
|
88
|
+
jobFeeType: "fixed",
|
|
89
|
+
requiredFunds: false,
|
|
90
|
+
},
|
|
91
|
+
x402janus_forensic_intelligence: {
|
|
92
|
+
name: "x402janus_forensic_intelligence",
|
|
93
|
+
jobFee: 25,
|
|
94
|
+
priceLabel: "$25.00 USDC",
|
|
95
|
+
jobFeeType: "fixed",
|
|
96
|
+
requiredFunds: false,
|
|
97
|
+
},
|
|
98
|
+
// -- Compute & training -------------------------------------------------
|
|
99
|
+
x402janus_experiment_tracking: {
|
|
100
|
+
name: "x402janus_experiment_tracking",
|
|
101
|
+
jobFee: 0.1,
|
|
102
|
+
priceLabel: "$0.10 USDC",
|
|
103
|
+
jobFeeType: "fixed",
|
|
104
|
+
requiredFunds: false,
|
|
105
|
+
},
|
|
106
|
+
x402janus_gpu_compute: {
|
|
107
|
+
name: "x402janus_gpu_compute",
|
|
108
|
+
jobFee: 0.5,
|
|
109
|
+
priceLabel: "$0.50 USDC",
|
|
110
|
+
jobFeeType: "fixed",
|
|
111
|
+
requiredFunds: false,
|
|
112
|
+
},
|
|
113
|
+
x402janus_distributed_training: {
|
|
114
|
+
name: "x402janus_distributed_training",
|
|
115
|
+
jobFee: 5,
|
|
116
|
+
priceLabel: "$5.00 USDC",
|
|
117
|
+
jobFeeType: "fixed",
|
|
118
|
+
requiredFunds: false,
|
|
119
|
+
},
|
|
120
|
+
// -- Multi-tool agent jobs ----------------------------------------------
|
|
121
|
+
x402janus_multi_tool: {
|
|
122
|
+
name: "x402janus_multi_tool",
|
|
123
|
+
jobFee: 5,
|
|
124
|
+
priceLabel: "$5.00 USDC",
|
|
125
|
+
jobFeeType: "fixed",
|
|
126
|
+
requiredFunds: false,
|
|
127
|
+
},
|
|
128
|
+
// -- SpendOS autonomous agent services -----------------------------------
|
|
129
|
+
spendos_summarize_url: {
|
|
130
|
+
name: "spendos_summarize_url",
|
|
131
|
+
jobFee: 0.01,
|
|
132
|
+
priceLabel: "$0.01 USDC",
|
|
133
|
+
jobFeeType: "fixed",
|
|
134
|
+
requiredFunds: false,
|
|
135
|
+
},
|
|
136
|
+
spendos_tweet_gen: {
|
|
137
|
+
name: "spendos_tweet_gen",
|
|
138
|
+
jobFee: 0.01,
|
|
139
|
+
priceLabel: "$0.01 USDC",
|
|
140
|
+
jobFeeType: "fixed",
|
|
141
|
+
requiredFunds: false,
|
|
142
|
+
},
|
|
143
|
+
spendos_translate: {
|
|
144
|
+
name: "spendos_translate",
|
|
145
|
+
jobFee: 0.02,
|
|
146
|
+
priceLabel: "$0.02 USDC",
|
|
147
|
+
jobFeeType: "fixed",
|
|
148
|
+
requiredFunds: false,
|
|
149
|
+
},
|
|
150
|
+
// -- SigilX formal verification services --------------------------------
|
|
151
|
+
sigilx_verify_proof: {
|
|
152
|
+
name: "sigilx_verify_proof",
|
|
153
|
+
jobFee: 2.0,
|
|
154
|
+
priceLabel: "$2.00 USDC",
|
|
155
|
+
jobFeeType: "fixed",
|
|
156
|
+
requiredFunds: false,
|
|
157
|
+
},
|
|
158
|
+
sigilx_audit_cert: {
|
|
159
|
+
name: "sigilx_audit_cert",
|
|
160
|
+
jobFee: 0.25,
|
|
161
|
+
priceLabel: "$0.25 USDC",
|
|
162
|
+
jobFeeType: "fixed",
|
|
163
|
+
requiredFunds: false,
|
|
164
|
+
},
|
|
165
|
+
sigilx_forge_test: {
|
|
166
|
+
name: "sigilx_forge_test",
|
|
167
|
+
jobFee: 1.0,
|
|
168
|
+
priceLabel: "$1.00 USDC",
|
|
169
|
+
jobFeeType: "fixed",
|
|
170
|
+
requiredFunds: false,
|
|
171
|
+
},
|
|
172
|
+
sigilx_full_audit: {
|
|
173
|
+
name: "sigilx_full_audit",
|
|
174
|
+
jobFee: 5.0,
|
|
175
|
+
priceLabel: "$5.00 USDC",
|
|
176
|
+
jobFeeType: "fixed",
|
|
177
|
+
requiredFunds: false,
|
|
178
|
+
},
|
|
179
|
+
} as const;
|
|
180
|
+
|
|
181
|
+
export const CANONICAL_OFFERING_NAMES = Object.keys(
|
|
182
|
+
CANONICAL_CATALOG,
|
|
183
|
+
) as ReadonlyArray<string>;
|
|
184
|
+
export const EXPECTED_OFFERING_COUNT = CANONICAL_OFFERING_NAMES.length;
|
|
185
|
+
|
|
186
|
+
export function getCanonicalOfferingNamesForAgent(
|
|
187
|
+
agentDirName: string,
|
|
188
|
+
): ReadonlyArray<string> {
|
|
189
|
+
const prefix = `${agentDirName}_`;
|
|
190
|
+
return CANONICAL_OFFERING_NAMES.filter((name) => name.startsWith(prefix));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getEffectiveCanonicalOffering(
|
|
194
|
+
offeringName: string,
|
|
195
|
+
): CanonicalOffering | undefined {
|
|
196
|
+
const canonical = CANONICAL_CATALOG[offeringName];
|
|
197
|
+
if (!canonical) return undefined;
|
|
198
|
+
|
|
199
|
+
const explicitOverride = parsePositiveNumber(
|
|
200
|
+
process.env[priceOverrideEnvKey(offeringName)],
|
|
201
|
+
);
|
|
202
|
+
const scopedMultiplier = offeringName.startsWith("sigilx_")
|
|
203
|
+
? parsePositiveNumber(process.env.SIGILX_ACP_PRICE_MULTIPLIER)
|
|
204
|
+
: undefined;
|
|
205
|
+
const globalMultiplier = parsePositiveNumber(
|
|
206
|
+
process.env.ACP_PRICE_MULTIPLIER,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const multiplier = scopedMultiplier ?? globalMultiplier;
|
|
210
|
+
const jobFee =
|
|
211
|
+
explicitOverride ??
|
|
212
|
+
(multiplier === undefined
|
|
213
|
+
? canonical.jobFee
|
|
214
|
+
: roundUsd(canonical.jobFee * multiplier));
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
...canonical,
|
|
218
|
+
jobFee,
|
|
219
|
+
priceLabel: formatPriceLabel(jobFee),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const SPENDOS_URL = process.env.SPENDOS_URL ?? "https://spendos.xyz";
|
|
2
|
+
|
|
3
|
+
export async function executeJob(context: any) {
|
|
4
|
+
const { url } = context.requirement ?? context;
|
|
5
|
+
if (!url) return { deliverable: 'Error: missing "url" parameter' };
|
|
6
|
+
|
|
7
|
+
const res = await fetch(`${SPENDOS_URL}/api/internal/summarize`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { "Content-Type": "application/json" },
|
|
10
|
+
body: JSON.stringify({ url }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
const text = await res.text();
|
|
15
|
+
return { deliverable: `Error: ${res.status} ${text.slice(0, 200)}` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const data = (await res.json()) as any;
|
|
19
|
+
return { deliverable: data.summary ?? JSON.stringify(data) };
|
|
20
|
+
}
|