All entries
Chapter VIII
Journal · 07 Nov 2026 · 12 min read

Privy vs Turnkey vs Coinbase CDP: Agent Spend Caps

Privy, Turnkey and Coinbase CDP policy engines compared for AI agent wallets: rule JSON, default-deny gaps, rolling spend caps and on-chain limits.

Write a Coinbase CDP wallet policy that caps sendEvmTransaction at $100 and nothing else, and your agent can still call signEvmHash, which signs any 32-byte hash, with no limit at all. CDP's documented rule is that an operation no policy mentions is accepted. Privy and Turnkey both do the opposite and deny anything you did not allow.

That difference, and a handful like it, decides how much an agent wallet can lose when the model goes wrong. None of the pages ranking for "privy vs turnkey" show it. The top results are Turnkey's own /vs/privy pages, Crossmint's alternatives page and cost calculators. None of them puts the three policy formats side by side or says whether the wallet can enforce a daily cap, which is the first thing an agent builder needs to know. Everything below is checked against each vendor's docs, OpenAPI spec and pricing page as of 7 November 2026.

What Is a Wallet Policy Engine for AI Agents?

An agent wallet is a server-side key that your code, or an LLM acting through your code, asks to sign things. A policy engine sits between the request and the key. It parses what is about to be signed (destination, value, calldata, typed-data fields), checks that against rules you wrote, and refuses to sign if they fail.

All three vendors run keys in AWS Nitro Enclaves. Privy splits each key 2-of-2 with Shamir secret sharing, with one share that only the enclave can decrypt. Turnkey runs its signer and its policy engine inside enclaves on QuorumOS, its own minimal Linux unikernel, with reproducible builds that operators check against the enclave attestation. CDP keeps keys in a Nitro TEE and guards wallet calls with a Wallet Secret, a P-256 key that signs a JWT sent as X-Wallet-Auth.

The enclave stops someone stealing the key. It does nothing about your own agent asking for a bad signature, and that is where the policy engine has to do the work.

Privy vs Turnkey vs Coinbase CDP: Policy Engines Compared

Coinbase CDPPrivyTurnkey
Rule formatJSON rules with typed criteriaJSON rules with field conditionsExpression language strings
Unmatched operationAccepted if no rule targets itDeniedDenied (implicit deny)
Conflict ruleMatching reject beats acceptDENY beats ALLOWEFFECT_DENY beats EFFECT_ALLOW
Policies per wallet1 project + 1 account1 per wallet, 1 override per signer5 per organisation (250 on Enterprise)
USD-denominated checknetUSDChange, per transactionNoNo
Rolling spend capNot public (rateLimiting in spec only)Yes, aggregations, 1 to 72 hoursNot released ("usage controls" in SDKs)
Who can bypassAPI key with "Manage" scope edits policyWallet owner; app secret if no owner setRoot quorum, by design
Rejection errorHTTP 403 policy_violationHTTP 400 policy_violationOUTCOME_DENY_EXPLICIT / _IMPLICIT

Two rows are worth reading twice. The "unmatched operation" row is the CDP trap from the opening. The "who can bypass" row tells you where the real root of trust sits, and in each case it is not the enclave.

How Do Coinbase CDP Wallet Policies Work?

CDP (which now calls these "Non-custodial Wallets" with API key authentication, rather than Server Wallets v2) lets you attach one project-level policy and one account-level policy. Each holds up to 100 rules. A rule names an operation, an action and a list of criteria that must all hold. This is the example from CDP's own overview:

{
  "scope": "project",
  "rules": [
    {
      "action": "accept",
      "operation": "signEvmTransaction",
      "criteria": [{ "type": "ethValue", "ethValue": "1000000000000000000", "operator": "<=" }]
    },
    {
      "action": "reject",
      "operation": "signEvmTransaction",
      "criteria": [{ "type": "evmAddress", "addresses": ["0x000000000000000000000000000000000000dEaD"], "operator": "in" }]
    }
  ]
}

There are 17 criterion types. The useful ones for agents are evmAddress (the to field, up to 300 addresses), evmData (ABI-decoded calldata, primitive types only), evmTypedDataField and netUSDChange. That last one is unique to CDP: it simulates the transaction and checks the USD value of assets leaving the wallet, for example {"type": "netUSDChange", "changeCents": 10000, "operator": "<="}. It only runs on mainnet, CDP does not document its price source, and a failed simulation fails closed with 422 transaction_simulation_failed.

Evaluation has three rules worth memorising:

1. Within a policy, a matching reject wins regardless of order. 2. If any project-level rule matches, the project decision is final, so an account-level reject cannot override a project-level accept. 3. If no rule matches, the request is rejected only when some rule targets that operation. If none does, it is accepted.

Rule three is why you must write a catch-all reject for every operation you do not use, including signEvmHash and signEvmMessage. The second gap is administrative. In the OpenAPI spec, creating, updating, deleting and pausing policies needs only an API key with the "Manage (modify policies)" scope, with no Wallet Secret. Keep that key away from anything an agent can reach.

How Do Privy Wallet Policies Work?

Privy's format is closer to a firewall rule. Each rule names an RPC method and a list of conditions over a field_source:

{
  "version": "1.0",
  "name": "Allowlist certain smart contracts",
  "chain_type": "ethereum",
  "rules": [{
    "name": "Allowlist OUSD",
    "method": "eth_sendTransaction",
    "conditions": [{
      "field_source": "ethereum_transaction",
      "field": "to",
      "operator": "eq",
      "value": "0x20c0000000000000000000006a37da5c996874be"
    }],
    "action": "ALLOW"
  }]
}

Field sources cover raw transactions, ABI-decoded calldata, EIP-712 domain and message fields, EIP-7702 authorisations, Solana program instructions (with an Anchor IDL up to 32 KB), Tron, Sui, XRPL and a system source exposing current_unix_timestamp. Values are compared raw, in wei or the token's smallest unit, and string comparisons are case-sensitive, so a checksummed address will not match a lowercase one.

Only rules for the requested method are evaluated, and a method with no rule is denied. On Solana every instruction in the transaction must resolve to ALLOW. The limits are tight: one policy per wallet and one override policy per additional signer. Privy's docs and OpenAPI also disagree in places. The spec has an ALLOW_WITH_MFA action the docs do not mention, and the docs list a neq operator the spec lacks.

The authorisation model is where Privy is strongest for agents. A wallet has owners, who can change policy, and signers, who can only sign inside it. Either can be a P-256 authorisation key or an m-of-n key quorum, and every write request carries a P-256 signature over the RFC 8785-canonicalised body in a privy-authorization-signature header. Give the agent a signer key and keep the owner key elsewhere. If you set no owner, the app secret alone can rewrite the policy.

How Does the Turnkey Policy Language Work?

Turnkey uses strings rather than JSON trees. A policy has an effect, a consensus expression (who must approve) and a condition expression (what is being signed):

{
  "policyName": "Agent may only call USDC transfer()",
  "effect": "EFFECT_ALLOW",
  "consensus": "approvers.any(user, user.id == '')",
  "condition": "eth.tx.to == '' && eth.tx.data[0..10] == '0xa9059cbb'"
}

With an uploaded ABI you can write eth.tx.contract_call_args['wad'] < 1000000000000000000. On Solana, solana.tx.spl_transfers.all(transfer, transfer.amount < ) caps every SPL transfer in a transaction. There is also a newer time field that accepts CronSpan('0 9 1-5','8h','America/New_York'), which is a neat way to stop an agent signing outside office hours.

It is the most expressive of the three, and it has the sharpest edges. && and || have equal precedence and group left to right, so add brackets. There is no short-circuiting: a clause that touches a field the request does not populate errors the whole policy, and an erroring policy is skipped. For an allow policy that fails safe. For a deny policy it means your blocklist silently stops applying to that request shape. Below Enterprise you also get only five policies per organisation, which pushes you towards one sub-organisation per agent. Sub-organisations are unlimited and each gets its own allowance.

Finally, the root quorum bypasses every policy, and Turnkey's docs say it is impossible to limit root quorum actions with any policy. Your agent's API user must never be in it.

Can You Set a Daily Spending Limit on an AI Agent Wallet?

A per-transaction cap of $100 does not stop an agent making 500 payments in an afternoon.

Privy is the only one with a released rolling cap. Its aggregations sum a field over a rolling window and a policy condition references the total with field_source: "reference" and field: "aggregation.". The constraints matter:

  • Only sum, and the window must be 3,600 to 259,200 seconds (1 to 72 hours).
  • Only eth_signTransaction and eth_signUserOperation. Not eth_sendTransaction, which most of Privy's own examples use, and not Solana.
  • Ten aggregations per app.
  • The total is updated after a request is signed, so concurrent requests can all pass before any of them is counted. Privy calls this "disaster prevention" rather than strict enforcement.
  • No USD denomination. You cap token units per contract, grouped by recipient if you like.
CDP has a rateLimiting criterion in its OpenAPI spec, with a 24-hour window, maxCount or maxValueCents, and the check windowCents + pendingCents + operationCents <= maxValueCents. It appears only in the policy simulation schema, not in the public rule schema, the SDK types or the docs. Do not build on it yet. What CDP does ship is maxCumulativeSpend in the x402 client's SpendControls, which the SDK source describes as "in-memory and process-local". Restart the process and the counter resets.

Turnkey's public swagger defines CreateVelocityControlIntent (sum or count, rolling or infinite window, grouped by organisation, user or wallet, measured in CAIP-19 token units). A go-sdk changeset dated 23 October 2026 renames these "unreleased" velocity controls to usage controls. Not generally available as of November 2026.

Signer Policies vs On-Chain Spend Permissions

The alternative is to enforce the cap in a contract rather than in the signer. Three designs are live:

Coinbase Spend PermissionsSafe Allowance ModuleERC-7715 (MetaMask)
StatusDeployed, same address on Base, Ethereum, OP, Arbitrum, Polygon and othersDeployed (v0.1.1 on 57 chains)Draft ERC; MetaMask Smart Accounts Kit
Wallet requiredCoinbase Smart Wallet or CDP smart accountSafeMetaMask smart account
Cap shapeallowance per period, fixed windows from startamount per resetTimeMin, max ~45.5 dayserc20-token-periodic, native-token-stream and others
ScopeNative and ERC-20 transfers onlyTransfers onlyPer permission type

Spend Permissions use a SpendPermission struct (account, spender, token, uint160 allowance, uint48 period, start, end, salt, extraData) checked by SpendPermissionManager at 0xf85210B21cC50302F477BA56686d2019dC9b67Ad. CDP's docs describe the windows as rolling, but getCurrentPeriod computes fixed windows anchored at start, and unused allowance does not carry over. ERC-7715's main method was renamed from wallet_grantPermissions to wallet_requestExecutionPermissions in January 2026, so older tutorials will not work.

A contract cap is exact and survives a compromised signer, because the chain does the counting. It costs gas, ties you to one smart-account family and usually restricts the agent to plain transfers. A signer policy works with any EOA and can inspect arbitrary calldata, but it judges one request at a time and only as well as its parser. CDP's evmData only decodes primitive argument types, so the bytes[] inside a router multicall is opaque to it.

Agent Wallet Pricing in 2026: Cost per Signature

Take an agent paying for APIs over x402, which means one EIP-712 signature per payment (see my EIP-3009 vs Permit2 guide), at 100,000 payments a month:

List price100k signatures/month
Coinbase CDP$0.005 per operation, 5,000 free; policy evaluation counts as 1 operation~$975 (200k operations less 5k free)
Privy50k signatures free, then $2,000 PAYG base + $0.01 per signature~$2,500
Turnkey Pay as You Go25 free, then $0.10 per signature~$9,998
Turnkey Pro$99 monthly minimum, $0.05 per signature~$5,000

The CDP figure assumes each signature triggers one billed policy evaluation, which is my reading of the pricing table rather than something CDP states for typed-data signing. All three offer enterprise rates: Privy "as low as $0.001", Turnkey "as low as $0.0015". At list price, CDP is cheapest by a wide margin, and Turnkey is priced for wallets that sign occasionally, not agents that sign constantly.

My Take: Put the Cap On-Chain and the Allowlist in the Signer

My view: no signer policy engine available today should be your only daily spend limit for an autonomous agent. Privy's aggregation is the best of the three, and it still has a post-sign race, a 72-hour ceiling and no coverage of eth_sendTransaction. CDP and Turnkey have the feature in their specs and not in their products.

The design I would ship uses two layers. Enforce the cumulative cap on-chain with Spend Permissions or a Safe allowance, because that is the only place a counter cannot be raced or reset by a process restart. Use the signer policy for what it is good at: an allowlist of contracts and recipients, a per-transaction ceiling, an explicit deny on every operation the agent does not need, and time windows. I expect CDP and Turnkey to ship their rolling caps by mid-2027, since both already have them in their API schemas. Even then I would keep the on-chain layer, because the signer still evaluates one request at a time.

On vendor choice: pick Privy if you need a working rolling cap now and can use eth_signTransaction. Pick CDP if you are on Base, sign at high volume and want USD-denominated checks. Pick Turnkey if you need multi-party approval and time-boxed signing, and can afford its per-signature pricing.

What This Means for Teams Building Agent Payments

  • On CDP, add an explicit reject for every operation you do not use, starting with signEvmHash, signEvmMessage and signEvmTypedData if the agent only sends transactions.
  • Separate the policy admin key from the agent. CDP's "Manage" API key, Privy's wallet owner key and Turnkey's root quorum can each loosen policy. None of them should be in the agent's environment.
  • Always set a Privy owner. Without one, the app secret can rewrite the policy.
  • Normalise addresses before writing Privy rules. Comparisons are case-sensitive.
  • Bracket every Turnkey expression and test deny policies against every request shape, since an erroring policy is skipped.
  • Handle the rejection codes as permanent failures: CDP returns 403 policy_violation, Privy 400 policy_violation. Privy deletes the idempotency record on a violation, so a retry with the same key is evaluated afresh rather than replayed, and an agent's naive retry loop will get through as soon as the rolling window moves on.
  • Put the daily cap in a contract if the agent can move more than you are willing to lose in an afternoon.
If you are still choosing a payment protocol to sit on top of the wallet, my AP2 vs ACP vs x402 comparison covers who holds the credential in each.

Key Takeaways

All three keep keys in Nitro enclaves, so the difference is policy semantics. CDP accepts operations no rule targets, while Privy and Turnkey deny them. Privy alone has a released rolling cap, limited to two signing methods and counted after signing. CDP and Turnkey have cumulative limits in their schemas, not their products. Each has an admin key that can bypass or rewrite policy, and that key is the real security boundary. For an autonomous agent, put the cumulative cap on-chain and use the signer for allowlists.

Written by Tom Wang, a payments engineer building agent-facing payment infrastructure in the UK.