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
5 changes: 3 additions & 2 deletions agentkit-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "bun link && concurrently \"tsc --watch\" \"tsc-alias -w\"",
"build": "tsc --project ./tsconfig.json && tsc-alias -p ./tsconfig.json",
"prepare": "bun run format && bun run clean && bun run build",
"test:smoke": "node test/platform.smoke.mjs"
"test:smoke": "node test/platform.smoke.mjs && node test/peer-cash.smoke.mjs"
},
"dependencies": {
"@langchain/core": "^0.3.40",
Expand All @@ -40,7 +40,8 @@
"viem": "2",
"zod": "^3.23.8",
"@0xgasless/smart-account-sdk": "^0.0.15",
"@0xgasless/agent": "^2.2.1"
"@0xgasless/agent": "^2.2.1",
"@zkp2p/cash": "0.4.9"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
Expand Down
64 changes: 64 additions & 0 deletions agentkit-core/src/actions/PeerCashAction/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { ZeroXgaslessSmartAccount } from "@0xgasless/smart-account-sdk";
import { capabilitiesToJson } from "@zkp2p/cash";
import { z } from "zod";
import type { AgentkitAction } from "../../agentkit";
import { formatError, formatResult, getPeerCashClient } from "./client";

const PEER_CASH_CAPABILITIES_PROMPT = `
Discovers what Peer Cash can do before you name a payout rail or currency.

Peer Cash turns Base USDC into fiat: your wallet's USDC becomes a protocol-held
order, a buyer pays the fiat to your own payment handle and proves it, and the
escrow releases the USDC at the live Chainlink oracle rate with zero spread.

USAGE
name : peer_cash_capabilities
args : none

RETURNS
- platforms: payout platform ids with the fiat currencies each can pay, the
handle format it expects, and whether it needs an identity attestation
- destination: always Base USDC (chain 8453)
- amount: the protocol minimum and the recommended minimum, in USDC base units
- pricing: the oracle model and spread in basis points

Call this first. peer_cash_estimate and peer_cash_prepare_cashout only accept a
platform and currency listed here.
`;

export const PeerCashCapabilitiesInput = z
.object({})
.strip()
.describe("No arguments; Peer Cash capabilities are static");

/**
* Reads the Peer Cash capability catalog.
*
* @param _wallet - Unused; capability discovery never touches the wallet.
* @param _args - Unused; the action takes no arguments.
* @returns The capability catalog as JSON, or a typed error message.
*/
export async function peerCashCapabilities(
_wallet: ZeroXgaslessSmartAccount,
_args: z.infer<typeof PeerCashCapabilitiesInput>,
): Promise<string> {
try {
return formatResult(capabilitiesToJson(getPeerCashClient().capabilities()));
} catch (error) {
return formatError("capabilities", error);
}
}

/**
* Peer Cash capability discovery action.
*/
export class PeerCashCapabilitiesAction
implements AgentkitAction<typeof PeerCashCapabilitiesInput>
{
public name = "peer_cash_capabilities";
public description = PEER_CASH_CAPABILITIES_PROMPT;
public argsSchema = PeerCashCapabilitiesInput;
public func = peerCashCapabilities;
public walletOptional = true;
public smartAccountRequired = false;
}
143 changes: 143 additions & 0 deletions agentkit-core/src/actions/PeerCashAction/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { type CashClient, createCashClient, usdc } from "@zkp2p/cash";

/**
* ERC-8021 analytics marker stamped on every Peer transaction these actions
* prepare, so Peer can attribute the order to Agentkit. It carries no funds
* and grants no permissions.
*/
const AGENTKIT_REFERRER = "0xgasless-agentkit";

const BASE_CHAIN_ID = 8453;
const BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const ERC20_APPROVE_SELECTOR = "0x095ea7b3";

/**
* Peer production contracts used by the four custody-separated prepare paths.
* These addresses come from the canonical Base deployment artifacts in
* zkp2p/zkp2p-contracts.
*/
const PEER_CASH_BASE_TARGETS = new Set([
"0x777777779d229cdf3110e9de47943791c26300ef", // EscrowV2
"0x888888359e981b5225ca48fbcdceff702fc3b888", // OrchestratorV2
"0xbc53641b4b2504f0061d6a9426c61b8ebe9b4ff0", // WhitelistPolicy
]);

type PeerPreparedTransaction = {
chainId: number;
to: string;
data: string;
};

/**
* Fail closed if the SDK prepares a transaction outside Peer Cash on Base.
* USDC is accepted only for an `approve(address,uint256)` whose spender is an
* allowlisted Peer Cash contract.
*
* @param txs - Transactions returned by a Peer Cash prepare method.
* @throws If a chain, target, selector, or approve spender is unexpected.
*/
export function assertPeerTargets(txs: readonly PeerPreparedTransaction[]): void {
for (const tx of txs) {
if (tx.chainId !== BASE_CHAIN_ID) {
throw new Error(`Peer Cash prepared unexpected chain ${tx.chainId}`);
}

const target = tx.to.toLowerCase();
if (PEER_CASH_BASE_TARGETS.has(target)) continue;

if (target === BASE_USDC_ADDRESS) {
const data = tx.data.toLowerCase();
if (
!/^0x[0-9a-f]+$/.test(data) ||
!data.startsWith(ERC20_APPROVE_SELECTOR) ||
data.length < 138 ||
data.slice(10, 34) !== "0".repeat(24)
) {
throw new Error("Peer Cash prepared unexpected Base USDC call");
}
const spender = `0x${data.slice(34, 74)}`;
if (PEER_CASH_BASE_TARGETS.has(spender)) continue;
throw new Error(`Peer Cash prepared USDC approval for unexpected spender ${spender}`);
}

throw new Error(`Peer Cash prepared unexpected target ${tx.to}`);
}
}

let client: CashClient | undefined;

/**
* The shared Peer Cash client.
*
* Peer Cash reads need no credential and every mutating verb is exposed only
* through its prepare path, so this client never holds, requests, or sees a
* private key. `PEER_CASH_RPC_URL` overrides the public Base RPC, which is
* rate limited; `PEER_CASH_REFERRAL_CODE` is the six-character code from the
* Peer app that credits fills on the orders this agent opens.
*
* @returns The memoized `CashClient` bound to Peer production on Base.
*/
export function getPeerCashClient(): CashClient {
if (!client) {
const rpcUrl = process.env.PEER_CASH_RPC_URL;
const referralCode = process.env.PEER_CASH_REFERRAL_CODE;
client = createCashClient({
environment: "production",
referrer: AGENTKIT_REFERRER,
...(rpcUrl ? { rpcUrl } : {}),
...(referralCode ? { referralCode } : {}),
});
}
return client;
}

/**
* Pretty-print an already-serializable Peer Cash value for an LLM.
*
* @param value - A value produced by one of the `@zkp2p/cash` JSON codecs.
* @returns Pretty-printed JSON.
*/
export function formatResult(value: unknown): string {
return JSON.stringify(value, null, 2);
}

/**
* Turn a failure into the typed, actionable message Peer Cash errors carry.
*
* Every `CashError` has a `code`, a `retryable` flag, and a `remediation`
* sentence, so the agent can decide whether to retry or change its input
* instead of guessing from a stack trace.
*
* @param verb - The Peer Cash verb that failed, for the message prefix.
* @param error - The thrown value.
* @returns A single-line error message.
*/
export function formatError(verb: string, error: unknown): string {
const source = error as {
message?: unknown;
code?: unknown;
retryable?: unknown;
remediation?: unknown;
};
const parts = [`Error: Peer Cash ${verb} failed.`];
if (typeof source?.code === "string") parts.push(`[${source.code}]`);
parts.push(typeof source?.message === "string" ? source.message : String(error));
if (typeof source?.remediation === "string") parts.push(`Remediation: ${source.remediation}`);
if (source?.retryable === true) parts.push("This error is retryable.");
return parts.join(" ");
}

/**
* Parse a decimal USDC amount into 6-decimal base units.
*
* @param amount - Decimal USDC amount, for example `"12.34"`.
* @returns The amount in USDC base units.
* @throws If the amount is not a positive decimal with at most 6 places.
*/
export function toUsdcBaseUnits(amount: string): bigint {
const value = usdc(amount.trim());
if (value <= 0n) {
throw new Error(`amount must be greater than zero; received "${amount}"`);
}
return value;
}
74 changes: 74 additions & 0 deletions agentkit-core/src/actions/PeerCashAction/estimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { ZeroXgaslessSmartAccount } from "@0xgasless/smart-account-sdk";
import { type CurrencyType, estimateToJson } from "@zkp2p/cash";
import { z } from "zod";
import type { AgentkitAction } from "../../agentkit";
import { formatError, formatResult, getPeerCashClient, toUsdcBaseUnits } from "./client";

const PEER_CASH_ESTIMATE_PROMPT = `
Estimates the fiat a Base USDC cash-out receives at the live Chainlink oracle
rate, with a recent-fill ETA for the chosen platform and currency.

USAGE
name : peer_cash_estimate
args :
• amount (string, required) - decimal USDC, e.g. "250" or "12.34"
• currency (string, required) - ISO 4217 code from peer_cash_capabilities
• platform (string, optional) - payout platform, for pair-specific fill timing

IMPORTANT
• This is not a locked quote. Peer Cash charges no spread and the binding rate
resolves when a buyer fills the order, so the fiat can move between the
estimate and the fill.
• The eta is a rolling 30-day median of how long similar orders waited for
their first fill. It is evidence, not a guarantee.

Use peer_cash_prepare_cashout next to build the transactions that open the order.
`;

export const PeerCashEstimateInput = z
.object({
amount: z.string().describe("The Base USDC amount to estimate, as a decimal string"),
currency: z.string().describe("The ISO 4217 fiat currency code to receive, e.g. USD"),
platform: z
.string()
.optional()
.nullable()
.describe("Optional payout platform id, for pair-specific fill timing"),
})
.strip()
.describe("Instructions for estimating a Peer Cash cash-out");

/**
* Estimates the fiat received for a Base USDC cash-out.
*
* @param _wallet - Unused; estimating never touches the wallet.
* @param args - The amount, currency, and optional payout platform.
* @returns The oracle estimate as JSON, or a typed error message.
*/
export async function peerCashEstimate(
_wallet: ZeroXgaslessSmartAccount,
args: z.infer<typeof PeerCashEstimateInput>,
): Promise<string> {
try {
const estimate = await getPeerCashClient().estimate({
amount: toUsdcBaseUnits(args.amount),
currency: args.currency.trim().toUpperCase() as CurrencyType,
...(args.platform ? { platform: args.platform } : {}),
});
return formatResult(estimateToJson(estimate));
} catch (error) {
return formatError("estimate", error);
}
}

/**
* Peer Cash estimate action.
*/
export class PeerCashEstimateAction implements AgentkitAction<typeof PeerCashEstimateInput> {
public name = "peer_cash_estimate";
public description = PEER_CASH_ESTIMATE_PROMPT;
public argsSchema = PeerCashEstimateInput;
public func = peerCashEstimate;
public walletOptional = true;
public smartAccountRequired = false;
}
35 changes: 35 additions & 0 deletions agentkit-core/src/actions/PeerCashAction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PeerCashCapabilitiesAction } from "./capabilities";
import { PeerCashEstimateAction } from "./estimate";
import { PeerCashOrderAction } from "./order";
import { PeerCashOrdersAction } from "./orders";
import { PeerCashPrepareAccessPolicyAction } from "./prepareAccessPolicy";
import { PeerCashPrepareCashoutAction } from "./prepareCashout";
import { PeerCashPrepareTopUpAction } from "./prepareTopUp";
import { PeerCashPrepareWithdrawAction } from "./prepareWithdraw";

export { PeerCashCapabilitiesAction } from "./capabilities";
export { PeerCashEstimateAction } from "./estimate";
export { PeerCashOrderAction } from "./order";
export { PeerCashOrdersAction } from "./orders";
export { PeerCashPrepareAccessPolicyAction } from "./prepareAccessPolicy";
export { PeerCashPrepareCashoutAction } from "./prepareCashout";
export { PeerCashPrepareTopUpAction } from "./prepareTopUp";
export { PeerCashPrepareWithdrawAction } from "./prepareWithdraw";

/**
* Peer Cash — Base USDC to fiat, non-custodial.
*
* Reads need no credential and every mutating verb returns unsigned
* transactions, so the agent submits them through send_transaction and Peer
* never sees a key.
*/
export const PEER_CASH_ACTIONS = [
new PeerCashCapabilitiesAction(),
new PeerCashEstimateAction(),
new PeerCashPrepareCashoutAction(),
new PeerCashPrepareAccessPolicyAction(),
new PeerCashOrderAction(),
new PeerCashOrdersAction(),
new PeerCashPrepareTopUpAction(),
new PeerCashPrepareWithdrawAction(),
];
Loading