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.
Files changed (90) hide show
  1. package/.dockerignore +4 -0
  2. package/.env.example +30 -0
  3. package/AGENTS.md +212 -0
  4. package/BOOTSTRAP.md +55 -0
  5. package/Dockerfile +52 -0
  6. package/HEARTBEAT.md +7 -0
  7. package/IDENTITY.md +23 -0
  8. package/LICENSE +21 -0
  9. package/README.md +162 -0
  10. package/SOUL.md +202 -0
  11. package/SUBMISSION.md +128 -0
  12. package/TOOLS.md +40 -0
  13. package/USER.md +17 -0
  14. package/acp-seller/bin/acp.ts +807 -0
  15. package/acp-seller/config.json +34 -0
  16. package/acp-seller/package.json +55 -0
  17. package/acp-seller/src/commands/agent.ts +328 -0
  18. package/acp-seller/src/commands/bounty.ts +1189 -0
  19. package/acp-seller/src/commands/deploy.ts +414 -0
  20. package/acp-seller/src/commands/job.ts +217 -0
  21. package/acp-seller/src/commands/profile.ts +71 -0
  22. package/acp-seller/src/commands/resource.ts +91 -0
  23. package/acp-seller/src/commands/search.ts +327 -0
  24. package/acp-seller/src/commands/sell.ts +883 -0
  25. package/acp-seller/src/commands/serve.ts +258 -0
  26. package/acp-seller/src/commands/setup.ts +399 -0
  27. package/acp-seller/src/commands/token.ts +88 -0
  28. package/acp-seller/src/commands/wallet.ts +123 -0
  29. package/acp-seller/src/lib/api.ts +118 -0
  30. package/acp-seller/src/lib/auth.ts +291 -0
  31. package/acp-seller/src/lib/bounty.ts +257 -0
  32. package/acp-seller/src/lib/client.ts +42 -0
  33. package/acp-seller/src/lib/config.ts +240 -0
  34. package/acp-seller/src/lib/open.ts +41 -0
  35. package/acp-seller/src/lib/openclawCron.ts +138 -0
  36. package/acp-seller/src/lib/output.ts +104 -0
  37. package/acp-seller/src/lib/wallet.ts +81 -0
  38. package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
  39. package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
  40. package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
  41. package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
  42. package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
  43. package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
  44. package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
  45. package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
  46. package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
  47. package/acp-seller/src/seller/runtime/logger.ts +36 -0
  48. package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
  49. package/acp-seller/src/seller/runtime/offerings.ts +277 -0
  50. package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
  51. package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
  52. package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
  53. package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
  54. package/acp-seller/src/seller/runtime/seller.ts +1041 -0
  55. package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
  56. package/acp-seller/src/seller/runtime/startup.ts +270 -0
  57. package/acp-seller/src/seller/runtime/types.ts +62 -0
  58. package/acp-seller/tsconfig.json +20 -0
  59. package/bin/spendos.js +23 -0
  60. package/contracts/SpendOSAudit.sol +29 -0
  61. package/dist/mcp-server.mjs +153 -0
  62. package/jobs/translate.json +7 -0
  63. package/jobs/tweet-gen.json +7 -0
  64. package/openclaw.json +41 -0
  65. package/package.json +49 -0
  66. package/plugins/spendos-events/index.ts +78 -0
  67. package/plugins/spendos-events/package.json +14 -0
  68. package/policies/enforce-bounds.mjs +71 -0
  69. package/public/index.html +509 -0
  70. package/public/landing.html +241 -0
  71. package/railway.json +12 -0
  72. package/railway.toml +12 -0
  73. package/scripts/deploy.ts +48 -0
  74. package/scripts/test-x402-mainnet.ts +30 -0
  75. package/scripts/xmtp-listener.ts +61 -0
  76. package/setup.sh +278 -0
  77. package/skills/spendos/skill.md +26 -0
  78. package/src/agent.ts +152 -0
  79. package/src/audit.ts +166 -0
  80. package/src/governance.ts +367 -0
  81. package/src/job-registry.ts +306 -0
  82. package/src/mcp-public.ts +145 -0
  83. package/src/mcp-server.ts +171 -0
  84. package/src/opportunity-scanner.ts +138 -0
  85. package/src/server.ts +870 -0
  86. package/src/venice-x402.ts +234 -0
  87. package/src/xmtp.ts +109 -0
  88. package/src/zerion.ts +58 -0
  89. package/start.sh +168 -0
  90. package/tsconfig.json +14 -0
