Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ DD_API_KEY=
DD_AGENT_HOST=localhost
DD_AGENT_PORT=8126

# Agent behaviour
MAX_DAILY_USD=10
MAX_AGENT_TURNS=10

# Notifications — POST purchase events to this URL (optional)
WEBHOOK_URL=

# Scheduled runs — JSON array of prompts, interval in hours (optional)
SCHEDULED_PROMPTS=[]
SCHEDULE_INTERVAL_HOURS=24

# Circle Programmable Wallet
CIRCLE_API_KEY=
CIRCLE_ENTITY_SECRET=
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ An autonomous Web3 shopping agent. Give it a prompt, it searches the web, picks
### 💸 Web3 Payments & Safety
- **Circle Programmable Wallets**: Uses **Circle's Developer-Controlled Wallets** on ARC-TESTNET. The agent holds its own USDC balance and signs transactions server-side via Circle's API — no private key management required.
- **USDC on ARC-TESTNET**: Facilitates real-world value transfer using stablecoins on ARC-TESTNET, with the agent wallet pre-funded with 20 USDC.
- **Spend Guard**: A hard-coded safety mechanism that enforces a **$10/day spending limit**. This prevents the agent from runaway spending in the event of an infinite loop or adversarial prompt.
- **Spend Guard**: A configurable daily spending limit (default $10, set via `MAX_DAILY_USD`). Backed by ClickHouse so it persists across restarts. Prevents runaway spending from infinite loops or adversarial prompts.
- **Wallet Balance Check**: Verifies on-chain USDC balance before submitting a payment. Fails fast with a clear error rather than letting the Circle API reject mid-flight.
- **Max Turns Guard**: The agentic loop is capped at `MAX_AGENT_TURNS` (default 10) iterations. If Claude loops without completing, the run aborts and logs an error instead of burning API credits indefinitely.
- **Dry-Run Mode**: Pass `--dry-run` (or `npm run dry-run`) to simulate a full agent run — search and evaluate without executing any payment or publishing any receipt. Useful for testing prompts and budget planning.
- **x402 Micropayment Protocol**: Implements a local "Payment Required" middleware. The agent handles `402` status codes by paying the required fee on-chain and retrying the request with a verifiable payment proof header.

### 📊 Transparency & Observability
- **Purchase Audit Log**: Every transaction is recorded in **ClickHouse Cloud**, capturing the original user query, selected product, price, and the immutable blockchain transaction hash.
- **Verified Receipts (cited.md)**: Automatically publishes public, markdown-formatted receipts via the **Senso platform**. These receipts are "citeable," making the agent's actions discoverable by search engines and other AI agents.
- **Datadog Instrumentation**: Full observability with Datadog APM. Tracks end-to-end agent run durations, per-tool execution spans, and custom metrics for payment success rates and on-chain confirmation times.
- **Datadog APM + Lapdog**: Full observability with Datadog APM. Tracks end-to-end agent run durations, per-tool execution spans, and custom metrics for payment success rates and on-chain confirmation times. In development, use **lapdog** for a local live dashboard showing every Claude API call with token counts, cost, cache hit rates, and tool traces — no Datadog account required.
- **GEO Monitoring**: Integrated AI brand visibility tracking. Monitors how major LLMs (ChatGPT, Claude, Perplexity, Gemini) perceive and represent the "Shop3" brand across the web.
- **Webhook Notifications**: Set `WEBHOOK_URL` to receive a POST on every completed (or dry-run) purchase with product, price, tx hash, and receipt URL.
- **Scheduled Runs**: `npm run schedule` runs a configurable list of prompts on a repeat interval (set via `SCHEDULED_PROMPTS` and `SCHEDULE_INTERVAL_HOURS`).
- **Purchase Memory**: The agent can call `check_purchase_history` before buying to avoid repurchasing the same product across separate runs.
- **Payment Replay Protection**: The x402 search middleware tracks used transaction hashes. A proof that has already unlocked a search result cannot be reused.

## Quick start

Expand Down Expand Up @@ -63,7 +70,20 @@ node history.js # last 10 purchases
node history.js 25 # last N purchases
```

6. (Optional) Run the local search middleware (x402 payment-gated search):
6. (Optional) Run with **lapdog** for a local LLM observability dashboard:

```bash
# Install lapdog (one-time)
pip install ddapm-test-agent

# Run agent with live token/cost/trace dashboard at lapdog.datadoghq.com
npm run lapdog

# Or run the search server with lapdog
npm run lapdog:server
```

7. (Optional) Run the local search middleware (x402 payment-gated search):

```bash
npm run start:server
Expand Down
119 changes: 90 additions & 29 deletions agent.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const Anthropic = require('@anthropic-ai/sdk');
const { searchWeb } = require('./search');
const { mockPaymentFlow, getWalletAddress } = require('./payment');
const { logPurchase } = require('./memory');
const { logPurchase, getRecentPurchases } = require('./memory');
const { publishReceipt } = require('./publish');
const { withSpan, increment, gauge, timing } = require('./telemetry');
const { notifyPurchase } = require('./notify');
const { withSpan, withLLMSpan, increment, gauge, timing } = require('./telemetry');

