Skip to content

feat: Peer Cash actions for non-custodial USDC to fiat on Base - #33

Open
ADWilkinson wants to merge 2 commits into
0xgasless:mainfrom
ADWilkinson:peer-cash-actions
Open

feat: Peer Cash actions for non-custodial USDC to fiat on Base#33
ADWilkinson wants to merge 2 commits into
0xgasless:mainfrom
ADWilkinson:peer-cash-actions

Conversation

@ADWilkinson

Copy link
Copy Markdown

Implements #32. Adds eight Peer Cash actions so an agent can turn its Base USDC into fiat.

Peer Cash is an offramp protocol on Base. The agent's wallet is the maker: its USDC becomes a protocol-held order, a buyer pays fiat to the agent's own Venmo, Revolut, Wise, Zelle, PayPal, Cash App, Chime, or Monzo handle and proves the payment with TEE-TLS, and the escrow releases the USDC at the live Chainlink oracle rate with zero spread.

#32 asks whether you want prepare-only or an additional signing verb. This PR is the prepare-only shape. If you would rather have a one-shot peer_cash_cashout that submits through wallet.sendTransaction, say so on the issue and I will add it here.

Custody

Every mutating verb returns UNSIGNED { to, data, value, chainId } for send_transaction to submit. No action in this PR accepts, reads, derives, or stores a private key, and @zkp2p/cash exposes no signing path in the surface these actions use. The order belongs to whichever address submits the transactions.

Files

agentkit-core/src/actions/PeerCashAction/

File Action What it does
client.ts shared client, typed error formatting, USDC amount parsing
capabilities.ts peer_cash_capabilities platforms, currencies, payee formats, amount bounds
estimate.ts peer_cash_estimate fiat at the live oracle rate, plus a 30-day median fill ETA
prepareCashout.ts peer_cash_prepare_cashout unsigned [approve, createDeposit]
prepareAccessPolicy.ts peer_cash_prepare_access_policy the follow-up Venmo, Cash App, and PayPal orders require
order.ts peer_cash_order one order's state, fills, and next actions
orders.ts peer_cash_orders orders for an address, defaulting to the agent's own
prepareTopUp.ts peer_cash_prepare_topup unsigned [approve, addFunds]
prepareWithdraw.ts peer_cash_prepare_withdraw unsigned transactions that unwind an order
index.ts barrel and PEER_CASH_ACTIONS

All eight are walletOptional: true and smartAccountRequired: false, so they run in platform mode, self-custody mode, and with no wallet configured. peer_cash_orders is the only one that reads the wallet, and only to default the address.

Also changed:

  • agentkit-core/src/actions/index.ts: registers PEER_CASH_ACTIONS.
  • agentkit-core/package.json: adds @zkp2p/cash, and runs the new smoke test from test:smoke.
  • agentkit-core/tsconfig.json: "target": "es2022". The newer ox that viem resolves to ships .ts sources needing String.replaceAll and the Error cause option, so bun run check fails at es2020. Scoped to this file rather than tsconfig.base.json.
  • agentkit-core/test/peer-cash.smoke.mjs: new, in the style of platform.smoke.mjs.
  • bun.lock.

Test plan

  • bun run check: exit 0
  • bun run build: exit 0
  • biome lint and biome check on the new directory: clean
  • bun run lint: same 30 pre-existing errors as main, none in PeerCashAction
  • bun run test:smoke: both PLATFORM SMOKE and PEER CASH SMOKE pass
  • PEER_CASH_SMOKE_LIVE=1 node test/peer-cash.smoke.mjs: passes against Peer production

The new smoke test is offline by default, covering registration, wallet-optional dispatch, argument schemas, and the typed error surface. PEER_CASH_SMOKE_LIVE=1 adds production reads and one unsigned cash-out plan; PEER_CASH_SMOKE_ORDER_ID adds order and list reads. It never signs or broadcasts.

