Crossmint agent wallets: where the spend cap meets x402
Crossmint sells agent wallets with "a spend cap, allowed counterparties, and a time window," enforced onchain. We traced where that cap applies when an agent pays an x402 endpoint. On Stellar, Crossmint's open-source contract checks every payment. On EVM, where Crossmint's x402 guide runs, the docs never say.
Crossmint's agent stack has three products, built to work alone or together: Agent Cards, Agent Wallets and Agent Checkouts. The wallet pitch is the one every agent platform wants to make. The user owns a non-custodial wallet. The agent gets its own key with limits. The limits live in the contract, not in the agent's prompt. The user can revoke the key at any time.
That is the right design. This audit asks two questions. How much of it does a developer get by following Crossmint's own guides? And where does an x402 payment actually meet the limit?
Sources: Crossmint's documentation as fetched on 9 October 2026; @crossmint/wallets-sdk 1.20.0 at commit 4ab8408; the M2M Payments sample app at 42994f3; Crossmint/stellar-smart-account at bee3339; and the x402 repository at 7f2b2f1. We did not open a Crossmint account. Wherever behavior depends on Crossmint's closed backend, we say so.
The short version:
- The primitive exists. Crossmint calls it scopes: a per-token spending limit with an optional reset interval, a recipient allowlist, and a signer expiry.
- Crossmint's agent guides and its M2M reference app register the agent's key without scopes. A signer without scopes has unrestricted token spending. The only cap in the sample is a per-call default in application code, and the calling agent can raise it.
- On EVM, an x402 payment reaches the wallet as a read-only ERC-1271 check on a 32-byte hash. That path cannot keep a running total. Crossmint does not document whether scopes apply to typed-data signatures at all.
- On Stellar, Soroban hands the account contract the full
transfer(from, to, amount)call, and Crossmint's policy checks the token, recipient, amount and a cumulative window. That is exactly the call an x402 payment on Stellar makes.
Three products, one promise
Crossmint's agents overview splits the job in two. Cards and wallets are spending power. Agent Checkouts and the x402 and MPP protocols are where that spending power goes. An agent buying from a web store uses an agent card inside a checkout. An agent paying an API per call, or paying another agent, uses an agent wallet over x402 or MPP.
The overview makes one promise for both: "Card limits hold at Visa and Mastercard, wallet limits hold onchain." The Agent Wallets page repeats it. The user "adds the agent as a signer with a spend cap, allowed counterparties, and a time window," and "a transaction outside the delegation is rejected at the wallet level."
Per Crossmint's architecture page, the EVM wallet is an ERC-4337 smart account with ERC-7579 modules. On Solana it is a program-derived address. On Stellar it is a Soroban contract. The same page says "all permissions are enforced onchain and fully auditable."
What a scope actually is
The cap, the counterparty list and the time window map to one feature, documented under Restrict a Signer with Scopes. The SDK type is short:
// @crossmint/wallets-sdk 1.20.0 — src/api/types.ts
export type TransferScope = {
type: "transfer";
tokenLocator: string;
recipients?: string[];
spendingLimit?: {
amount: string;
/** Positive integer representing the reset interval in seconds. */
interval?: number;
};
};
export type Scope = TransferScope;
The only scope type is transfer. Each scope covers one token on one chain. The amount is in display units: "10" means 10 USDC. On EVM, Crossmint reads the token's decimals() to convert it "to the wei amount the policy contract expects." Without interval the cap is a lifetime budget. With it, the counter resets the first time the signer transfers after the interval has passed. The expiry, expiresAt, sits on the signer, not inside the scope.
Two more rules shape everything below. First, a signer with no scopes "has unrestricted token spending." Second, scopes are checked "before the transaction is broadcast onchain," and a transfer over the limit "is rejected at validation time." Keep an eye on the word transaction.
The reference path ships without scopes
When the wallet docs reach this step, they link to Crossmint's Authorize the Agent guide. It registers the agent's server signer like this:
const { locator, signatureId } = await wallet.addSigner(
{ type: "server", secret: process.env.CROSSMINT_SIGNER_SECRET },
{ prepareOnly: true }
);
There is no scopes argument. The wallet quickstart describes the same call and adds: "From here on, the server signer can sign for the wallet without any user prompt." The signers concept page spells out what that means. A signer added after wallet creation "has the same transaction authority as the wallet's recovery methods by default."
Crossmint's M2M Payments sample app, last pushed on 6 October, is the reference for agents that pay machines. It registers the agent signer with the same call, also without scopes. Its architecture document is open about the result. The approval screen tells the user the agent will be able to "pay x402 and MPP services, transfer credits, send transactions, all from this wallet."
The sample does have a cap. Its x402 payer refuses a 402 that asks for more than maxAmount, before anything is signed. The server default is "1.00", set by M2M_PAYMENTS_MAX_PAYMENT. But the payment handler uses body.maxAmount ?? ctx.maxPayment. The agent's own request can pass any positive value, and the refusal message tells the agent to "Raise maxAmount if the user agrees." The transfer and raw-transaction handlers have no cap at all. The bundled skill covers them with a heading: "Transfers and transactions: only when the user asks."
Crossmint's own How Agents Pay page names this exact pattern as the problem: "Limits enforced only in application code do not hold. They stop honest mistakes, not a compromised agent, a manipulated model, or a leaked credential."
The blast radius is wide. A server signer derives from one secret, and per the signer types reference, "the same secret produces one signer address across EVM chains within a project and environment." The authorize guide warns that "anyone who holds CROSSMINT_SIGNER_SECRET can sign on behalf of every wallet it has been authorized on." Revocation is per wallet. A platform that authorizes one backend signer on every user's wallet holds one secret that signs for all of them. In that design, scopes are the only per-wallet bound. The reference path leaves them out.
Adding scopes later is not an edit
A team that ships the guide and adds limits later hits a wall. Scopes "are immutable for the lifetime of the signer," and the SDK enforces that quietly. Call addSigner with scopes for a signer that is already approved, and it logs a wallet.addSigner.scopesIgnored warning and returns the existing, unscoped signer. If the registration is still pending, it throws and tells you to remove the signer and register it again.
So adding a cap means removing the agent signer, registering it again with scopes, and getting a fresh approval from every user through their recovery method. The SDK also cannot set the expiry. Per the scopes guide, "expiresAt and wallet-creation-time scope registration require the REST API."
The cheap fix is to scope the signer at first registration. For an agent that pays, the minimum is a USDC transfer scope with a spendingLimit and an interval, a recipients list when the counterparties are known, and an expiresAt.
Where an x402 payment touches the EVM wallet
Now assume the scope is in place. Does it bind an x402 payment? Crossmint's x402 guide uses the standard @x402/core client with the exact EVM scheme. The signer it hands to the client wraps one method:
const x402Signer = {
address: evmWallet.address,
async signTypedData(typedData) {
const { signature } = await evmWallet.signTypedData({
...typedData,
chain: "base",
});
return signature;
},
};
With USDC, that typed data is an EIP-3009 TransferWithAuthorization. Follow the signature from there.
Signing. In the SDK, signTypedData does not sign locally. It posts the typed data to Crossmint's Create Signature endpoint with type: "typed-data" and the signer's locator, waits for the signer to approve, and returns the wallet's signature. The Create Signature reference has an isSmartWalletSignature flag that wraps the result in ERC-6492. It has no field or error that mentions signer scopes.
Settlement. The facilitator never asks the wallet to act. The x402 EVM facilitator strips the ERC-6492 wrapper and calls USDC's transferWithAuthorization with the signature as bytes. We checked Base: the USDC proxy currently points to implementation 0x2ce6311ddae708829bc0784c967b7d77d19fd779, whose bytecode contains selector 0xcf092995, the bytes-signature variant. Circle's SignatureChecker sees that the payer has code and makes a staticcall to isValidSignature(bytes32, bytes) on the wallet.
agent ── typed data ──▶ Crossmint API (Create Signature)
│ scope check here? undocumented
▼
facilitator ── transferWithAuthorization(from, to, value, ..., bytes sig) ──▶ USDC
│
staticcall isValidSignature(bytes32 hash, bytes sig)
▼
Crossmint wallet
Three facts follow. The wallet receives a 32-byte hash, not the transfer. It cannot learn the amount or the recipient unless the signature itself carries the typed data, which is what ERC-7739 nested signatures are for. And it cannot write state. ERC-1271 says isValidSignature "should not be able to modify states," and a staticcall makes sure of it. A cumulative cap with a reset window needs a counter that goes up on every payment. Nothing can increment it on this path.
The x402 wallet-compatibility guide confirms the routing. A deployed ERC-4337 smart account is "Type B." It is supported for exact EIP-3009 and verified through the token's SignatureChecker.
That leaves two places where a scope could bind an x402 payment from a Crossmint EVM wallet. One is Crossmint's signing service, which could refuse to produce a TransferWithAuthorization above the cap. That is enforcement in Crossmint's backend, not onchain. The other is a stateless check inside the wallet's ERC-1271 validator, which could cap one payment but never a running total.
The docs describe neither. They describe a transfer scope checked before a transaction is broadcast, and an x402 exact payment is not a transaction the wallet sends. As of today, it is undocumented whether a scoped signer's typed-data signature counts against its limit. That needs a test, and it is step one below.
We hit the same boundary when we audited Smart Sessions. Account modules guard the execute path, and EIP-3009 goes around it. Crossmint uses the same account model, so the same question applies.
Two quieter traps on the same path
Lazy installation. Crossmint's add-signers guide says "a signer must be installed onchain before it can sign." With deployImmediately: false, "the signer is installed on its first transaction." The REST endpoint defaults to false. The SDK defaults to true on EVM. An agent that only pays x402 never sends a transaction, so it should not depend on lazy installation. The x402 compatibility guide describes the failure for wallets whose validator is installed late: "the deployed wallet can reject the inner signature and the on-chain transfer reverts."
Counterfactual wallets. A wallet that is not yet deployed pays with an ERC-6492 signature. The x402 facilitator deploys it only if its factory is on eip6492AllowedFactories, and the default is an empty list that "denies all factory deployment calls." A fresh Crossmint wallet paying a facilitator that has not allowlisted Crossmint's factory fails at verify. Deploy the wallet first.
On Stellar, the same cap binds onchain
Stellar shows what the promise looks like when the protocol cooperates. Crossmint publishes its Stellar account contract as open source. We read commit bee3339. We make no claim about which commit Crossmint runs in production.
Soroban's authorization model differs from ERC-1271 in the two ways that matter. Stellar's complex-account example says __check_auth "will be called by the Soroban environment every time require_auth or require_auth_for_args is called for the address of the account contract." Its auth_context vector "contains all the contract calls that are being authorized." And because only the host can call it, "it's safe to write to the account contract storage from __check_auth."
Crossmint's TokenTransferPolicy uses both. For every authorized call, it requires the contract to be the policy's token, the function to be transfer, and exactly three arguments. It checks the recipient in args[1] against the allowlist. It reads the amount from args[2], adds it to a persistent per-signer tracker, resets the tracker when the window has passed, and refuses once the total would exceed the limit. It also checks expiry. A standard signer with no policies is unrestricted, the same default as on EVM.
// contracts/smart-account/src/auth/policy/token_transfer.rs (bee3339)
if contract != policy.token { return None; }
if fn_name != symbol_short!("transfer") { return None; }
if args.len() != 3 { return None; }
// recipient allowlist on args[1], amount from args[2]
// then: tracker.spent + amount <= limit, window reset, record_spend()
Now compare the x402 exact scheme for Stellar. The payment is a transaction with exactly one invokeHostFunction calling transfer(from, to, amount) on a SEP-41 token. The client signs only the authorization entries. The facilitator rebuilds the transaction with its own account as source, keeps the auth entries, and submits it. The spec forbids subInvocations beyond the transfer.
When the token calls from.require_auth() on a Crossmint contract account, Soroban calls __check_auth with that exact transfer in the context. The policy sees the token, the recipient and the amount, and it records the spend. The x402 payment and the scope meet inside the contract.
There is a gap here too. Crossmint documents x402 only on EVM. The guide's prerequisite is "a funded Crossmint EVM wallet on Base with USDC." The chain where the cap provably binds an x402 payment is the one Crossmint has not documented for x402.
Cards label the trade-off. Wallets should too.
The card side of the stack handles the same trade-off openly. An agent card is an order intent: an amount, a description, a required expiry and, optionally, a merchant. From it, the agent mints a credential on a rail. On Visa Intelligent Commerce and Mastercard Agent Pay, the network holds the limit. The agent cards overview lists who enforces the limit on each rail.
Crossmint also names the weak rail. When a card supports neither network program, an order intent can fall back to an encrypted copy of the saved card. The create guide says it plainly: "Card networks enforce amount and merchant only for agentic-token rails." With the encrypted card, the application has to keep the purchase within the limits. In production that rail needs per-project access, "because minting from this rail gives your application the user's real card number."
The wallet docs have no such list, and the protocols differ. Crossmint's MPP guide uses the tempo method in push mode. The wallet sends an onchain transaction through sendTransaction, which is the path a transfer scope describes. The M2M sample's MPP payer uses the evm charge method instead, which, in its own comment, "wants an EIP-3009 transfer authorization." That is the signature path. One product, two published MPP integrations, two different enforcement points. Our MPP audit covers the protocol's settlement modes.
What it means for LLM4Agents
We sit on the other side of these payments. Our walk-up path sells inference behind an x402 quote, settled with an EIP-3009 authorization in USDC on Base. A Crossmint agent wallet is exactly the buyer that path exists for. It is card-funded, it needs no KYC for closed-loop credits in the sample's design, and it is aimed at machine services.
Smart-account buyers are a first-class case. A Crossmint EVM wallet pays from a contract, so its payments go through USDC's ERC-1271 branch. Base USDC supports that branch today. A counterfactual wallet is different: unless its factory is allowlisted, the facilitator refuses the ERC-6492 deploy.
Buyer caps act on our quote, not on our bill. The sample's default per-call cap is 1.00 credit, compared against the 402 amount before signing. Our quote is a worst case. In our tokenizer drift audit, a call with ten images was quoted at $1.13, while the call itself costs $0.08. A buyer with a $1 cap refuses it outright. Every cent we over-quote is a potential refusal from a capped buyer.
A settled payment says nothing about the buyer's limits. As the seller, we see a valid signature and a transfer. We cannot tell whether the owner capped the agent, whether a scope was checked, or whether the agent raised its own maxAmount. The same was true of the x402 SDK's default spend cap. Limits are the buyer's job. The seller's job is to stay predictable enough that a limited buyer can still pay.
Stellar is now a policy argument, not just another chain. An agent owner who wants an onchain cumulative cap on x402 spend can get one from the Soroban model today. The EVM signature path cannot provide it. That makes x402 exact on Stellar worth evaluating as a second rail.
Staying on the frontier
1. Run the scoped-signer matrix against our own endpoint. Use a Crossmint staging wallet on Base Sepolia to pay our walk-up endpoint in five configurations: an unscoped signer; a USDC scope with a limit below the quote; a recipient list that excludes our payTo; an expired signer; and a signer registered with deployImmediately: false. For each, record whether Crossmint refuses to sign, whether the facilitator verifies, and whether the transfer settles. Publish the results. That turns "undocumented" into a fact.
2. Make our quote fit common buyer caps. Cut the over-quote where it is structural, starting with images and large binary payloads. Publish the maximum walk-up quote per model at the default max_tokens, so a buyer can set a cap that does not refuse ordinary calls.
3. Decide the counterfactual policy. Either add verified factory addresses to eip6492AllowedFactories, or return an error that tells the buyer to deploy the wallet first. A silent verify failure loses a paying customer.
4. Publish buyer guidance for smart-account wallets. One page. Register the agent signer at first registration, through REST, with a USDC transfer scope, an interval, a recipients list that includes our payTo, and an expiresAt. Use deployImmediately: true. Treat any per-call maxAmount in agent code as advisory.
5. Evaluate x402 exact on Stellar. It is the rail where an account-level cumulative cap provably binds an x402 payment today. Scope it after the Base matrix, so we know what we are comparing against.
6. Watch for a typed-data statement. Track Crossmint's scopes guide and the Create Signature reference. When either says how scopes treat EIP-712 signatures, rerun step one.
Steps one and three are cheap and tell us what is true. Two and four make us easy to buy from with limits on. Five is the longer bet.
Sell to agents that have limits
OpenAI-compatible inference, paid per call in USDC over x402.
Register your agent