{"_id":"@ar-agents/wallet-cdp","name":"@ar-agents/wallet-cdp","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@ar-agents/wallet-cdp","version":"0.2.0","description":"Society USDC wallet on Coinbase CDP (Base): per-society account provisioning, a CALLDATA-level ERC-20 spend policy (recipient allowlist + per-tx cap, not the token-contract address), and guardedTransferUsdc — the two-layer gate that requires BOTH a human ","keywords":["ar-agents","argentina","wallet","coinbase","cdp","coinbase-cdp","usdc","base","erc20","spend-policy","policy-engine","approvals","treasury","ai-sdk","agent","sociedad-automatizada"],"license":"MIT","author":{"name":"Nazareno Clemente","email":"naza@naza.ar"},"funding":{"type":"github","url":"https://github.com/sponsors/naza00000"},"homepage":"https://github.com/ar-agents/ar-agents/tree/main/packages/wallet-cdp","repository":{"type":"git","url":"git+https://github.com/ar-agents/ar-agents.git","directory":"packages/wallet-cdp"},"bugs":{"url":"https://github.com/ar-agents/ar-agents/issues"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./tools":{"import":{"types":"./dist/tools.d.ts","default":"./dist/tools.js"},"require":{"types":"./dist/tools.d.cts","default":"./dist/tools.cjs"}},"./tools.manifest.json":"./tools.manifest.json"},"dependencies":{"@coinbase/cdp-sdk":"^1.52.0","viem":"^2.47.0","@ar-agents/core":"0.4.1"},"peerDependencies":{"ai":">=6.0.0","zod":">=3.0.0"},"peerDependenciesMeta":{"ai":{"optional":true},"zod":{"optional":true}},"devDependencies":{"@types/node":"^20.19.39","ai":"^7.0.0","tsup":"^8.3.5","typescript":"^5.9.3","vitest":"^2.1.8","zod":"^4.0.0"},"publishConfig":{"access":"public","provenance":true},"engines":{"node":">=20.0.0"},"typesVersions":{"*":{"tools":["./dist/tools.d.ts"]}},"sideEffects":false,"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","typecheck":"tsc --noEmit","validate":"publint && attw --pack .","clean":"rm -rf dist"},"_id":"@ar-agents/wallet-cdp@0.2.0","_integrity":"sha512-ngw5LPapov7rN3mDEhYn6ZLs+TxM7o0i8Ky3vV2L+uGGa+kRiI3fFSG1BDGm7lCOx2jPp+hA8fiKZbcSFvWDow==","_resolved":"/tmp/568f00ca553a5c5a28e117503c7148fc/ar-agents-wallet-cdp-0.2.0.tgz","_from":"file:ar-agents-wallet-cdp-0.2.0.tgz","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-ngw5LPapov7rN3mDEhYn6ZLs+TxM7o0i8Ky3vV2L+uGGa+kRiI3fFSG1BDGm7lCOx2jPp+hA8fiKZbcSFvWDow==","shasum":"5dd285c3ef50ce4b5b37bc32332681a67c8a6c60","tarball":"https://registry.npmjs.org/@ar-agents/wallet-cdp/-/wallet-cdp-0.2.0.tgz","fileCount":19,"unpackedSize":325136,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ar-agents%2fwallet-cdp@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFj4N70v+WSeJ3kq/WPbELnQbgEjfV4A46m7uKhLwjvIAiACFNGx5oVMWpWX0VtrPQoW92pujjFn+WzbfKdQJV5wPg=="}]},"_npmUser":{"name":"naza-ar","email":"clementenaza@gmail.com"},"directories":{},"maintainers":[{"name":"naza-ar","email":"clementenaza@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/wallet-cdp_0.2.0_1783947528743_0.43734736120600193"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-13T12:58:48.573Z","0.2.0":"2026-07-13T12:58:48.889Z","modified":"2026-07-13T12:58:49.327Z"},"maintainers":[{"name":"naza-ar","email":"clementenaza@gmail.com"}],"description":"Society USDC wallet on Coinbase CDP (Base): per-society account provisioning, a CALLDATA-level ERC-20 spend policy (recipient allowlist + per-tx cap, not the token-contract address), and guardedTransferUsdc — the two-layer gate that requires BOTH a human ","homepage":"https://github.com/ar-agents/ar-agents/tree/main/packages/wallet-cdp","keywords":["ar-agents","argentina","wallet","coinbase","cdp","coinbase-cdp","usdc","base","erc20","spend-policy","policy-engine","approvals","treasury","ai-sdk","agent","sociedad-automatizada"],"repository":{"type":"git","url":"git+https://github.com/ar-agents/ar-agents.git","directory":"packages/wallet-cdp"},"author":{"name":"Nazareno Clemente","email":"naza@naza.ar"},"bugs":{"url":"https://github.com/ar-agents/ar-agents/issues"},"license":"MIT","readme":"# @ar-agents/wallet-cdp\n\nA Sociedad Automatizada's USDC wallet on **Coinbase CDP** (Base), with a two-layer spend gate. ROADMAP.md M2-4a chose CDP over Circle after running both live on Base Sepolia (Circle's API-key-authenticated path has no provider-side policy at all; CDP's does). This package (M2-4b) wires that provider policy AND the existing ar-agents human-approval gate onto the same transfer, so a real spend needs both to clear.\n\n## The gap this closes\n\nFor a native ETH transfer, the transaction's `to` field IS the recipient. For an ERC-20 USDC transfer, `to` is the **USDC contract**; the real recipient sits inside the `transfer(to, amount)` calldata. A plain address allowlist on `to` cannot tell a good USDC recipient from a bad one, since both go to the same contract. This package builds CDP's `evmData` policy criterion instead, which decodes the calldata and constrains the DECODED recipient and amount, closing the gap.\n\n## What it does\n\n- **`createSocietyWallet`**, provision (or reuse) one CDP account per society, name derived deterministically from the society id.\n- **`buildErc20SpendPolicyRules` / `applySpendPolicy`**, the CALLDATA-level policy: an `evmAddress` rule pinning the contract to USDC, an `evmData` rule decoding `transfer(to, value)` to enforce a recipient allowlist (optional) and a per-tx cap, plus a default reject rule for native ETH. Attached server-side on CDP; enforced by CDP itself before signing, independent of anything this package's caller does.\n- **`transferUsdc`**, execute a transfer; provider failures surface as one of two typed errors: `WalletCdpPolicyDeniedError` (`code: \"policy_denied\"`, not retryable, the policy engine said no) or `WalletCdpUpstreamError` (`code: \"upstream_error\"`, retryable, anything else).\n- **`guardedTransferUsdc`**, the two-layer gate M2-4b asks for: above a configurable threshold, an ar-agents approvals-gate decision is required BEFORE the provider is ever called (below threshold, the gate is skipped and CDP's own policy is the only check); either layer can block the transfer independently of the other.\n- **`encodeErc20TransferCalldata` / `decodeErc20TransferCalldata`**, the exact bytes (`viem`-backed): 4-byte selector, recipient right-aligned in a 32-byte slot, amount right-aligned in a 32-byte slot. Ground truth for what the `evmData` policy criterion is actually deciding on.\n- **`getUsdcBalanceAtomic`** (M2-4d), read the wallet's current USDC balance. Parses CDP's `listTokenBalances()` response defensively -- the amount comes back as a nested `{amount:{amount,decimals}}` object on some responses, a bare value on others; unwrap before `BigInt`, same landmine the M2-4a spike found.\n- **`checkBalanceAndDetectTopUp`** (M2-4d), the v0 owner top-up flow's detection half: compares the current balance against the last one seen (via an injectable `LastBalanceStore`) and reports an increase/decrease/none delta. Deliberately simple: no chain-scanning, no per-transaction attribution -- an AGGREGATED delta between two checks. See \"Fondear la wallet (v0)\" below.\n- **`@ar-agents/wallet-cdp/tools`**, `walletCdpTools()`: two Vercel AI SDK 6 tools. `wallet_transfer_usdc`'s name matches `@ar-agents/core`'s risk-manifest \"transfer\" override, so a host that wires it through `enforceRiskPolicy` (the way `apps/sociedad-ia-starter` wires every package) gets the categorical art. 102 gate for free, in addition to this package's own amount-based threshold. `wallet_check_balance` is read-only and never gated (classifies \"read\").\n\n## Entry points\n\n- `@ar-agents/wallet-cdp`, the wallet/policy/guard core + `createCdpClient`. No `ai`/`zod` deps.\n- `@ar-agents/wallet-cdp/tools`, the AI SDK tool wrapper (needs the `ai` + `zod` peers).\n\n## The two layers, precisely\n\n```\nguardedTransferUsdc(to, amountAtomic, ...)\n  1. classify \"wallet_transfer_usdc\" via the risk manifest -> \"money\"\n  2. if amountAtomic >= thresholdAtomic:\n       approved = await approve(\"wallet_transfer_usdc\", {to, amountAtomic, idempotencyKey})\n       if !approved -> return {status:\"deferred\"}   <- provider NEVER called\n  3. transferUsdc(account, {to, amountAtomic, idempotencyKey})\n       -> CDP's own policy (attached via applySpendPolicy) evaluates the\n          decoded calldata server-side, before signing\n       -> throws WalletCdpPolicyDeniedError if IT says no, even though\n          step 2 already approved\n```\n\n`approve` is the exact same `(toolName, args) => Promise<boolean>` callback `@ar-agents/core`'s `withApproval` takes, so a host wires it to the SAME async consume-or-queue rail already live at `apps/sociedad-ia-starter/src/lib/governance.ts` -> `POST /api/approvals/gate`. No separate `approvalId` hand-off is introduced: the queue already dedupes on `(society, tool, argsHash)`.\n\n## Configuration\n\nReal usage needs `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `CDP_WALLET_SECRET` (from the CDP Portal, https://portal.cdp.coinbase.com, see `createCdpClient`'s doc comment). Never logged; `createCdpClient` throws a typed, value-free `ArAgentsUnconfiguredError` naming only which keys are missing.\n\n## Fondear la wallet (v0)\n\nROADMAP.md M2-4d: no automated ARS-in top-up route exists yet (that is M2-4f, blocked on a legal review). Until then, the owner funds a society's wallet by sending USDC directly, on-chain, on Base. This is a manual procedure -- write it down, follow it exactly.\n\n### 1. Conseguí la dirección de la wallet\n\nCada sociedad tiene UNA wallet CDP, provista una sola vez (`createSocietyWallet`, ver arriba). Para saber la dirección:\n\n- **`GET /api/status`** del deploy de la sociedad (el mismo endpoint que usa el cockpit de studio, `Authorization: Bearer <STUDIO_STATUS_TOKEN>`): el campo `treasury.address` es la dirección pública de la wallet. `treasury.available: false` significa que la sociedad todavía no tiene una wallet CDP configurada (faltan `SOCIETY_ID` + `CDP_API_KEY_ID`/`CDP_API_KEY_SECRET`/`CDP_WALLET_SECRET`) -- resolvé eso primero.\n- Alternativa: pedile al agente que corra `wallet_check_balance` (tool de solo lectura, siempre segura); la respuesta incluye `address`.\n\n### 2. Mandá USDC, en la red correcta, al contrato correcto\n\n- **Red**: la que indique `treasury.network` (o la variable de entorno `CDP_NETWORK` del deploy). Por defecto `base-sepolia` (testnet); en producción real es `base` (mainnet).\n- **Token**: USDC nativo (no un USDC \"bridgeado\" de otra chain -- llega a una dirección distinta y NO se computa).\n  - Base mainnet: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`\n  - Base Sepolia (testnet, faucet en https://portal.cdp.coinbase.com/faucet): `0x036CbD53842c5426634e7929541eC2318f3dCF7e`\n- Mandá la transferencia ERC-20 `transfer(to, amount)` a esa dirección de wallet, desde donde tengas el USDC (un exchange con retiro a Base, otra wallet propia, etc).\n\n### 3. Confirmá que llegó\n\n- **`GET /api/status`** de nuevo: `treasury.balanceAtomic` (unidades atómicas, 6 decimales) y `treasury.usd` (el mismo número decimal que corresponde a `@ar-agents/treasury`'s `TreasuryState.usd` para esa sociedad) deberían reflejar el nuevo balance en cuanto la transacción confirme on-chain (segundos en Base).\n- **`wallet_check_balance`**: si se corrió una vez ANTES del envío (para fijar la base de comparación) y se vuelve a correr DESPUÉS, la respuesta trae `depositDetected: true` y el log de auditoría de la sociedad registra una entrada `\"USDC N recibido en la wallet (...) ejecutada\"` -- ver el schema `MoneyAuditEvent` (`kind: \"deposit\"`) en `@ar-agents/treasury`.\n\n### Limitación honesta (v0)\n\nNo hay indexer ni escaneo de la chain. `wallet_check_balance` compara el balance actual contra el ÚLTIMO chequeo guardado -- si se mandan dos transferencias entre dos chequeos, se ve como UN solo incremento agregado, no como dos depósitos separados. Tampoco hay atribución por transacción (no se sabe de qué dirección vino un depósito puntual sin mirar el explorer). El PRIMER chequeo que se corre nunca cuenta como \"depósito detectado\": es la base de referencia (el fondeo inicial de la wallet), no un top-up observado. Para atribución real, transacción por transacción, hace falta un indexer que escuche eventos `Transfer` del contrato USDC -- fuera de alcance para v0.\n\n## Status\n\n`0.1.0`, first ship, 40 tests (unit, mocked CDP client) plus one FULL LIVE proof on Base Sepolia (2026-07-12, `scripts/wallet-cdp-live-check.mjs`, real account + real `createPolicy`/`updateAccount` + real funded transfers):\n\n- The `evmData` recipient/amount rule shape was reconstructed from CDP's documentation first, then corrected against the **installed SDK's own client-side zod schema** (`@coinbase/cdp-sdk/src/policies/evmSchema.ts`) after a first live attempt came back with a `ZodError` naming the exact expected shape (`values`, plural, for the `\"in\"` recipient condition, not `value`). See `src/policy.ts`'s header for the full paper trail.\n- With the corrected shape, `applySpendPolicy` attached the policy server-side successfully.\n- A transfer ABOVE the cap was rejected by CDP itself: `WalletCdpPolicyDeniedError: ... The request is forbidden due to violating at least one configured policy.`\n- A transfer AT the cap, to the same allowlisted recipient, executed on-chain (tx `0x9f95e747516c72be2e759279e92a6cbba82e0fa317832eef6c754237ac15fd7f`, Base Sepolia).\n\nThis is the strongest form of proof available: not \"the SDK accepted the shape\" but \"CDP's server enforced the decoded-calldata amount bound against a real transaction.\" See `docs/research/spikes/wallet-provider/COMPARISON.md` for the M2-4a finding this fixes.\n\n`wallet_transfer_usdc` is wired into `apps/sociedad-ia-starter`'s agent loop with a durable per-society KV idempotency store (ROADMAP.md M2-4c). `wallet_check_balance` + the v0 owner top-up flow (M2-4d, this section) are wired in too: the starter's `GET /api/status` surfaces `treasury.address`/`treasury.balanceAtomic`/`treasury.usd`, and the balance tool's last-seen reading is persisted in the starter's own KV-backed `LastBalanceStore` (`apps/sociedad-ia-starter/src/lib/wallet-balance-store.ts`) so top-up detection survives across serverless invocations. 67 tests in this package (unit, mocked CDP client) plus the transfer-side live proof above.\n\n`getUsdcBalanceAtomic` was ALSO run live (2026-07-13, against the same `wallet-cdp-live-check` society/wallet the M2-4b proof funded on Base Sepolia): it called the real `account.listTokenBalances({network:\"base-sepolia\"})` and correctly parsed the real response into `1000000` atomic units (1.0 USDC) -- confirming the defensive nested/bare-amount parsing in `./balance.ts` matches the REAL SDK response shape, not just the shapes the M2-4a spike guessed at.\n","readmeFilename":"README.md","_rev":"1-f683290822271223a2987efefeaea61a"}