Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b26a235
feat(basepay): add schemas for BasePay action provider
osr21 Jun 20, 2026
074d35a
feat(basepay): add BasePayActionProvider with 5 USDC actions
osr21 Jun 20, 2026
fe7bd2b
feat(basepay): add basepay index exports
osr21 Jun 20, 2026
787cf92
feat(basepay): export basepay action provider from index
osr21 Jun 20, 2026
77a1f99
test: add BasePay action provider unit tests
osr21 Jun 28, 2026
c156f72
feat: implement policy hook scaffolding with two-set atomic gating
LumenFromTheFuture Jun 29, 2026
2143d4d
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
92cf7ee
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
242ccae
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
7086b22
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
56dbbea
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
f79f934
feat(policy): add PolicyOutcome and PolicyReceipt interfaces
LumenFromTheFuture Jun 29, 2026
a2b25aa
merge(basepay): apply policy hook fixups from osr21
LumenFromTheFuture Jun 29, 2026
d952631
fix(policy): add relay_confirmed outcome and fix indentation
LumenFromTheFuture Jun 29, 2026
4c29157
style: apply prettier formatter pass to basepay action provider
LumenFromTheFuture Jun 29, 2026
ccb4062
feat(basepay): record relay_confirmed outcome on successful relay acc…
LumenFromTheFuture Jun 29, 2026
e7cde28
fix(basepay): classify duplicate decision_ref as denied outcome
LumenFromTheFuture Jun 30, 2026
2dfc8bc
fix(basepay): split unbound_execution into missing and duplicate deci…
LumenFromTheFuture Jun 30, 2026
8380c34
test(basepay): update tests for missing vs duplicate decision_ref
LumenFromTheFuture Jun 30, 2026
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions typescript/agentkit/src/action-providers/basepay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { BasePayActionProvider, basePayActionProvider } from "./basepayActionProvider";
export type { BasePayConfig } from "./basepayActionProvider";
export {
SendUsdcSchema,
SendUsdcGaslessSchema,
BatchPayUsdcSchema,
CreateEscrowSchema,
SubscribeSchema,
} from "./schemas";
74 changes: 74 additions & 0 deletions typescript/agentkit/src/action-providers/basepay/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { z } from "zod";

const ethAddress = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid 0x Ethereum address");

export const SendUsdcSchema = z.object({
to: ethAddress.describe("Recipient address on Base Mainnet"),
amount: z
.string()
.describe('Amount of USDC to send, as a human-readable decimal (e.g. "10.5" for 10.5 USDC)'),
});

export const SendUsdcGaslessSchema = z.object({
to: ethAddress.describe("Recipient address on Base Mainnet"),
amount: z
.string()
.describe(
'Amount of USDC to send gaslessly via EIP-3009, as a decimal (e.g. "5" for 5 USDC). ' +
"The BasePay relay pays the ETH gas — the agent wallet needs no ETH for this action.",
),
});

export const BatchPayUsdcSchema = z.object({
recipients: z
.array(
z.object({
address: ethAddress.describe("Recipient wallet address"),
amount: z.string().describe('USDC amount for this recipient (e.g. "10.5")'),
}),
)
.min(1)
.max(200)
.describe("List of recipient address and USDC amount pairs (max 200 entries)."),
memo: z
.string()
.max(64)
.default("")
.describe("Optional note recorded on-chain with the batch payment"),
});

export const CreateEscrowSchema = z.object({
payee: ethAddress.describe(
"Address of the escrow beneficiary who can claim the USDC after the lock period expires",
),
amount: z.string().describe('Amount of USDC to lock in escrow (e.g. "100" for 100 USDC)'),
unlockAfterSeconds: z
.number()
.int()
.min(60)
.describe(
"Seconds until the payee can claim, or the payer can reclaim. " +
"Examples: 86400 = 1 day, 604800 = 1 week, 2592000 = 30 days",
),
memo: z.string().max(64).default("").describe("Optional note recorded on-chain with the escrow"),
});

export const SubscribeSchema = z.object({
payee: ethAddress.describe("Address that receives USDC at each billing interval"),
amount: z
.string()
.describe('USDC amount charged per interval (e.g. "9.99" for $9.99 per period)'),
intervalSeconds: z
.number()
.int()
.min(3600)
.describe(
"Seconds between each recurring charge. " +
"Examples: 604800 = weekly, 2592000 = monthly, 31536000 = yearly",
),
memo: z
.string()
.max(64)
.default("")
.describe("Optional description of the subscription recorded on-chain"),
});
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export * from "./zerion";
export * from "./zerodev";
export * from "./zeroX";
export * from "./zora";
export * from "./basepay";
1 change: 1 addition & 0 deletions typescript/agentkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./agentkit";
export * from "./wallet-providers";
export * from "./action-providers";
export * from "./network";
export * from "./policy";
2 changes: 2 additions & 0 deletions typescript/agentkit/src/policy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./interfaces";
export * from "./utils";
46 changes: 46 additions & 0 deletions typescript/agentkit/src/policy/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export interface ActionContext {
action: string;
to?: string;
amount_usdc?: string;
aggregate_usdc?: string;
recipient_count?: number;
recipient_allocation_hash?: string;
per_recipient_max?: string;
transfer_mechanism?: "direct" | "eip3009" | "permit" | "x402";
creates_recurring_obligation?: boolean;
creates_commitment?: boolean;
}

export interface PolicyDecision {
allowed: boolean;
reason_codes?: string[];
signal_refs?: Record<string, string>;
policy_version: string;
action_context_hash: string;
decision_ref: string;
issued_at_ms: number;
expires_at_ms: number;
signature?: string;
}

export type PolicyOutcome =
| "executed"
| "relay_confirmed"
| "failed"
| "denied"
| "expired"
| "context_drift"
| "unauditable_outcome";

export interface PolicyReceipt {
decision: PolicyDecision;
outcome: PolicyOutcome;
tx_hash?: string;
error?: string;
issued_at_ms: number;
}

export interface PolicyProvider {
evaluate(ctx: ActionContext): Promise<PolicyDecision>;
record?(receipt: PolicyReceipt): Promise<void>;
}
33 changes: 33 additions & 0 deletions typescript/agentkit/src/policy/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import canonicalize from "canonicalize";
import { ActionContext } from "./interfaces";

/**
* SHA-256 hash of a string, using the Web Crypto API.
*/
export async function sha256(text: string): Promise<string> {
const msgUint8 = new TextEncoder().encode(text);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}

/**
* recipientAllocationHash: RFC 8785 JCS hash over sorted address+amount pairs.
* Catches address substitution, amount redistribution, and silent reordering.
*/
export async function recipientAllocationHash(
recipients: Array<{ address: string; amount: bigint }>,
): Promise<string> {
const normalized = recipients
.map(r => ({ to: r.address.toLowerCase(), amount_atomic: r.amount.toString() }))
.sort((a, b) => a.to.localeCompare(b.to) || a.amount_atomic.localeCompare(b.amount_atomic));
return sha256(canonicalize(normalized) ?? "{}");
}

/**
* actionContextHash: RFC 8785 JCS hash of an ActionContext.
* Policy-independent content identifier for cross-implementation join keys.
*/
export async function actionContextHash(ctx: ActionContext): Promise<string> {
return sha256(canonicalize(ctx) ?? "{}");
}
Loading