-
Notifications
You must be signed in to change notification settings - Fork 8
feat(examples): add budget governance example with agentpay-mcp #324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; |
| 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), | ||
| }); |
| 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 = { | ||
| 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; | ||
| } | ||
| 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(), | ||
| }, | ||
| ]) | ||
| ), | ||
| }; | ||
| }, | ||
| }); |
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Parsing 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)}`, | ||
| }; | ||
| }, | ||
| }); | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This package is missing 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" | ||
| } | ||
| } | ||
| 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"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example advertises session-level caps, but this singleton
stateis 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 👍 / 👎.