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
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
|
+
}
|