Skip to content
Open
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
31 changes: 31 additions & 0 deletions examples/budget-governance/aixyz.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { AixyzConfig } from "aixyz/config";

const config: AixyzConfig = {
name: "Budget Governance Agent",
description:
"AI agent with x402 payments AND session-level budget governance via agentpay-mcp. " +
"Enforces per-session caps, velocity limits, and category policies on top of x402 pricing.",
version: "0.1.0",
x402: {
payTo: process.env.X402_PAY_TO ?? "0x0799872E07EA7a63c79357694504FE66EDfE4a0A",
network: process.env.X402_NETWORK ?? (process.env.NODE_ENV === "production" ? "eip155:8453" : "eip155:84532"),
},
skills: [
{
id: "check-budget",
name: "Check Budget",
description: "Check remaining session budget and spending breakdown by category",
tags: ["budget", "governance", "agentpay"],
examples: ["How much budget do I have left?", "Show my spending breakdown"],
},
{
id: "request-payment",
name: "Request Payment",
description: "Request an x402 payment with budget governance checks (per-call limit, session cap, category cap)",
tags: ["payment", "x402", "governance", "agentpay"],
examples: ["Pay $0.50 for data API access", "Execute payment for compute service"],
},
],
};

export default config;
47 changes: 47 additions & 0 deletions examples/budget-governance/app/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { openai } from "@ai-sdk/openai";
import { stepCountIs, ToolLoopAgent } from "ai";
import type { Accepts } from "aixyz/accepts";

import checkBudget from "./tools/check-budget";
import requestPayment from "./tools/request-payment";

const instructions = `
# Budget Governance Agent

You are a payment-aware AI agent with built-in spend governance.
Every x402 payment you process goes through agentpay-mcp's budget
governance layer BEFORE settlement.

## Governance Rules

- Each session has a maximum budget (default $5.00 USDC)
- Each individual payment has a per-call limit (default $1.00 USDC)
- Spending is tracked by category (data, compute, services)
- Each category has its own cap to prevent runaway spend in one area

## Guidelines

- Always check remaining budget before initiating large payments
- If a payment would exceed any limit, explain which limit was hit
- Report spending breakdowns when asked
- Never bypass governance checks — they exist to protect the agent operator

## Why This Matters

x402 gives agents the ability to pay. agentpay-mcp gives operators the
ability to control HOW MUCH agents pay. Without governance, autonomous
agents can drain wallets. This is the missing layer between "agents can
pay" and "agents can pay safely."
`.trim();

export const accepts: Accepts = {
scheme: "exact",
price: "$0.001",
};

export default new ToolLoopAgent({
model: openai("gpt-4o-mini"),
instructions: instructions,
tools: { checkBudget, requestPayment },
stopWhen: stepCountIs(10),
});
47 changes: 47 additions & 0 deletions examples/budget-governance/app/budget-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* In-memory budget state for the governance example.
*
* In production, replace this with agentpay-mcp MCP tool calls:
* - check_budget → getBudgetState()
* - approve_payment → recordSpend()
* - get_spending_report → full audit trail
*
* agentpay-mcp persists state across sessions and settles payments
* on-chain via x402 on Base (USDC).
*
* @see https://github.com/up2itnow0822/agentpay-mcp
*/

export interface BudgetState {
sessionCap: number;
perCallLimit: number;
spent: number;
callCount: number;
blockedCount: number;
categoryCaps: Record<string, number>;
categorySpent: Record<string, number>;
}

const state: BudgetState = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope budget state to a session key

The example advertises session-level caps, but this singleton state is shared process-wide, so spending/blocks from one conversation affect all others. In a multi-user server process this causes cross-session interference and incorrect enforcement of the documented “per session” limits.

Useful? React with 👍 / 👎.

sessionCap: 5.0, // $5.00 USDC per session
perCallLimit: 1.0, // $1.00 max per individual payment
spent: 0,
callCount: 0,
blockedCount: 0,
categoryCaps: {
data: 3.0, // $3.00 cap for data APIs
compute: 2.0, // $2.00 cap for compute services
services: 2.0, // $2.00 cap for general services
},
categorySpent: {},
};

export function getBudgetState(): BudgetState {
return state;
}

export function recordSpend(amount: number, category: string): void {
state.spent += amount;
state.callCount++;
state.categorySpent[category] = (state.categorySpent[category] ?? 0) + amount;
}
39 changes: 39 additions & 0 deletions examples/budget-governance/app/tools/check-budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { tool } from "ai";
import { z } from "zod";
import { getBudgetState } from "../budget-state";

/**
* Check remaining session budget and spending breakdown.
*
* In production, this calls agentpay-mcp's `get_spending_report` MCP tool.
* This example uses an in-memory budget state for demonstration.
*
* @see https://github.com/up2itnow0822/agentpay-mcp
*/
export default tool({
description: "Check remaining session budget and spending breakdown by category",
parameters: z.object({}),
execute: async () => {
const budget = getBudgetState();
const remaining = budget.sessionCap - budget.spent;

return {
sessionCap: budget.sessionCap.toString(),
spent: budget.spent.toString(),
remaining: remaining.toString(),
perCallLimit: budget.perCallLimit.toString(),
callsMade: budget.callCount,
callsBlocked: budget.blockedCount,
byCategory: Object.fromEntries(
Object.entries(budget.categorySpent).map(([k, v]) => [
k,
{
spent: v.toString(),
cap: (budget.categoryCaps[k] ?? budget.sessionCap).toString(),
remaining: ((budget.categoryCaps[k] ?? budget.sessionCap) - v).toString(),
},
])
),
};
},
});
80 changes: 80 additions & 0 deletions examples/budget-governance/app/tools/request-payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { tool } from "ai";
import { z } from "zod";
import { getBudgetState, recordSpend } from "../budget-state";

/**
* Request an x402 payment with budget governance checks.
*
* Enforces three gates before any payment settles:
* 1. Per-call limit — no single payment exceeds the operator's threshold
* 2. Session cap — total session spend stays within budget
* 3. Category cap — spending in each category (data, compute, services)
* is independently capped
*
* In production, this calls agentpay-mcp's `approve_payment` MCP tool
* which settles via x402 on Base (USDC).
*
* @see https://github.com/up2itnow0822/agentpay-mcp
*/
export default tool({
description:
"Request an x402 payment with budget governance. " +
"Checks per-call limit, session cap, and category cap before approving.",
parameters: z.object({
amount: z.string().describe("Payment amount in USD (e.g. '0.25')"),
category: z
.enum(["data", "compute", "services"])
.describe("Spending category for this payment"),
recipient: z.string().describe("Recipient address or service name"),
reason: z.string().describe("Why this payment is needed"),
}),
execute: async ({ amount, category, recipient, reason }) => {
const cost = parseFloat(amount);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate payment amount before governance checks

Parsing amount with parseFloat without validating finiteness/positivity lets invalid inputs bypass the gates: e.g., "abc" becomes NaN (all > checks are false) and then recordSpend turns totals into NaN, while negative values reduce spend and effectively increase remaining budget. This breaks the governance guarantees and can approve payments that should be blocked.

Useful? React with 👍 / 👎.

const budget = getBudgetState();

// Gate 1: per-call limit
if (cost > budget.perCallLimit) {
budget.blockedCount++;
return {
approved: false,
reason: `Per-call limit exceeded: $${cost} > $${budget.perCallLimit} limit`,
gate: "per-call",
};
}

// Gate 2: session cap
if (budget.spent + cost > budget.sessionCap) {
budget.blockedCount++;
return {
approved: false,
reason: `Session cap exceeded: $${(budget.spent + cost).toFixed(2)} > $${budget.sessionCap} cap`,
gate: "session",
};
}

// Gate 3: category cap
const catSpent = budget.categorySpent[category] ?? 0;
const catCap = budget.categoryCaps[category] ?? budget.sessionCap;
if (catSpent + cost > catCap) {
budget.blockedCount++;
return {
approved: false,
reason: `Category '${category}' cap exceeded: $${(catSpent + cost).toFixed(2)} > $${catCap} cap`,
gate: "category",
};
}

// All gates passed — record the spend
recordSpend(cost, category);

return {
approved: true,
amount: cost.toFixed(2),
category,
recipient,
reason,
sessionRemaining: (budget.sessionCap - budget.spent).toFixed(2),
receipt: `x402-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
};
},
});
14 changes: 14 additions & 0 deletions examples/budget-governance/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "budget-governance",
"version": "0.1.0",
"private": true,
Comment on lines +1 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add runnable scripts to the new example package

This package is missing scripts.dev/scripts.build, so the documented example workflow does not work here (bun run dev in this directory exits with Script not found "dev"). Without these scripts, users cannot run or build this example consistently with the other examples/* packages.

Useful? React with 👍 / 👎.

"dependencies": {
"aixyz": "workspace:*",
"@ai-sdk/openai": "^1.0.0",
"ai": "^4.0.0",
"zod": "^3.24.0"
},
"devDependencies": {
"typescript": "^5.7.0"
}
}
12 changes: 12 additions & 0 deletions examples/budget-governance/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["app/**/*.ts", "aixyz.config.ts"]
}