Appearance
The no-license custody model
pocket agent gives every AI agent a real card, a real wallet, and real hands. The uncomfortable question that follows is: who actually controls the money? If the answer is "our servers can move your funds whenever they decide to," then we are a money transmitter, and that pulls in a heavy stack of licensing, bonding, and regulatory obligations before we can touch a single real dollar.
So we are building toward a different answer, enforced in code rather than promised in a policy:
Pocket Agent must have no technical ability to unilaterally move a user's assets.
If the company literally cannot sign a transaction that spends your crypto, then the custody — and the licensing burden that comes with it — never lands on us. This page explains the model, the three tracks of work that get us there, and the automated guard that keeps the surface shrinking instead of growing.

This is an engineering model, not legal advice
"No-license" describes a technical design goal. Whether it satisfies a given jurisdiction's money-transmission rules is a legal determination made with counsel — one of the two external gates (alongside production Reap access) still open before real-money launch. The code is built so that determination is possible, not to pre-empt it.
The idea in one picture
Custody is really a question about signing keys. Whoever holds the key that signs an on-chain transaction controls the asset. Today, in the legacy mode, our backend holds those keys for both users and agents (custodial). The non-custodial model moves every user key to the user and leaves the company holding exactly one key — for its own corporate treasury.
In the target state the backend is demoted from signer to librarian: it can read balances and prepare unsigned transactions, but the signature can only come from a key the user controls.
The custody guard: a ratchet, not a hope
Good intentions decay. A well-meaning engineer adding a feature six months from now could easily reintroduce a server-side signing path without realizing it reopens the custody question. So the plan is enforced by a CI check that fails the build if the signing surface grows.
bash
npm run check:custody # also runs inside `npm test`scripts/check-custody.ts scans every .ts file under src/ for the raw primitives that can produce a valid on-chain transaction from a backend-controlled wallet:
| Signing primitive | What it does |
|---|---|
ethereum.sendTransaction | Signs + broadcasts an EVM transaction |
solana.signAndSendTransaction | Signs + broadcasts a Solana transaction |
signAndSendTransaction( | Any Solana sign-and-send call |
createViemAccount( | Builds the owner account for a 4337 smart account (owner = server EOA) |
Any match outside a hand-maintained allowlist is a new custody path and fails the check. The allowlist is designed to only ever shrink — every file you delete from it is a step toward the goal.
The current baseline (the target we have not yet met)
| Metric | Baseline today | Target |
|---|---|---|
| Files with server-side signing capability | 6 (5 custodial + 1 treasury) | 1 (isolated company treasury only) |
| Agent money-tools exposed to the LLM | 5 (wallet_withdraw, defi_swap, defi_bridge, defi_earn, defi_smart_account) | 0 (scoped card credentials only) |
The guard also carries a second ratchet: a baseline of every agent tool name. If a new tool appears in agent/tools.ts that isn't in the baseline, the check fires so a reviewer must ask "does this move money? does it need a scoped credential or user approval?" before it's allowed in. The guiding assumption throughout: the LLM is not a security boundary. A prompt-injected model will try to call every tool it has; safety has to live in what the tools are allowed to do, not in the model's good judgment.
Three tracks to zero
Getting from the baseline to the target is split into three independent tracks, each behind its own feature flag so it can be developed, tested, and rolled out without destabilizing the live custodial path.
| Track | Scope | Flag | Status |
|---|---|---|---|
| A | User wallets become user-owned, client-signed | EMBEDDED_WALLETS | Scaffolding in place; not wired into money paths |
| B | Card balance custody moves to Reap, signer = user's wallet | REAP_USER_FUNDED | Partially built; gated on Reap provisioning the project |
| C | Agent gets payment capability, not custody | AGENT_CARD_ONLY, PRIVY_AUTHORIZATION_KEY | Scaffolding + card-only flag defined; on-chain session key not yet wired |
All three default off. In the shipped build today, pocket agent runs the legacy custodial path end to end — the non-custodial machinery is present, reviewed, and inert until each flag is deliberately turned on.
Track A — User wallets: server-signed → embedded
Flag: EMBEDDED_WALLETS (default off).
In legacy mode a user's wallet is a Privy server wallet: the key lives server-side and our backend signs on the user's behalf. Convenient, fully custodial. Track A replaces that with a user-owned embedded wallet — the key is created and held on the user's device, and only the user can sign.
The seam is defined in src/wallet/boundary.ts, which encodes the hard rule as a set of interfaces:
WalletReader— read balances. Safe for the backend; no signing capability.TransactionBuilder— assemble anUnsignedTx(calldata, gas, routes). Builds, never signs.UserAuthorizationBoundary— hand theUnsignedTxto the client, let the user sign with their own key, and record only the resulting hash. There is deliberately no method here that signs.
The backend's role shrinks to read and build; the signature is structurally out of reach.
A tripwire backs the rule during migration. assertNotEmbeddedUserSigning() is called at the top of the legacy server-signing path (sendWithdrawal, for any owner that isn't the treasury). If embedded mode is on and a user-wallet server-sign is somehow still attempted, it throws loudly instead of quietly signing custodially:
text
[custody] <context>: server-side signing of a user wallet is disabled in embedded mode —
build an UnsignedTx and route it through UserAuthorizationBoundary.Funds migration: already handled
An earlier version of the plan (step A4) required sweeping balances out of the old server wallets before flipping users to embedded. That is marked no longer required — the operator withdrew all balances and the old server wallets are abandoned, so there's nothing left to migrate.
Status: the interfaces, the guard, and the tripwire exist and are reviewed, but they are not yet wired into the live money endpoints. Today every money path still runs the custodial server-signing route.
Track B — Card custody: Program-Funded → Reap User-Funded
Flag: REAP_USER_FUNDED (default off; effective only with a real Reap key — exposed as the derived reapUserFunded).
This track is about who holds the collateral behind the card. The two models differ in a fundamental way, and the choice is locked at Reap project-creation time.
Program-Funded (legacy, off) | Reap User-Funded (on) | |
|---|---|---|
| Who holds the balance | Our treasury holds pooled collateral | Reap holds each user's balance at per-chain deposit addresses |
| Account signer | None (we open an account with no signers) | The user's own embedded wallet |
| Who authorizes a swipe | We do — against our own ledger (External auth) | Reap does — internally (Managed auth) |
| External-auth webhook | Required (we approve/decline live) | Not needed |
| Balance of record | Our ledger_agents table | Reap (we syncReapBalance) |
| Custody on us? | Yes — we custody the collateral | No — no master collateral, user controls the account |
Under User-Funded, the Reap account is created with the user as signer: the user signs Reap's authorization message with their own wallet, so they — not us — control the account. Deposits need no signature (money just arrives at the deposit address); withdrawals and off-ramp require the user's signature. We never hold a key to that account.
The provisioning code (openUserFundedAccount, the /reap/account signer handshake, reapRedeemInit, syncReapBalance) all exist and switch on reapUserFunded. Cash-out under this model is a two-step Reap crypto-withdrawal the user signs — never a treasury payout, never a ledger debit on us (Reap holds and debits). The destination is pinned to the Privy-token-verified embedded wallet, never a mutable stored address, so a tampered record can't redirect funds.
Status: partially built and flag-gated. It activates only once Reap has created the User-Funded + Managed-Auth project and the account/deposit/balance shapes are confirmed against the sandbox — one of the external gates to launch. See the Reap integration page for the authorization and settlement details.
Track C — Agent authority: custody → session key
Flags: AGENT_CARD_ONLY, PRIVY_AUTHORIZATION_KEY (both default/empty).
The sharpest custody question is about the agent, because the agent acts autonomously. Today each agent is handed its own Privy server wallet with the full set of on-chain money-tools — the same red custody as a user server wallet, except now a language model is in the loop. The target flips this entirely: the agent gets a payment capability, not custody.
Track C splits the agent's spending into two rails, neither of which involves the agent holding or the server unilaterally controlling user funds.
The card-only flag
AGENT_CARD_ONLY is the first, bluntest piece of Track C. When on, the agent is card-only: its money-tools are limited to the scoped card (card_pay, commerce tools), and the on-chain money-tools (wallet_withdraw, defi_swap, defi_bridge, defi_earn, defi_smart_account) are not registered at all — so a prompt-injected agent has nothing to call to move on-chain funds. This moves the custody-guard count straight toward its target of zero agent on-chain money-tools.
Defined, not yet wired
The AGENT_CARD_ONLY flag and its intent are defined in config.ts, but it is not yet wired into tool registration — the agent still receives the full tool set today. Treat the card-only agent rail as planned behavior that the flag will gate once wired.
The session-key rail: mandate policies
The on-chain rail is the real innovation, and it is scaffolding only so far. The idea: the agent's wallet is user-owned (embedded, from Track A). The user delegates it to our server with client consent. We then sign agent actions server-side with an authorization key — but strictly inside an enclave-enforced policy the user granted. We can act autonomously for the agent, yet cannot exceed the mandate and cannot act without the delegation. That is a bounded, revocable session key — not custody.
Two files hold the scaffolding:
src/wallet/policies.ts—agentMandatePolicy()builds a Privy default-deny policy. A method with noALLOWrule is denied, so the wallet can do only what the mandate permits:eth_sendTransactionallowed only to allowlisted targets, with native value ≤ cap, on allowlisted chains;DENYrules (which win overALLOW) cap ERC-20transfer/approveamounts by decoding calldata against the token ABI;exportPrivateKeyexplicitly denied, belt-and-suspenders over the default-deny.
src/wallet/delegated.ts—agentSendEvm()signs a transaction from the user's delegated wallet via@privy-io/node, with the authorization key (PRIVY_AUTHORIZATION_KEY) proving the request. Privy's secure enclave evaluates the policy before signing; a transaction outside the mandate is rejected in the enclave, not in our code.applyAgentMandate()attaches the policy to the wallet.delegatedSignerReadyis true only whenEMBEDDED_WALLETSis on and an authorization key is configured.
Status: the policy builder, the delegated signer, and the card-only flag all exist, but Track C is not yet activated — agents today still get their own server wallet with the full tool set. This is the track with the most remaining work.
How this stacks with the approval gate
Non-custody is the structural floor; it is not the only line of defense. Even in the legacy custodial build, no money moves on the model's say-so:

wallet_withdrawand everydefi_*tool never execute when the model calls them — they create a pending approval (exact amount + destination) that the user taps to approve. Execution runs through the single guardedexecuteWithdraw/executeApprovedDefipath.- Untrusted tool output (web pages, email, browsing) is fenced in
<untrusted-content>tags so injected instructions can't masquerade as the user. - Autonomous runs withhold irreversible tools at execution time (enforced in the loop, not merely hidden from the prompt) and restrict email to an owner allowlist.
- On-chain sends are additionally screened against a sanctions denylist (
src/provider/compliance.ts) at the destination, with an optional external screening provider;COMPLIANCE_FAIL_CLOSEDcontrols whether an un-screenable destination is blocked.
The non-custodial tracks make the worst case smaller: today a compromised server could, in principle, sign. In the target state it structurally cannot — the approval gate and the custody floor reinforce each other.
Flag reference
| Flag | Default | Effect |
|---|---|---|
EMBEDDED_WALLETS | false | On: user-owned embedded wallets, backend reads + builds unsigned tx, user signs client-side. Off: legacy Privy server wallets, server-side signing. |
REAP_USER_FUNDED | false | On (with real Reap key): Reap holds per-user balance, signer = user's wallet, Reap authorizes + settles. Off: our treasury holds collateral, we authorize each swipe. |
AGENT_CARD_ONLY | false | On (once wired): agent's on-chain money-tools unregistered; card + commerce only. Off: legacy full tool set. |
PRIVY_AUTHORIZATION_KEY | empty | P-256 delegated-signer key enabling the enclave-enforced agent session key (Track C). delegatedSignerReady requires this and EMBEDDED_WALLETS. |
Only check:custody turns these aspirations into something enforceable: as long as the number next to "Files with signing capability" is above 1 and "Agent money-tools" is above 0, the no-license thesis is not yet met — and the guard makes sure the number can only go down.
Related reading
- Reap integration — the card-issuing and settlement rails that Track B rides on.
- Security & control — the Tap-to-Approve boundary from the user's point of view.
- Wallet — how balances, funding, and cash-out behave in the app today.