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,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SpendOS Public MCP Server — x402 Paid AI Tools
|
|
4
|
+
*
|
|
5
|
+
* External agents (Claude Code, Cursor, any MCP client) connect to this
|
|
6
|
+
* server and get access to x402-paid AI tools on Base mainnet.
|
|
7
|
+
* Each call pays real USDC to the SpendOS agent wallet.
|
|
8
|
+
*
|
|
9
|
+
* Track 3 #5: "Build MCP tool servers that charge per invocation via x402."
|
|
10
|
+
*
|
|
11
|
+
* Setup:
|
|
12
|
+
* export EVM_PRIVATE_KEY=0x... # wallet with USDC on Base
|
|
13
|
+
* npx tsx src/mcp-public.ts
|
|
14
|
+
*
|
|
15
|
+
* Claude Desktop config:
|
|
16
|
+
* {
|
|
17
|
+
* "mcpServers": {
|
|
18
|
+
* "spendos": {
|
|
19
|
+
* "command": "npx",
|
|
20
|
+
* "args": ["tsx", "/path/to/spendos/src/mcp-public.ts"],
|
|
21
|
+
* "env": {
|
|
22
|
+
* "EVM_PRIVATE_KEY": "0x...",
|
|
23
|
+
* "SPENDOS_URL": "https://spendos.xyz"
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
30
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
31
|
+
import axios from 'axios';
|
|
32
|
+
import { x402Client, wrapAxiosWithPayment } from '@x402/axios';
|
|
33
|
+
import { registerExactEvmScheme } from '@x402/evm/exact/client';
|
|
34
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
35
|
+
import { z } from 'zod';
|
|
36
|
+
|
|
37
|
+
const SPENDOS_URL = process.env.SPENDOS_URL ?? 'https://spendos.xyz';
|
|
38
|
+
const EVM_KEY = process.env.EVM_PRIVATE_KEY ?? '';
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
// ── x402 Payment Client ──────────────────────────────
|
|
42
|
+
const client = new x402Client();
|
|
43
|
+
|
|
44
|
+
if (EVM_KEY) {
|
|
45
|
+
const signer = privateKeyToAccount(EVM_KEY as `0x${string}`);
|
|
46
|
+
registerExactEvmScheme(client, { signer });
|
|
47
|
+
console.error(`[SpendOS MCP] Payment wallet: ${signer.address} (Base mainnet USDC)`);
|
|
48
|
+
} else {
|
|
49
|
+
console.error('[SpendOS MCP] No EVM_PRIVATE_KEY — x402 payments will fail');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const api = wrapAxiosWithPayment(axios.create({ baseURL: SPENDOS_URL }), client);
|
|
53
|
+
|
|
54
|
+
// ── MCP Server ────────────────────────────────────────
|
|
55
|
+
const server = new McpServer({
|
|
56
|
+
name: 'spendos-x402',
|
|
57
|
+
version: '0.1.0',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Tool: Summarize URL ($0.01 USDC)
|
|
61
|
+
server.tool(
|
|
62
|
+
'summarize_url',
|
|
63
|
+
'Summarize any URL into a concise brief. Costs $0.01 USDC on Base via x402. Revenue goes to the SpendOS autonomous agent.',
|
|
64
|
+
{ url: z.string().describe('The URL to summarize') },
|
|
65
|
+
async ({ url }) => {
|
|
66
|
+
const res = await api.post('/api/summarize', { url });
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Summary: ${res.data.summary}\n\nPaid: $${res.data.cost?.earned ?? '0.01'} USDC → SpendOS agent`,
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Tool: Generate Image ($0.05 USDC)
|
|
77
|
+
server.tool(
|
|
78
|
+
'generate_image',
|
|
79
|
+
'Generate an AI image from a text prompt. Costs $0.05 USDC on Base via x402. Revenue goes to the SpendOS autonomous agent.',
|
|
80
|
+
{ prompt: z.string().describe('Text description of the image to generate') },
|
|
81
|
+
async ({ prompt }) => {
|
|
82
|
+
const res = await api.post('/api/generate-image', { prompt });
|
|
83
|
+
const imageUrl = res.data.images?.[0]?.url ?? '[base64 image data]';
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: 'text', text: `Image: ${imageUrl}\nPaid: $0.05 USDC → SpendOS agent` }],
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Tool: Check Agent P&L (free)
|
|
91
|
+
server.tool(
|
|
92
|
+
'check_agent_pnl',
|
|
93
|
+
'Check the SpendOS agent revenue, costs, and profit margin. Free — no payment required.',
|
|
94
|
+
{},
|
|
95
|
+
async () => {
|
|
96
|
+
const res = await axios.get(`${SPENDOS_URL}/api/pnl`);
|
|
97
|
+
const pnl = res.data;
|
|
98
|
+
const margin = pnl.totalEarned > 0 ? ((pnl.profit / pnl.totalEarned) * 100).toFixed(0) : 'n/a';
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: `SpendOS Agent P&L (live):\n Revenue: $${pnl.totalEarned.toFixed(3)} USDC\n Costs: $${pnl.totalSpent.toFixed(4)}\n Profit: $${pnl.profit.toFixed(4)}\n Queries: ${pnl.queryCount}\n Margin: ${margin}%`,
|
|
103
|
+
}],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// ── Dynamic Jobs (loaded from agent's job registry) ────
|
|
109
|
+
try {
|
|
110
|
+
const jobsRes = await axios.get(`${SPENDOS_URL}/api/jobs`);
|
|
111
|
+
const jobs = jobsRes.data as Array<{ name: string; price: string; inputs: string[]; description?: string }>;
|
|
112
|
+
|
|
113
|
+
for (const job of jobs) {
|
|
114
|
+
const inputSchema: Record<string, any> = {};
|
|
115
|
+
for (const input of job.inputs) {
|
|
116
|
+
inputSchema[input] = z.string().describe(input);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
server.tool(
|
|
120
|
+
`job_${job.name}`,
|
|
121
|
+
`${job.description ?? job.name}. Costs ${job.price} USDC on Base via x402.`,
|
|
122
|
+
inputSchema,
|
|
123
|
+
async (args) => {
|
|
124
|
+
const res = await api.post(`/api/jobs/${job.name}`, args);
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: 'text', text: `Result: ${res.data.result}\nPaid: ${job.price} USDC → SpendOS agent` }],
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
console.error(`[SpendOS MCP] Loaded ${jobs.length} dynamic jobs from registry`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`[SpendOS MCP] Failed to load jobs: ${err}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Start ──────────────────────────────────────────────
|
|
137
|
+
const transport = new StdioServerTransport();
|
|
138
|
+
await server.connect(transport);
|
|
139
|
+
console.error(`[SpendOS MCP] Ready — 3 tools (2 paid, 1 free) → ${SPENDOS_URL}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch((err) => {
|
|
143
|
+
console.error(`[SpendOS MCP] Fatal: ${err}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SpendOS MCP Tool Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes x402-gated AI compute as MCP tools.
|
|
6
|
+
* Any MCP-compatible agent (Claude Code, Cursor, GPT) can call these tools.
|
|
7
|
+
* Each invocation is paid via x402 from the agent's OWS wallet.
|
|
8
|
+
*
|
|
9
|
+
* Track 3 #5: "Build MCP tool servers that charge per invocation via x402."
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx tsx src/mcp-server.ts # stdio transport (for Claude Code)
|
|
13
|
+
* Add to openclaw.json or claude settings as MCP server
|
|
14
|
+
*/
|
|
15
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import {
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
21
|
+
|
|
22
|
+
const SPENDOS_URL = process.env.SPENDOS_URL ?? 'http://localhost:3030';
|
|
23
|
+
|
|
24
|
+
const server = new Server(
|
|
25
|
+
{ name: 'spendos', version: '0.1.0' },
|
|
26
|
+
{ capabilities: { tools: {} } },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// ── Tool Definitions ───────────────────────────────────
|
|
30
|
+
|
|
31
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
32
|
+
tools: [
|
|
33
|
+
{
|
|
34
|
+
name: 'summarize_url',
|
|
35
|
+
description: 'Summarize a URL into a concise brief. Costs $0.01 via x402. Powered by Venice AI (wallet-authenticated, decentralized inference).',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object' as const,
|
|
38
|
+
properties: {
|
|
39
|
+
url: { type: 'string', description: 'The URL to summarize' },
|
|
40
|
+
},
|
|
41
|
+
required: ['url'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'generate_image',
|
|
46
|
+
description: 'Generate an image from a text prompt. Costs $0.05 via x402. Powered by Venice AI.',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object' as const,
|
|
49
|
+
properties: {
|
|
50
|
+
prompt: { type: 'string', description: 'Text description of the image to generate' },
|
|
51
|
+
},
|
|
52
|
+
required: ['prompt'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'check_pnl',
|
|
57
|
+
description: 'Check the SpendOS agent P&L (earnings, spending, profit margin).',
|
|
58
|
+
inputSchema: { type: 'object' as const, properties: {} },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'request_delegation',
|
|
62
|
+
description: 'Request a spending delegation from SpendOS governance. Must be approved by the wallet owner before the agent can sign transactions.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object' as const,
|
|
65
|
+
properties: {
|
|
66
|
+
reason: { type: 'string', description: 'Why the agent needs spending permission' },
|
|
67
|
+
chains: { type: 'array', items: { type: 'string' }, description: 'CAIP-2 chain IDs (e.g. eip155:8453)' },
|
|
68
|
+
totalBudget: { type: 'string', description: 'Maximum total spend in USD' },
|
|
69
|
+
expiresInMinutes: { type: 'number', description: 'How many minutes until the delegation expires' },
|
|
70
|
+
},
|
|
71
|
+
required: ['reason'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
// ── Tool Execution ─────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
80
|
+
const { name, arguments: args } = request.params;
|
|
81
|
+
|
|
82
|
+
switch (name) {
|
|
83
|
+
case 'summarize_url': {
|
|
84
|
+
const res = await fetch(`${SPENDOS_URL}/api/summarize`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ url: args?.url }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (res.status === 402) {
|
|
91
|
+
return { content: [{ type: 'text', text: 'Payment required: $0.01 via x402. This endpoint is gated by the x402 payment protocol.' }] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = await res.json() as any;
|
|
95
|
+
return {
|
|
96
|
+
content: [{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: `Summary: ${data.summary}\n\nCost: earned $${data.cost?.earned}, inference $${data.cost?.inference}, profit $${data.cost?.profit}`,
|
|
99
|
+
}],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case 'generate_image': {
|
|
104
|
+
const res = await fetch(`${SPENDOS_URL}/api/generate-image`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({ prompt: args?.prompt }),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (res.status === 402) {
|
|
111
|
+
return { content: [{ type: 'text', text: 'Payment required: $0.05 via x402.' }] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await res.json() as any;
|
|
115
|
+
const imageUrl = data.images?.[0]?.url ?? data.images?.[0]?.b64_json ? '[base64 image]' : 'no image';
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: 'text', text: `Image generated: ${imageUrl}\nCost: earned $0.05, inference $0.01, profit $0.04` }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'check_pnl': {
|
|
122
|
+
const res = await fetch(`${SPENDOS_URL}/api/pnl`);
|
|
123
|
+
const pnl = await res.json() as any;
|
|
124
|
+
const margin = pnl.totalEarned > 0 ? ((pnl.profit / pnl.totalEarned) * 100).toFixed(0) : 'n/a';
|
|
125
|
+
return {
|
|
126
|
+
content: [{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: `Agent P&L:\n Earned: $${pnl.totalEarned.toFixed(3)}\n Spent: $${pnl.totalSpent.toFixed(4)}\n Profit: $${pnl.profit.toFixed(4)}\n Queries: ${pnl.queryCount}\n Margin: ${margin}%`,
|
|
129
|
+
}],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'request_delegation': {
|
|
134
|
+
const expiresAt = new Date(Date.now() + (Number(args?.expiresInMinutes) || 30) * 60000).toISOString();
|
|
135
|
+
const res = await fetch(`${SPENDOS_URL}/api/delegate`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
agentAddress: '0x0000000000000000000000000000000000000000', // MCP agent
|
|
140
|
+
reason: args?.reason ?? 'MCP tool request',
|
|
141
|
+
chains: args?.chains ?? ['eip155:8453'],
|
|
142
|
+
operations: ['sign_tx'],
|
|
143
|
+
maxAmountPerAction: args?.totalBudget ?? '1.00',
|
|
144
|
+
totalBudget: args?.totalBudget ?? '1.00',
|
|
145
|
+
expiresAt,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const d = await res.json() as any;
|
|
150
|
+
return {
|
|
151
|
+
content: [{
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: `Delegation requested:\n ID: ${d.id}\n Status: ${d.status}\n Risk: ${d.aiInterpretation?.riskLevel ?? 'unknown'}\n Warnings: ${d.aiInterpretation?.warnings?.join('; ') ?? 'none'}\n\nAwaiting wallet owner approval in SpendOS dashboard.`,
|
|
154
|
+
}],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
default:
|
|
159
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Start ──────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const transport = new StdioServerTransport();
|
|
167
|
+
await server.connect(transport);
|
|
168
|
+
console.error('[SpendOS MCP] Server started on stdio');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opportunity Scanner
|
|
3
|
+
*
|
|
4
|
+
* Runs on a schedule (every 30 min). The agent:
|
|
5
|
+
* 1. Checks its P&L and available revenue
|
|
6
|
+
* 2. Scans for yield/staking/swap opportunities via MoonPay + Zerion
|
|
7
|
+
* 3. Creates delegation requests for each opportunity
|
|
8
|
+
* 4. Owner wakes up to a queue of proposals to approve/reject
|
|
9
|
+
*
|
|
10
|
+
* The agent NEVER acts without approval. It only proposes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createDelegation, getPnL, getTreasuryAddress, generateHeuristicInterpretation } from './governance.js';
|
|
14
|
+
import { logAuditEvent } from './audit.js';
|
|
15
|
+
import { notifyDelegationRequest } from './xmtp.js';
|
|
16
|
+
|
|
17
|
+
interface Opportunity {
|
|
18
|
+
reason: string;
|
|
19
|
+
chains: string[];
|
|
20
|
+
operations: string[];
|
|
21
|
+
maxAmountPerAction: string;
|
|
22
|
+
totalBudget: string;
|
|
23
|
+
allowedRecipients: string[];
|
|
24
|
+
expiresInMinutes: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Known yield opportunities on Base ──────────────────
|
|
28
|
+
|
|
29
|
+
const BASE_OPPORTUNITIES: Opportunity[] = [
|
|
30
|
+
{
|
|
31
|
+
reason: 'Stake USDC on Aave V3 (Base) for ~3.5% APY',
|
|
32
|
+
chains: ['eip155:8453'],
|
|
33
|
+
operations: ['sign_tx'],
|
|
34
|
+
maxAmountPerAction: '5.00',
|
|
35
|
+
totalBudget: '5.00',
|
|
36
|
+
allowedRecipients: ['0x18cd499e3d7ed42FEBa981ac9236A278E4Cdc2ee'], // Aave Pool on Base
|
|
37
|
+
expiresInMinutes: 60,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
reason: 'Swap 50% of USDC earnings to ETH via Uniswap (portfolio diversification)',
|
|
41
|
+
chains: ['eip155:8453'],
|
|
42
|
+
operations: ['sign_tx'],
|
|
43
|
+
maxAmountPerAction: '2.50',
|
|
44
|
+
totalBudget: '2.50',
|
|
45
|
+
allowedRecipients: ['0x2626664c2603336E57B271c5C0b26F421741e481'], // Uniswap Universal Router on Base
|
|
46
|
+
expiresInMinutes: 30,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
reason: 'Buy VVV tokens for Venice DIEM staking (perpetual compute access)',
|
|
50
|
+
chains: ['eip155:8453'],
|
|
51
|
+
operations: ['sign_tx'],
|
|
52
|
+
maxAmountPerAction: '7.50',
|
|
53
|
+
totalBudget: '7.50',
|
|
54
|
+
allowedRecipients: [
|
|
55
|
+
'0x2626664c2603336E57B271c5C0b26F421741e481', // Uniswap (buy VVV)
|
|
56
|
+
'0x321b7ff75154472b18edb199033ff4d116f340ff', // Venice staking contract
|
|
57
|
+
],
|
|
58
|
+
expiresInMinutes: 120,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
reason: 'Top up Venice inference credits with earned USDC (self-sustaining compute)',
|
|
62
|
+
chains: ['eip155:8453'],
|
|
63
|
+
operations: ['sign_tx'],
|
|
64
|
+
maxAmountPerAction: '5.00',
|
|
65
|
+
totalBudget: '5.00',
|
|
66
|
+
allowedRecipients: ['0x2670B922ef37C7Df47158725C0CC407b5382293F'], // Venice receiver
|
|
67
|
+
expiresInMinutes: 60,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// ── Scanner ────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export async function scanOpportunities(): Promise<number> {
|
|
74
|
+
const pnl = getPnL();
|
|
75
|
+
const address = getTreasuryAddress();
|
|
76
|
+
|
|
77
|
+
if (pnl.profit <= 0) {
|
|
78
|
+
console.log('[Scanner] No profit yet — skipping opportunity scan');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`[Scanner] Profit: $${pnl.profit.toFixed(4)} — scanning for opportunities...`);
|
|
83
|
+
|
|
84
|
+
let created = 0;
|
|
85
|
+
for (const opp of BASE_OPPORTUNITIES) {
|
|
86
|
+
// Only propose if we can afford it
|
|
87
|
+
const budget = parseFloat(opp.totalBudget);
|
|
88
|
+
// Always propose opportunities for the demo — in production, threshold by revenue
|
|
89
|
+
// if (budget > pnl.profit * 100) continue;
|
|
90
|
+
|
|
91
|
+
const expiresAt = new Date(Date.now() + opp.expiresInMinutes * 60000).toISOString();
|
|
92
|
+
|
|
93
|
+
const delegation = createDelegation({
|
|
94
|
+
agentAddress: address,
|
|
95
|
+
reason: opp.reason,
|
|
96
|
+
chains: opp.chains,
|
|
97
|
+
operations: opp.operations,
|
|
98
|
+
maxAmountPerAction: opp.maxAmountPerAction,
|
|
99
|
+
totalBudget: opp.totalBudget,
|
|
100
|
+
allowedRecipients: opp.allowedRecipients,
|
|
101
|
+
expiresAt,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
delegation.aiInterpretation = generateHeuristicInterpretation(delegation);
|
|
105
|
+
|
|
106
|
+
await logAuditEvent('requested', delegation.id, delegation.reason, 0);
|
|
107
|
+
await notifyDelegationRequest(delegation);
|
|
108
|
+
|
|
109
|
+
console.log(`[Scanner] Proposed: ${opp.reason} ($${opp.totalBudget})`);
|
|
110
|
+
created++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`[Scanner] Created ${created} delegation proposals`);
|
|
114
|
+
return created;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Schedule ───────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
let scanInterval: ReturnType<typeof setInterval> | null = null;
|
|
120
|
+
|
|
121
|
+
export function startScanner(intervalMinutes: number = 30): void {
|
|
122
|
+
console.log(`[Scanner] Starting — scanning every ${intervalMinutes} minutes`);
|
|
123
|
+
|
|
124
|
+
// Run once immediately after a short delay (let server init first)
|
|
125
|
+
setTimeout(() => scanOpportunities().catch(console.error), 10000);
|
|
126
|
+
|
|
127
|
+
scanInterval = setInterval(
|
|
128
|
+
() => scanOpportunities().catch(console.error),
|
|
129
|
+
intervalMinutes * 60000,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function stopScanner(): void {
|
|
134
|
+
if (scanInterval) {
|
|
135
|
+
clearInterval(scanInterval);
|
|
136
|
+
scanInterval = null;
|
|
137
|
+
}
|
|
138
|
+
}
|