const MAX_TURNS = parseInt(process.env.MAX_AGENT_TURNS) || 10;
const client = new Anthropic();

const tools = [
Expand All @@ -22,7 +24,7 @@ const tools = [
},
{
name: 'pay_for_purchase',
description: 'Pay for a selected product/service from the agent smart wallet using USDC on Base Sepolia. Enforces $10/day spending limit.',
description: 'Pay for a selected product/service from the agent smart wallet using USDC on ARC-TESTNET. Enforces a daily spending limit.',
input_schema: {
type: 'object',
properties: {
Expand Down Expand Up @@ -68,9 +70,20 @@ const tools = [
required: ['query', 'selected_result', 'price', 'tx_hash', 'source_url'],
},
},
{
name: 'check_purchase_history',
description: 'Check what Shop3 has already purchased. Use this before buying to avoid duplicates.',
input_schema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Number of recent purchases to retrieve (default 10)' },
},
required: [],
},
},
];

async function executeTool(name, input, context) {
async function executeTool(name, input, context, dryRun) {
switch (name) {
case 'search_web': {
console.log(`\n[agent] Searching: "${input.query}"`);
Expand All @@ -87,6 +100,15 @@ async function executeTool(name, input, context) {
}

case 'pay_for_purchase': {
if (dryRun) {
console.log(`\n[agent] DRY RUN — would pay for: ${input.selected_result} (${input.price})`);
context.txHash = '0x0000000000000000000000000000000000000000000000000000000000000000';
context.selectedResult = input.selected_result;
context.price = input.price;
context.sourceUrl = input.source_url;
increment('payment.dry_run');
return { success: true, dry_run: true, tx_hash: context.txHash };
}
console.log(`\n[agent] Paying for: ${input.selected_result} (${input.price})`);
const start = Date.now();
const txHash = await withSpan('agent.tool.pay_for_purchase', {
Expand Down Expand Up @@ -116,6 +138,10 @@ async function executeTool(name, input, context) {
}

case 'publish_receipt': {
if (dryRun) {
console.log(`\n[agent] DRY RUN — skipping receipt publish for: ${input.selected_result}`);
return { success: true, dry_run: true, receipt_url: null };
}
console.log(`\n[agent] Publishing receipt to cited.md`);
const url = await withSpan('agent.tool.publish_receipt', {}, () =>
publishReceipt({
Expand All @@ -131,55 +157,79 @@ async function executeTool(name, input, context) {
return { success: true, receipt_url: url };
}

case 'check_purchase_history': {
const purchases = await withSpan('agent.tool.check_purchase_history', {}, () =>
getRecentPurchases(input.limit ?? 10)
);
if (purchases.length === 0) {
return { purchases: [], message: 'No purchases yet.' };
}
return {
purchases: purchases.map((p) => ({
product: p.selected_result,
price: p.price,
when: p.timestamp,
tx_hash: p.tx_hash,
})),
};
}

default:
throw new Error(`Unknown tool: ${name}`);
}
}

async function runAgent(userPrompt) {
async function runAgent(userPrompt, { dryRun = false } = {}) {
const runStart = Date.now();
increment('agent.run.started');
if (dryRun) increment('agent.run.dry_run');

const walletAddress = await getWalletAddress();
console.log(`\n[agent] Smart wallet: ${walletAddress}`);
if (dryRun) console.log('[agent] DRY RUN MODE — no payments or receipts will be submitted');
console.log(`[agent] Starting: "${userPrompt}"\n`);

const context = {};
const messages = [
{
role: 'user',
content: userPrompt,
},
];
const messages = [{ role: 'user', content: userPrompt }];

const systemPrompt = `You are an autonomous Web3 shopping agent. Your job is to:
1. Search the web to find the best option matching the user's request
2. Evaluate results and select the best one under the user's budget
3. Pay for it autonomously from your smart wallet (USDC on Base Sepolia testnet)
4. Log the purchase to the database
5. Publish a verified receipt
const systemPrompt = `You are Shop3, an autonomous Web3 shopping agent. Your job is to:
1. (Optional) Check purchase history to avoid buying duplicates
2. Search the web to find the best option matching the user's request
3. Evaluate results and select the best one under the user's budget
4. Pay for it autonomously from your smart wallet (USDC on ARC-TESTNET)
5. Log the purchase to the database
6. Publish a verified receipt

Your smart wallet address is: ${walletAddress}
Daily spend limit: $10 USD (enforced on-chain)
Network: Base Sepolia (testnet)
Daily spend limit: $${parseFloat(process.env.MAX_DAILY_USD) || 10} USD (enforced on-chain)
Network: ARC-TESTNET
Payment token: USDC
${dryRun ? '\nDRY RUN: You are in simulation mode. Payments will not be executed and receipts will not be published.' : ''}

When selecting a result to buy, prefer options that are:
- Under $10/month or one-time
- Have clear pricing
- Are reputable API services or products

Always complete all 4 steps: search → pay → log → publish. Do not stop early.`;
Always complete all steps: search → pay → log → publish. Do not stop early.`;

let turns = 0;

// Agentic loop
while (true) {
const response = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 4096,
system: systemPrompt,
tools,
messages,
});
if (turns >= MAX_TURNS) {
throw new Error(`Agent exceeded maximum turn limit (${MAX_TURNS}). Aborting to prevent runaway loop.`);
}
turns++;

const response = await withLLMSpan('claude-sonnet-4-6', () =>
client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 4096,
system: systemPrompt,
tools,
messages,
})
);

messages.push({ role: 'assistant', content: response.content });

Expand All @@ -190,8 +240,18 @@ Always complete all 4 steps: search → pay → log → publish. Do not stop ear
.join('\n');
timing('agent.run.duration_ms', Date.now() - runStart);
increment('agent.run.completed');
gauge('agent.run.turns', turns);
console.log('\n[agent] Done.\n');
console.log(finalText);

await notifyPurchase({
selectedResult: context.selectedResult,
price: context.price,
txHash: context.txHash,
receiptUrl: context.receiptUrl,
dryRun,
});

return { summary: finalText, ...context };
}

Expand All @@ -203,9 +263,10 @@ Always complete all 4 steps: search → pay → log → publish. Do not stop ear

let result;
try {
result = await executeTool(block.name, block.input, context);
result = await executeTool(block.name, block.input, context, dryRun);
} catch (err) {
console.error(`[agent] Tool error (${block.name}):`, err.message);
increment('agent.tool.error', { tool: block.name });
result = { error: err.message };
}

Expand Down
18 changes: 9 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ if (missing.length > 0) {

const { runAgent } = require('./agent');

const prompt = process.argv.slice(2).join(' ') || 'Find me the best web data API subscription under $10 and buy it';
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const promptArgs = args.filter((a) => a !== '--dry-run');
const prompt = promptArgs.join(' ') || 'Find me the best web data API subscription under $10 and buy it';

console.log('='.repeat(60));
console.log(' Valution Agent — Autonomous Web3 Shopping');
console.log(' Shop3 — Autonomous Web3 Shopping Agent');
console.log('='.repeat(60));
if (dryRun) console.log(' [DRY RUN MODE]');
console.log(`\nPrompt: "${prompt}"\n`);

runAgent(prompt)
runAgent(prompt, { dryRun })
.then((result) => {
console.log('\n' + '='.repeat(60));
if (result.receiptUrl) {
console.log(`\nReceipt: ${result.receiptUrl}`);
}
if (result.txHash) {
console.log(`Tx: ${result.txHash}`);
}
if (result.receiptUrl) console.log(`Receipt: ${result.receiptUrl}`);
if (result.txHash) console.log(`Tx: ${result.txHash}`);
console.log('='.repeat(60));
})
.catch((err) => {
Expand Down
24 changes: 24 additions & 0 deletions notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const axios = require('axios');

async function notifyPurchase({ selectedResult, price, txHash, receiptUrl, dryRun = false }) {
const url = process.env.WEBHOOK_URL;
if (!url) return;

const payload = {
event: dryRun ? 'shop3.dry_run' : 'shop3.purchase_complete',
product: selectedResult,
price,
tx_hash: txHash ?? null,
receipt_url: receiptUrl ?? null,
timestamp: new Date().toISOString(),
};

try {
await axios.post(url, payload, { timeout: 5000 });
console.log(`[notify] Webhook delivered to ${url}`);
} catch (err) {
console.warn(`[notify] Webhook failed (non-fatal): ${err.message}`);
}
}

module.exports = { notifyPurchase };
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"start:server": "node server.js",
"history": "node history.js",
"setup:geo": "node scripts/setup-geo.js",
"geo:status": "node scripts/geo-status.js"
"geo:status": "node scripts/geo-status.js",
"lapdog": "lapdog node index.js",
"lapdog:server": "lapdog node server.js",
"schedule": "node scripts/schedule.js",
"dry-run": "node index.js --dry-run"
},
"keywords": [],
"author": "",
Expand All @@ -18,13 +22,10 @@
"@anthropic-ai/sdk": "^0.98.0",
"@circle-fin/developer-controlled-wallets": "^10.3.1",
"@clickhouse/client": "^1.18.5",
"@zerodev/ecdsa-validator": "^5.4.9",
"@zerodev/sdk": "^5.5.10",
"axios": "^1.16.1",
"dd-trace": "^5.104.0",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"permissionless": "^0.3.5",
"tslib": "^2.8.1",
"viem": "^2.50.4"
}
Expand Down
Loading
Loading