package/setup.sh ADDED
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ── SpendOS Setup ──────────────────────────────────────
5
+ # One command to provision your autonomous agent economy.
6
+ # Usage: ./setup.sh [--deploy --provider railway]
7
+
8
+ GREEN='\033[0;32m'
9
+ YELLOW='\033[1;33m'
10
+ RED='\033[0;31m'
11
+ CYAN='\033[0;36m'
12
+ NC='\033[0m'
13
+ BOLD='\033[1m'
14
+
15
+ log() { echo -e "${GREEN}[SpendOS]${NC} $1"; }
16
+ warn() { echo -e "${YELLOW}[SpendOS]${NC} $1"; }
17
+ err() { echo -e "${RED}[SpendOS]${NC} $1"; }
18
+ info() { echo -e "${CYAN}[SpendOS]${NC} $1"; }
19
+
20
+ echo ""
21
+ echo -e "${BOLD} ╔═══════════════════════════════════════════════╗${NC}"
22
+ echo -e "${BOLD} ║ SpendOS — Autonomous Agent Economy ║${NC}"
23
+ echo -e "${BOLD} ║ The first agent that governs its own spend ║${NC}"
24
+ echo -e "${BOLD} ╚═══════════════════════════════════════════════╝${NC}"
25
+ echo ""
26
+
27
+ # ── Check prerequisites ───────────────────────────────
28
+
29
+ command -v node >/dev/null 2>&1 || { err "Node.js required (22+). Install: https://nodejs.org"; exit 1; }
30
+ NODE_V=$(node -v | sed 's/v//' | cut -d. -f1)
31
+ if [ "$NODE_V" -lt 22 ]; then
32
+ err "Node.js 22+ required (found v$NODE_V)"
33
+ exit 1
34
+ fi
35
+ log "Node.js $(node -v)"
36
+
37
+ # ── Install dependencies ──────────────────────────────
38
+
39
+ log "Installing dependencies..."
40
+ npm install --legacy-peer-deps 2>&1 | tail -3
41
+
42
+ # ── Auto-provision everything ─────────────────────────
43
+
44
+ if [ ! -f .env ]; then
45
+ log "Provisioning your SpendOS instance..."
46
+ cp .env.example .env
47
+
48
+ # ── 1. Admin token (auto) ──
49
+ ADMIN_TOKEN=$(openssl rand -hex 32)
50
+ sed -i.bak "s/generate-with-openssl-rand-hex-32/$ADMIN_TOKEN/" .env && rm -f .env.bak
51
+ log "Admin token: ${CYAN}$ADMIN_TOKEN${NC}"
52
+
53
+ # ── 2. OWS passphrase (auto) ──
54
+ OWS_PASS=$(openssl rand -base64 32)
55
+ sed -i.bak "s|your-vault-passphrase|$OWS_PASS|" .env && rm -f .env.bak
56
+ log "OWS vault passphrase: generated"
57
+
58
+ # ── 3. OWS wallet mnemonic (auto-generate via viem) ──
59
+ log "Generating OWS wallet..."
60
+ MNEMONIC=$(node --input-type=module -e "
61
+ import { generateMnemonic, english } from 'viem/accounts';
62
+ console.log(generateMnemonic(english));
63
+ " 2>/dev/null || echo "")
64
+
65
+ if [ -n "$MNEMONIC" ]; then
66
+ ESCAPED=$(echo "$MNEMONIC" | sed 's/[&/\]/\\&/g')
67
+ sed -i.bak "s/your twelve word mnemonic phrase here/$ESCAPED/" .env && rm -f .env.bak
68
+ # Derive the wallet address
69
+ WALLET_ADDR=$(node --input-type=module -e "
70
+ import { mnemonicToAccount } from 'viem/accounts';
71
+ const a = mnemonicToAccount('$MNEMONIC');
72
+ console.log(a.address);
73
+ " 2>/dev/null || echo "unknown")
74
+ log "OWS wallet created: ${CYAN}$WALLET_ADDR${NC}"
75
+ echo ""
76
+ echo -e " ${YELLOW}IMPORTANT: Save this mnemonic somewhere safe!${NC}"
77
+ echo -e " ${BOLD}$MNEMONIC${NC}"
78
+ echo ""
79
+ else
80
+ warn "Could not auto-generate wallet. Add mnemonic manually to .env"
81
+ fi
82
+
83
+ # ── 4. Deployer key (auto-generate for audit contract) ──
84
+ DEPLOYER_KEY=$(node --input-type=module -e "
85
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
86
+ const key = generatePrivateKey();
87
+ const acc = privateKeyToAccount(key);
88
+ console.log(key + ' ' + acc.address);
89
+ " 2>/dev/null || echo "")
90
+
91
+ if [ -n "$DEPLOYER_KEY" ]; then
92
+ DKEY=$(echo "$DEPLOYER_KEY" | cut -d' ' -f1)
93
+ DADDR=$(echo "$DEPLOYER_KEY" | cut -d' ' -f2)
94
+ sed -i.bak "s|0x...your-deployer-private-key|$DKEY|" .env && rm -f .env.bak
95
+ log "Deployer wallet: ${CYAN}$DADDR${NC}"
96
+ info "Fund this with a tiny amount of ETH on Base for audit gas"
97
+ fi
98
+
99
+ # ── 5. Optional: CDP keys (user provides) ──
100
+ echo ""
101
+ echo -e "${BOLD}Optional — x402 payments (skip with Enter):${NC}"
102
+ echo -e " Get keys at: https://docs.cdp.coinbase.com"
103
+ echo ""
104
+
105
+ if [ -t 0 ]; then
106
+ read -p " CDP API Key ID: " CDP_ID
107
+ if [ -n "$CDP_ID" ]; then
108
+ sed -i.bak "s/your-cdp-key-id/$CDP_ID/" .env && rm -f .env.bak
109
+ read -p " CDP API Key Secret: " CDP_SECRET
110
+ if [ -n "$CDP_SECRET" ]; then
111
+ sed -i.bak "s/your-cdp-key-secret/$CDP_SECRET/" .env && rm -f .env.bak
112
+ log "CDP x402 payments configured"
113
+ fi
114
+ else
115
+ info "Skipped — endpoints will run without payment gate (free mode)"
116
+ fi
117
+
118
+ echo ""
119
+ echo -e "${BOLD}Optional — Twitter/X (agent can post and interact):${NC}"
120
+ echo -e " The agent uses a visual browser to log into X."
121
+ echo -e " Credentials are stored securely (never in git, env var only)."
122
+ echo ""
123
+ read -p " Twitter/X username (or Enter to skip): " TWITTER_USER
124
+ if [ -n "$TWITTER_USER" ]; then
125
+ read -sp " Twitter/X password: " TWITTER_PASS
126
+ echo ""
127
+ echo "TWITTER_USERNAME=$TWITTER_USER" >> .env
128
+ echo "TWITTER_PASSWORD=$TWITTER_PASS" >> .env
129
+ log "Twitter configured — agent will log in via browser on first boot"
130
+ info "Credentials stored in .env only (never committed to git)"
131
+ else
132
+ info "Skipped — agent runs without Twitter access"
133
+ fi
134
+
135
+ echo ""
136
+ echo -e "${BOLD}Optional — MoonPay (agent gets market data, quotes, balances):${NC}"
137
+ echo -e " Get key at: https://dashboard.moonpay.com"
138
+ echo ""
139
+ read -p " MoonPay API Key (or Enter to skip): " MOONPAY_KEY
140
+ if [ -n "$MOONPAY_KEY" ]; then
141
+ echo "MOONPAY_API_KEY=$MOONPAY_KEY" >> .env
142
+ log "MoonPay connected — agent can view quotes, balances, token data"
143
+ info "Signing/sending tools are blocked — transactions require delegation approval"
144
+ else
145
+ info "Skipped — agent runs without MoonPay market data"
146
+ fi
147
+ fi
148
+
149
+ else
150
+ log ".env already exists, skipping provisioning"
151
+ fi
152
+
153
+ # ── Create local data directories ─────────────────────
154
+
155
+ DATA_DIR="${SPENDOS_DATA_DIR:-./data}"
156
+ mkdir -p "$DATA_DIR" 2>/dev/null || mkdir -p /tmp/spendos-data
157
+
158
+ # ── Summary ───────────────────────────────────────────
159
+
160
+ # ── ACP Agent Commerce setup ──────────────────────
161
+
162
+ if [ -d "acp-seller" ] && [ -t 0 ]; then
163
+ echo ""
164
+ echo -e "${BOLD}ACP — Agent Commerce Protocol (earn from other agents):${NC}"
165
+ echo ""
166
+
167
+ if [ -f "acp-seller/config.json" ] && node -e "const c=JSON.parse(require('fs').readFileSync('acp-seller/config.json'));process.exit(c.LITE_AGENT_API_KEY?0:1)" 2>/dev/null; then
168
+ log "ACP already configured"
169
+ ACP_AGENT=$(node -e "const c=JSON.parse(require('fs').readFileSync('acp-seller/config.json'));console.log(c.agents?.find(a=>a.active)?.name||'unknown')" 2>/dev/null)
170
+ info "Active agent: ${ACP_AGENT}"
171
+ else
172
+ read -p " Connect to ACP marketplace? (Y/n): " ACP_CONNECT
173
+ if [ "${ACP_CONNECT,,}" != "n" ]; then
174
+ log "Opening browser for ACP login..."
175
+ cd acp-seller
176
+ npx tsx bin/acp.ts setup 2>&1
177
+ if [ $? -eq 0 ]; then
178
+ log "ACP connected! Registering SpendOS offerings..."
179
+ # Create agent named spendos
180
+ npx tsx bin/acp.ts agent create spendos 2>&1 || true
181
+ npx tsx bin/acp.ts profile update description "Autonomous AI agent — URL summarization, tweet generation, translation. Pay-per-call via USDC." 2>&1 || true
182
+ # Register offerings
183
+ for offering in src/seller/offerings/spendos/spendos_*/; do
184
+ name=$(basename "$offering")
185
+ npx tsx bin/acp.ts sell create "$name" 2>&1 || true
186
+ done
187
+ log "ACP offerings registered"
188
+ fi
189
+ cd ..
190
+ else
191
+ info "Skipped — run 'cd acp-seller && npx tsx bin/acp.ts setup' later"
192
+ fi
193
+ fi
194
+ fi
195
+
196
+ echo ""
197
+ echo -e "${GREEN}══════════════════════════════════════════════════${NC}"
198
+ echo -e "${GREEN} Setup complete!${NC}"
199
+ echo -e "${GREEN}══════════════════════════════════════════════════${NC}"
200
+ echo ""
201
+ echo -e " ${BOLD}Run locally:${NC}"
202
+ echo -e " npx tsx src/server.ts"
203
+ echo ""
204
+ ENVTOKEN=$(grep SPENDOS_ADMIN_TOKEN .env 2>/dev/null | cut -d= -f2 || echo "YOUR_TOKEN")
205
+ echo -e " ${BOLD}Dashboard:${NC}"
206
+ echo -e " http://localhost:3030/?token=${ENVTOKEN}"
207
+ echo ""
208
+ echo -e " ${BOLD}Deploy to Railway:${NC}"
209
+ echo -e " ./setup.sh --deploy --provider railway"
210
+ echo ""
211
+
212
+ # ── Deploy to Railway ─────────────────────────────────
213
+
214
+ DEPLOY=false
215
+ PROVIDER=""
216
+
217
+ for arg in "$@"; do
218
+ case $arg in
219
+ --deploy) DEPLOY=true ;;
220
+ railway) PROVIDER="railway" ;;
221
+ esac
222
+ done
223
+
224
+ if [ "$DEPLOY" = true ] && [ "$PROVIDER" = "railway" ]; then
225
+ echo ""
226
+ log "Deploying to Railway..."
227
+
228
+ command -v railway >/dev/null 2>&1 || {
229
+ log "Installing Railway CLI..."
230
+ npm install -g @railway/cli
231
+ }
232
+
233
+ # Login check
234
+ railway whoami >/dev/null 2>&1 || {
235
+ warn "Not logged in to Railway"
236
+ railway login
237
+ }
238
+
239
+ # Init project if needed
240
+ if ! railway status >/dev/null 2>&1; then
241
+ log "Creating Railway project..."
242
+ railway init
243
+ fi
244
+
245
+ # Push all env vars
246
+ log "Setting environment variables on Railway..."
247
+ while IFS= read -r line; do
248
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
249
+ [[ -z "${line// }" ]] && continue
250
+ key="${line%%=*}"
251
+ value="${line#*=}"
252
+ key=$(echo "$key" | xargs)
253
+ [ -z "$key" ] && continue
254
+ railway variables --set "$key=$value" 2>/dev/null || true
255
+ done < .env
256
+
257
+ # Inject ACP config (base64 encoded — no secrets in plain text)
258
+ if [ -f "acp-seller/config.json" ]; then
259
+ ACP_CONFIG_B64=$(base64 < acp-seller/config.json | tr -d '\n')
260
+ railway variables --set "ACP_CONFIG_B64=$ACP_CONFIG_B64" 2>/dev/null || true
261
+ log "ACP config injected (base64)"
262
+ fi
263
+
264
+ # Deploy
265
+ log "Building and deploying..."
266
+ railway up --detach
267
+
268
+ echo ""
269
+ echo -e "${GREEN}══════════════════════════════════════════════════${NC}"
270
+ echo -e "${GREEN} Deployed to Railway!${NC}"
271
+ echo -e "${GREEN}══════════════════════════════════════════════════${NC}"
272
+ echo ""
273
+ echo -e " ${BOLD}Next steps:${NC}"
274
+ echo -e " 1. Add a persistent volume mounted at ${CYAN}/data${NC} in Railway dashboard"
275
+ echo -e " 2. Your URL will appear in Railway once the build completes"
276
+ echo -e " 3. Append ${CYAN}?token=$ENVTOKEN${NC} to access the dashboard"
277
+ echo ""
278
+ fi
@@ -0,0 +1,26 @@
1
+ # SpendOS Governance Skill
2
+
3
+ You have access to the SpendOS governance system via MCP tools. Use these to manage your finances autonomously.
4
+
5
+ ## Available Tools
6
+
7
+ - `check_pnl` — Check your earnings, spending, and profit margin
8
+ - `request_delegation` — Request permission to spend your revenue (requires owner approval)
9
+ - `summarize_url` — Earn $0.01 by summarizing a URL (x402-gated)
10
+ - `generate_image` — Earn $0.05 by generating an image (x402-gated)
11
+
12
+ ## Rules
13
+
14
+ 1. **NEVER sign transactions directly.** Always use `request_delegation` first.
15
+ 2. Include real contract addresses in `allowedRecipients` for vendor allowlists.
16
+ 3. Check your P&L before proposing investments — don't propose more than you've earned.
17
+ 4. Every delegation has an expiry. Set `expiresInMinutes` based on urgency.
18
+ 5. The wallet owner approves or rejects your proposals in the SpendOS dashboard.
19
+
20
+ ## Investment Strategy
21
+
22
+ You are NOT limited to specific protocols. Use MoonPay and Zerion MCP tools to discover the best current opportunities. Search for staking, LP, swap, yield farming, and credit top-up opportunities across any protocol on Base. Always include verified contract addresses in your delegation requests.
23
+
24
+ ## Self-Funding
25
+
26
+ Your inference runs on Venice AI via wallet auth (SIWE). Venice credits are funded from your USDC on Base. When credits run low, propose a Venice top-up delegation. The system auto-tops-up via x402 EIP-3009 when possible.
package/src/agent.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { recordEarning, recordSpending } from './governance.js';
2
+ import { initVeniceWallet, walletInference, checkBalance, type VeniceBalance } from './venice-x402.js';
3
+
4
+ const VENICE_API_KEY = process.env.VENICE_API_KEY ?? '';
5
+ const VENICE_BASE_URL = 'https://api.venice.ai/api/v1';
6
+ // Venice's house model — cheapest and always available
7
+ const VENICE_MODEL = process.env.VENICE_MODEL ?? 'venice-uncensored';
8
+ const EARN_PER_QUERY = 0.01;
9
+ const COST_PER_QUERY = 0.002;
10
+
11
+ // Venice pricing per 1M tokens (approximate)
12
+ const MODEL_PRICING: Record<string, { input: number; output: number }> = {
13
+ 'venice-uncensored': { input: 0.10, output: 0.25 },
14
+ 'llama-3.3-70b': { input: 0.15, output: 0.60 },
15
+ 'kimi-k2-5': { input: 0.50, output: 1.50 },
16
+ 'deepseek-v3.2': { input: 0.15, output: 0.75 },
17
+ 'qwen3-coder-480b-a35b-instruct': { input: 0.15, output: 0.75 },
18
+ };
19
+
20
+ function calculateInferenceCost(model: string, promptTokens: number, completionTokens: number): number {
21
+ const pricing = MODEL_PRICING[model] ?? { input: 0.10, output: 0.25 };
22
+ return (promptTokens * pricing.input + completionTokens * pricing.output) / 1_000_000;
23
+ }
24
+
25
+ // Inference mode: 'x402' (wallet auth, self-funding) or 'api_key' (Bearer token)
26
+ let inferenceMode: 'x402' | 'api_key' | 'none' = 'none';
27
+ let lastBalanceCheck: VeniceBalance | null = null;
28
+
29
+ export function configureAgent(privateKey: string, address: string): void {
30
+ initVeniceWallet(privateKey, address);
31
+ inferenceMode = 'x402';
32
+ console.log(`[Agent] Inference mode: x402 wallet auth (self-funding)`);
33
+ }
34
+
35
+ export function configureApiKey(): void {
36
+ if (VENICE_API_KEY) {
37
+ inferenceMode = 'api_key';
38
+ console.log(`[Agent] Inference mode: API key (Bearer)`);
39
+ }
40
+ }
41
+
42
+ // ── Venice Inference ────────────────────────────────────
43
+
44
+ async function callVenice(content: string): Promise<{ content: string; cost: number }> {
45
+ const systemPrompt = 'You are a concise summarizer. Summarize the following web page content in 2-3 sentences.';
46
+
47
+ if (inferenceMode === 'x402') {
48
+ try {
49
+ const text = await walletInference(VENICE_MODEL, systemPrompt, content, 200);
50
+ const estPrompt = Math.ceil(content.length / 4);
51
+ const estCompletion = Math.ceil(text.length / 4);
52
+ return { content: text, cost: calculateInferenceCost(VENICE_MODEL, estPrompt, estCompletion) };
53
+ } catch (err) {
54
+ console.log(`[Agent] x402 inference failed, trying API key fallback: ${err}`);
55
+ if (!VENICE_API_KEY) throw err;
56
+ // Fall through to API key
57
+ }
58
+ }
59
+
60
+ if (VENICE_API_KEY) {
61
+ const res = await fetch(`${VENICE_BASE_URL}/chat/completions`, {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Authorization': `Bearer ${VENICE_API_KEY}`,
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ body: JSON.stringify({
68
+ model: VENICE_MODEL,
69
+ messages: [
70
+ { role: 'system', content: systemPrompt },
71
+ { role: 'user', content },
72
+ ],
73
+ max_tokens: 200,
74
+ }),
75
+ });
76
+
77
+ if (!res.ok) {
78
+ const text = await res.text();
79
+ throw new Error(`Venice API error: ${res.status} ${text}`);
80
+ }
81
+
82
+ const data = await res.json() as any;
83
+ const usage = data.usage ?? {};
84
+ const cost = calculateInferenceCost(VENICE_MODEL, usage.prompt_tokens ?? 0, usage.completion_tokens ?? 0);
85
+ return { content: data.choices?.[0]?.message?.content ?? 'No summary generated.', cost };
86
+ }
87
+
88
+ throw new Error('No Venice inference configured.');
89
+ }
90
+
91
+ // ── Public API ──────────────────────────────────────────
92
+
93
+ export async function summarizeUrl(url: string): Promise<{
94
+ summary: string;
95
+ inferenceMode: string;
96
+ veniceBalance?: VeniceBalance;
97
+ cost: { earned: number; inference: number; profit: number };
98
+ }> {
99
+ // Fetch URL content
100
+ let content: string;
101
+ try {
102
+ const res = await fetch(url, {
103
+ headers: { 'User-Agent': 'SpendOS-Agent/1.0' },
104
+ signal: AbortSignal.timeout(10000),
105
+ });
106
+ const text = await res.text();
107
+ content = text.slice(0, 4000);
108
+ } catch (err) {
109
+ content = `Failed to fetch URL: ${url}. Error: ${err}`;
110
+ }
111
+
112
+ // Call Venice
113
+ let summary: string;
114
+ let actualCost = 0;
115
+ try {
116
+ const result = await callVenice(content);
117
+ summary = result.content;
118
+ actualCost = result.cost;
119
+ } catch (err) {
120
+ summary = `Summarization failed: ${err}`;
121
+ }
122
+
123
+ // Check Venice balance (best-effort, for dashboard display)
124
+ if (inferenceMode === 'x402') {
125
+ try {
126
+ lastBalanceCheck = await checkBalance();
127
+ } catch { /* non-critical */ }
128
+ }
129
+
130
+ // Track P&L with REAL inference cost
131
+ recordEarning(EARN_PER_QUERY);
132
+ recordSpending(actualCost);
133
+
134
+ return {
135
+ summary,
136
+ inferenceMode,
137
+ veniceBalance: lastBalanceCheck ?? undefined,
138
+ cost: {
139
+ earned: EARN_PER_QUERY,
140
+ inference: COST_PER_QUERY,
141
+ profit: EARN_PER_QUERY - COST_PER_QUERY,
142
+ },
143
+ };
144
+ }
145
+
146
+ export function getVeniceBalance(): VeniceBalance | null {
147
+ return lastBalanceCheck;
148
+ }
149
+
150
+ export function getInferenceMode(): string {
151
+ return inferenceMode;
152
+ }
package/src/audit.ts ADDED
@@ -0,0 +1,166 @@
1
+ import { createWalletClient, http, keccak256, toHex } from 'viem';
2
+ import { baseSepolia, base } from 'viem/chains';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { writeFileSync, readFileSync, existsSync } from 'node:fs';
5
+
6
+ // ── Config ─────────────────────────────────────────────
7
+
8
+ const AUDIT_CONTRACT = process.env.SPENDOS_AUDIT_CONTRACT ?? '0x37a66d3404aDCaf0a558189a338bCeaD3a06A5Ee';
9
+ const DEPLOYER_KEY = process.env.DEPLOYER_PRIVATE_KEY ?? '';
10
+ const CHAIN = process.env.SPENDOS_CHAIN === 'mainnet' ? base : baseSepolia;
11
+ const RPC_URL = process.env.SPENDOS_RPC_URL ?? (process.env.SPENDOS_CHAIN === 'mainnet'
12
+ ? 'https://mainnet.base.org'
13
+ : 'https://sepolia.base.org');
14
+ const AUDIT_FILE = './audit.json';
15
+
16
+ const LOG_ABI = [{
17
+ name: 'log',
18
+ type: 'function',
19
+ inputs: [
20
+ { name: 'delegationHash', type: 'bytes32' },
21
+ { name: 'action', type: 'string' },
22
+ { name: 'details', type: 'string' },
23
+ { name: 'amount', type: 'uint256' },
24
+ ],
25
+ outputs: [],
26
+ stateMutability: 'nonpayable',
27
+ }] as const;
28
+
29
+ // ── Types ──────────────────────────────────────────────
30
+
31
+ interface AuditEntry {
32
+ timestamp: string;
33
+ action: string;
34
+ delegationId: string;
35
+ details: string;
36
+ amount: number;
37
+ txHash?: string;
38
+ }
39
+
40
+ // ── In-Memory + File Store ─────────────────────────────
41
+
42
+ const entries: AuditEntry[] = [];
43
+
44
+ function persist(): void {
45
+ writeFileSync(AUDIT_FILE, JSON.stringify(entries, null, 2));
46
+ }
47
+
48
+ // Load existing entries on startup
49
+ if (existsSync(AUDIT_FILE)) {
50
+ try {
51
+ const content = readFileSync(AUDIT_FILE, 'utf-8').trim();
52
+ if (content.startsWith('[')) {
53
+ entries.push(...JSON.parse(content));
54
+ } else if (content) {
55
+ // Legacy line-delimited format
56
+ entries.push(...content.split('\n').map(l => JSON.parse(l)));
57
+ }
58
+ } catch { /* start fresh */ }
59
+ }
60
+
61
+ export function getAuditLog(): AuditEntry[] {
62
+ return [...entries].reverse();
63
+ }
64
+
65
+ // ── Nonce Queue ────────────────────────────────────────
66
+
67
+ const txQueue: Array<() => Promise<void>> = [];
68
+ let processing = false;
69
+
70
+ async function drainQueue(): Promise<void> {
71
+ if (processing) return;
72
+ processing = true;
73
+ while (txQueue.length > 0) {
74
+ const fn = txQueue.shift()!;
75
+ try { await fn(); } catch (e) { console.error('[Audit] Queue tx failed:', e); }
76
+ }
77
+ processing = false;
78
+ }
79
+
80
+ // ── Public API ─────────────────────────────────────────
81
+
82
+ const ONCHAIN_ACTIONS = new Set(['approved', 'rejected', 'revoked', 'expired']);
83
+
84
+ export async function logAuditEvent(
85
+ action: string,
86
+ delegationId: string,
87
+ details: string,
88
+ amount: number,
89
+ ): Promise<string | null> {
90
+ const entry: AuditEntry = {
91
+ timestamp: new Date().toISOString(),
92
+ action,
93
+ delegationId,
94
+ details,
95
+ amount,
96
+ };
97
+
98
+ // Add to in-memory store immediately (dashboard sees it right away)
99
+ entries.push(entry);
100
+ persist();
101
+
102
+ // Only governance events go on-chain
103
+ if (!AUDIT_CONTRACT || !DEPLOYER_KEY || !ONCHAIN_ACTIONS.has(action)) {
104
+ console.log(`[Audit] Local: ${action} — ${details}`);
105
+ return null;
106
+ }
107
+
108
+ // Queue on-chain tx (updates entry.txHash when complete)
109
+ return new Promise((resolve) => {
110
+ txQueue.push(async () => {
111
+ try {
112
+ const hash = await sendOnChainLog(delegationId, action, details, amount);
113
+ if (hash) {
114
+ entry.txHash = hash;
115
+ persist(); // Re-persist with txHash
116
+ }
117
+ resolve(hash);
118
+ } catch {
119
+ resolve(null);
120
+ }
121
+ });
122
+ drainQueue();
123
+ });
124
+ }
125
+
126
+ // ── On-Chain Sender ────────────────────────────────────
127
+
128
+ // Track nonce to avoid "replacement transaction underpriced"
129
+ let currentNonce: number | null = null;
130
+
131
+ async function sendOnChainLog(
132
+ delegationId: string, action: string, details: string, amount: number
133
+ ): Promise<string | null> {
134
+ try {
135
+ const account = privateKeyToAccount(DEPLOYER_KEY as `0x${string}`);
136
+ const client = createWalletClient({
137
+ account,
138
+ chain: CHAIN,
139
+ transport: http(RPC_URL),
140
+ });
141
+
142
+ // Get or increment nonce
143
+ if (currentNonce === null) {
144
+ const { createPublicClient } = await import('viem');
145
+ const pub = createPublicClient({ chain: CHAIN, transport: http(RPC_URL) });
146
+ currentNonce = await pub.getTransactionCount({ address: account.address });
147
+ }
148
+ const nonce = currentNonce++;
149
+
150
+ const hash = await client.writeContract({
151
+ address: AUDIT_CONTRACT as `0x${string}`,
152
+ abi: LOG_ABI,
153
+ functionName: 'log',
154
+ args: [keccak256(toHex(delegationId)), action, details, BigInt(Math.round(amount * 1e6))],
155
+ nonce,
156
+ });
157
+
158
+ console.log(`[Audit] On-chain: ${action} — tx: ${hash}`);
159
+ return hash;
160
+ } catch (err) {
161
+ // Reset nonce on failure so next tx refetches from chain
162
+ currentNonce = null;
163
+ console.error(`[Audit] On-chain failed (${action} for ${delegationId}): ${err}`);
164
+ return null;
165
+ }
166
+ }