Skip to content

Instantly share code, notes, and snippets.

@pamorgan01
Created June 10, 2026 04:34
Show Gist options
  • Select an option

  • Save pamorgan01/cb267b0daa794651fe60bd721ff8083f to your computer and use it in GitHub Desktop.

Select an option

Save pamorgan01/cb267b0daa794651fe60bd721ff8083f to your computer and use it in GitHub Desktop.
Solana Agent Allowance Guard demo for Superteam Canada bounty

Solana Agent Allowance Guard Demo

A technical demo for Solana Native Subscriptions and Allowances showing how an AI agent can operate with a bounded budget instead of direct wallet custody.

Concept

The user grants a fixed or recurring allowance to an agent-controlled delegate. The agent can propose actions such as API calls, swaps, or content purchases, but the local guard checks:

  • cap remaining
  • expiration
  • per-action maximum
  • allowed merchant/program category
  • suspicious approval flags

Only safe actions are converted into an execution plan. Risky or out-of-policy actions are rejected before any on-chain transfer attempt.

This aligns with the official primitive:

  • Solana describes allowances as fixed delegation with a cap and optional expiration, suitable for autonomous agents operating inside a budget.
  • The official solana-foundation/subscriptions README documents fixed delegation, recurring delegation, subscription plans, and @solana/subscriptions client methods such as createFixedDelegation, transferFixed, createRecurringDelegation, and transferRecurring.

Files

  • src/agentAllowanceGuard.ts: runnable TypeScript-style demo logic with sample decisions.
  • src/subscriptionsAdapter.ts: adapter boundary showing where the official @solana/subscriptions SDK calls fit.
  • sample-output.json: expected output from the demo scenario.

Demo scenario

A Canadian analytics agent receives a 25 USDC fixed allowance for pay-per-call data tasks. It tries four actions:

  1. Pay 3.50 USDC for an approved API call: allowed.
  2. Pay 6.00 USDC for a second approved API call: allowed.
  3. Attempt 18.00 USDC swap on an unknown program: rejected for merchant/category risk.
  4. Attempt 30.00 USDC spend: rejected because it exceeds both per-action max and remaining allowance.

Why this is useful

The demo shows how subscriptions and allowances let a user separate permission from custody. An AI agent can be productive, but it cannot exceed the user's pre-authorized budget or merchant scope.

Running locally

npm install
npm run demo

This gist is intentionally offline-safe: it does not require private keys, wallets, RPC calls, or funds. The adapter file marks the exact place where a production implementation would call the official Solana subscriptions client.