Live results from getAllAgentkitActions() with no wallet configured: discovery returned 8 payout platforms across 13 currencies on chain 8453 at 0 bps spread; a 250 USDC estimate returned 215.67 EUR at 0.8627 with a 40 minute ETA; peer_cash_prepare_cashout returned [approve, createDeposit] both with chainId: 8453; peer_cash_prepare_topup returned [approve, addFunds]; peer_cash_prepare_withdraw returned a single withdrawDeposit; peer_cash_prepare_access_policy returned one transaction; a nonexistent order id returned Error: Peer Cash order failed. [ORDER_NOT_FOUND] ... This error is retryable.

Notes

  • Peer Cash settles on Base (8453) only, which is already in supportedChains with USDC in tokenMappings. Prepared transactions carry chainId: 8453, so submitting them needs an agent configured on Base. The read actions work on any chain.
  • The client is constructed with referrer: "0xgasless-agentkit", an ERC-8021 analytics marker appended to the calldata suffix beside the peer-cash marker the SDK adds regardless. It carries no funds and grants no permissions. Happy to drop it.
  • PEER_CASH_RPC_URL overrides the public Base RPC, which is rate limited. PEER_CASH_REFERRAL_CODE is the operator's own six-character Peer code. Both are optional and read the same way sxt.ts reads SXT_API_KEY.

@Adi-21

Adi-21 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Thanks, reviewed the code (incl. client.ts and prepareCashout.ts), and the prepare-only shape is exactly right: no keys touched, custody preserved, clean pass-through. Can't merge as-is, but it's close. Two changes needed:

  1. Pin the SDK — "@zkp2p/cash": "^0.4.9" → exact "0.4.9" (it builds signable calldata, so no silent version drift).
  2. Add a target allowlist — since the host signs whatever the SDK returns, please add an assertPeerTargets(txs) check in client.ts, called from all four prepare* actions before they return. It should reject any prepared tx that isn't on Base and targeting a known Peer Cash contract (or a USDC approve whose spender is one). I've got a sketch ready to share the one blocker is sourcing Peer's canonical Base contract addresses (they're not in the docs). Can you point me to the authoritative deployment list?

Two non-blocking decisions to note on the thread: whether this ships in agentkit-core or as its own package (leaning separate, given 1.0 dep pruning), and confirming the 0.1.0 → 1.0.0 bump + lockfile changes belong in this PR.

Do the two changes above and it's good to merge.

@ADWilkinson

Copy link
Copy Markdown
Author

Implemented both blockers in 8ce25b2:

  1. Pinned @zkp2p/cash to exact 0.4.9 in the manifest and lockfile.
  2. Added assertPeerTargets(txs) and call it from all four prepare actions before returning. It fails closed unless every transaction is on Base (8453) and targets Peer Cash EscrowV2, OrchestratorV2, or WhitelistPolicy; Base USDC is accepted only for approve(address,uint256) with an allowlisted spender. The check also rejects malformed/truncated approve calldata and non-zero address padding.

Canonical address source: the checked-in Base deployment artifacts in https://github.com/zkp2p/zkp2p-contracts/tree/main/deployments/base (EscrowV2.json, OrchestratorV2.json, WhitelistPolicy.json). Base USDC is 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.

Verification on the pushed head:

  • bun run --cwd agentkit-core check — passed
  • bun run --cwd agentkit-core build — passed
  • bun run --cwd agentkit-core test:smoke — both platform and Peer Cash offline smoke suites passed, including allowlist rejection cases
  • independent pre-push review — passed with no blocking findings

On the two non-blocking points: I kept this in agentkit-core to avoid broadening the current contribution; moving it to a dedicated package can be a maintainer-directed follow-up. agentkit-core/package.json is already 1.0.0 on upstream main; the lockfile had stale 0.1.0/removed dependency entries, so its 1.0.0 and dependency-pruning changes are lockfile synchronization from bun install, alongside the new pinned SDK graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants