Skip to content

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.

The in-app wallet, where a user's balances and digital assets live

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 primitiveWhat it does
ethereum.sendTransactionSigns + broadcasts an EVM transaction
solana.signAndSendTransactionSigns + 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) ​

MetricBaseline todayTarget
Files with server-side signing capability6 (5 custodial + 1 treasury)1 (isolated company treasury only)
Agent money-tools exposed to the LLM5 (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.

TrackScopeFlagStatus
AUser wallets become user-owned, client-signedEMBEDDED_WALLETSScaffolding in place; not wired into money paths
BCard balance custody moves to Reap, signer = user's walletREAP_USER_FUNDEDPartially built; gated on Reap provisioning the project
CAgent gets payment capability, not custodyAGENT_CARD_ONLY, PRIVY_AUTHORIZATION_KEYScaffolding + 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 an UnsignedTx (calldata, gas, routes). Builds, never signs.
  • UserAuthorizationBoundary — hand the UnsignedTx to 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 balanceOur treasury holds pooled collateralReap holds each user's balance at per-chain deposit addresses
Account signerNone (we open an account with no signers)The user's own embedded wallet
Who authorizes a swipeWe do — against our own ledger (External auth)Reap does — internally (Managed auth)
External-auth webhookRequired (we approve/decline live)Not needed
Balance of recordOur ledger_agents tableReap (we syncReapBalance)
Custody on us?Yes — we custody the collateralNo — 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 no ALLOW rule is denied, so the wallet can do only what the mandate permits:
    • eth_sendTransaction allowed only to allowlisted targets, with native value ≤ cap, on allowlisted chains;
    • DENY rules (which win over ALLOW) cap ERC-20 transfer/approve amounts by decoding calldata against the token ABI;
    • exportPrivateKey explicitly 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. delegatedSignerReady is true only when EMBEDDED_WALLETS is 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:

Per-agent spend policy in Settings — the limits that bound autonomous action

  • wallet_withdraw and every defi_* 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 guarded executeWithdraw / executeApprovedDefi path.
  • 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_CLOSED controls 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 ​

FlagDefaultEffect
EMBEDDED_WALLETSfalseOn: user-owned embedded wallets, backend reads + builds unsigned tx, user signs client-side. Off: legacy Privy server wallets, server-side signing.
REAP_USER_FUNDEDfalseOn (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_ONLYfalseOn (once wired): agent's on-chain money-tools unregistered; card + commerce only. Off: legacy full tool set.
PRIVY_AUTHORIZATION_KEYemptyP-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.

  • 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.

be everywhere — live here.