{
"name": "solana-agent-allowance-guard-demo",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"demo": "tsx src/agentAllowanceGuard.ts"
},
"dependencies": {
"@solana/subscriptions": "latest",
"tsx": "latest",
"typescript": "latest"
},
"devDependencies": {}
}
{
"allowance": {
"id": "agent-canada-research-budget",
"mint": "USDC",
"totalCap": 25,
"spent": 9.5,
"perActionMax": 10
},
"decisions": [
{ "actionId": "a1", "decision": "ALLOW", "remainingAfter": 21.5 },
{ "actionId": "a2", "decision": "ALLOW", "remainingAfter": 15.5 },
{ "actionId": "a3", "decision": "ESCALATE", "reasons": ["category swap is not allowed", "merchant unknown-dex-program is not allowlisted", "suspicious approval requires human review"] },
{ "actionId": "a4", "decision": "REJECT", "reasons": ["amount exceeds per-action maximum", "amount exceeds remaining allowance"] }
]
}
type Category = "api" | "content" | "swap" | "unknown";
type Allowance = {
id: string;
mint: string;
delegate: string;
totalCap: number;
spent: number;
perActionMax: number;
expiresAt: string;
allowedCategories: Category[];
allowedMerchants: string[];
};
type AgentAction = {
id: string;
merchant: string;
category: Category;
amount: number;
memo: string;
suspiciousApproval?: boolean;
};
type Decision = {
actionId: string;
decision: "ALLOW" | "REJECT" | "ESCALATE";
reasons: string[];
remainingBefore: number;
remainingAfter: number;
executionPlan?: {
model: "fixed-delegation-transfer";
sdkMethod: "transferFixed";
memo: string;
};
};
function decide(allowance: Allowance, action: AgentAction, now = new Date()): Decision {
const reasons: string[] = [];
const remainingBefore = allowance.totalCap - allowance.spent;
if (now > new Date(allowance.expiresAt)) reasons.push("allowance expired");
if (action.amount <= 0) reasons.push("amount must be positive");
if (action.amount > allowance.perActionMax) reasons.push("amount exceeds per-action maximum");
if (action.amount > remainingBefore) reasons.push("amount exceeds remaining allowance");
if (!allowance.allowedCategories.includes(action.category)) reasons.push(`category ${action.category} is not allowed`);
if (!allowance.allowedMerchants.includes(action.merchant)) reasons.push(`merchant ${action.merchant} is not allowlisted`);
if (action.suspiciousApproval) reasons.push("suspicious approval requires human review");
if (reasons.length > 0) {
return {
actionId: action.id,
decision: action.suspiciousApproval ? "ESCALATE" : "REJECT",
reasons,
remainingBefore,
remainingAfter: remainingBefore,
};
}
return {
actionId: action.id,
decision: "ALLOW",
reasons: ["inside fixed allowance cap", "merchant and category allowed"],
remainingBefore,
remainingAfter: remainingBefore - action.amount,
executionPlan: {
model: "fixed-delegation-transfer",
sdkMethod: "transferFixed",
memo: action.memo,
},
};
}
const allowance: Allowance = {
id: "agent-canada-research-budget",
mint: "USDC",
delegate: "VoidKaiAgentDelegate111111111111111111111111",
totalCap: 25,
spent: 0,
perActionMax: 10,
expiresAt: "2026-06-23T22:59:59.999Z",
allowedCategories: ["api", "content"],
allowedMerchants: ["pay.sh:canada-data-api", "pay.sh:solana-rpc-lite"],
};
const actions: AgentAction[] = [
{ id: "a1", merchant: "pay.sh:canada-data-api", category: "api", amount: 3.5, memo: "Canadian builder index lookup" },
{ id: "a2", merchant: "pay.sh:solana-rpc-lite", category: "api", amount: 6, memo: "RPC batch for subscription demo" },
{ id: "a3", merchant: "unknown-dex-program", category: "swap", amount: 18, memo: "Unplanned swap route", suspiciousApproval: true },
{ id: "a4", merchant: "pay.sh:canada-data-api", category: "api", amount: 30, memo: "Oversized scrape request" },
];
const decisions: Decision[] = [];
for (const action of actions) {
const decision = decide(allowance, action, new Date("2026-06-10T12:00:00.000Z"));
decisions.push(decision);
if (decision.decision === "ALLOW") allowance.spent += action.amount;
}
console.log(JSON.stringify({ allowance, decisions }, null, 2));
export { decide };
export type { Allowance, AgentAction, Decision };
// Adapter boundary for a production Solana implementation.
// The official repository documents @solana/subscriptions as the TypeScript client.
// In production, replace this file's placeholders with real wallet, mint, token-account,
// and connection objects, then call the SDK methods referenced below.
import { SubscriptionsClient } from "@solana/subscriptions";
export async function createAgentFixedAllowance(client: SubscriptionsClient) {
// 1. Ensure the user's Subscription Authority exists for (user, mint).
// await client.initSubscriptionAuthority(...)
// 2. Create a fixed delegation for the agent delegate, with cap and expiry.
// await client.createFixedDelegation({
// delegatee: agentDelegate,
// amount: 25_000_000n, // 25 USDC if mint decimals = 6
// expiresAt: Math.floor(Date.parse("2026-06-23T22:59:59.999Z") / 1000),
// })
return "fixed allowance creation plan prepared";
}
export async function executeAllowedAgentSpend(client: SubscriptionsClient) {
// The local guard must return ALLOW before this point.
// await client.transferFixed({ amount, destinationTokenAccount, memo })
return "fixed delegation transfer plan prepared";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment