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 CDP | Privy | Turnkey | |
|---|---|---|---|
| Rule format | JSON rules with typed criteria | JSON rules with field conditions | Expression language strings |
| Unmatched operation | Accepted if no rule targets it | Denied | Denied (implicit deny) |
| Conflict rule | Matching reject beats accept | DENY beats ALLOW | EFFECT_DENY beats EFFECT_ALLOW |
| Policies per wallet | 1 project + 1 account | 1 per wallet, 1 override per signer | 5 per organisation (250 on Enterprise) |
| USD-denominated check | netUSDChange, per transaction | No | No |
| Rolling spend cap | Not public (rateLimiting in spec only) | Yes, aggregations, 1 to 72 hours | Not released ("usage controls" in SDKs) |
| Who can bypass | API key with "Manage" scope edits policy | Wallet owner; app secret if no owner set | Root quorum, by design |
| Rejection error | HTTP 403 policy_violation | HTTP 400 policy_violation | OUTCOME_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_signTransactionandeth_signUserOperation. Noteth_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.
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 Permissions | Safe Allowance Module | ERC-7715 (MetaMask) | |
|---|---|---|---|
| Status | Deployed, same address on Base, Ethereum, OP, Arbitrum, Polygon and others | Deployed (v0.1.1 on 57 chains) | Draft ERC; MetaMask Smart Accounts Kit |
| Wallet required | Coinbase Smart Wallet or CDP smart account | Safe | MetaMask smart account |
| Cap shape | allowance per period, fixed windows from start | amount per resetTimeMin, max ~45.5 days | erc20-token-periodic, native-token-stream and others |
| Scope | Native and ERC-20 transfers only | Transfers only | Per 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 price | 100k signatures/month | |
|---|---|---|
| Coinbase CDP | $0.005 per operation, 5,000 free; policy evaluation counts as 1 operation | ~$975 (200k operations less 5k free) |
| Privy | 50k signatures free, then $2,000 PAYG base + $0.01 per signature | ~$2,500 |
| Turnkey Pay as You Go | 25 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
rejectfor every operation you do not use, starting withsignEvmHash,signEvmMessageandsignEvmTypedDataif 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 400policy_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.
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.