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
0d58732
feat(basepay): add schemas for BasePay action provider
osr21 Jun 20, 2026
75fb829
feat(basepay): add BasePayActionProvider with 5 USDC actions
osr21 Jun 20, 2026
a3f5098
feat(basepay): add basepay index exports
osr21 Jun 20, 2026
1853349
feat(basepay): export basepay action provider from index
osr21 Jun 20, 2026
b5a3731
test: add BasePay action provider unit tests
osr21 Jun 28, 2026
c931502
feat: implement policy hook scaffolding with two-set atomic gating
LumenFromTheFuture Jun 29, 2026
cf80bf2
feat(policy): add PolicyOutcome and PolicyReceipt interfaces
LumenFromTheFuture Jun 29, 2026
79b53cb
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
9d30090
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
c8760d3
fix(basepay): policy hook fixup — seven items
osr21 Jun 29, 2026
f4b1f8d
fix(policy): add relay_confirmed outcome and fix indentation
LumenFromTheFuture Jun 29, 2026
82234ff
style: apply prettier formatter pass to basepay action provider
LumenFromTheFuture Jun 29, 2026
5e61298
feat(basepay): record relay_confirmed outcome on successful relay acc…
LumenFromTheFuture Jun 29, 2026
c7bfaae
test(basepay): update tests for missing vs duplicate decision_ref
LumenFromTheFuture Jun 30, 2026
15bdc2e
fix(basepay): gasless outer catch + policy-resolution regressions (FI…
LumenFromTheFuture Aug 25, 2026
1c2330e
fix(basepay): authority-bound decision consumption + relay-untouched …
LumenFromTheFuture Aug 26, 2026
1d16d0a
docs(basepay): precise single-attempt contract wording (osr21 review)
LumenFromTheFuture Aug 27, 2026
9d28b41
docs(basepay): replace review-history labels with direct invariants i…
LumenFromTheFuture Aug 28, 2026
746975e
docs(basepay): fix checkPolicy docblock placement, ambiguity-aware au…
LumenFromTheFuture Aug 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>;
}
34 changes: 34 additions & 0 deletions typescript/agentkit/src/policy/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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.
* Order-insensitive (permutation is intentionally invisible); catches address
* substitution, amount changes, and redistribution.
*/
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