Guides
Crypto payments
Govern agents that pay in USDC, USDT, EURC, ETH, BTC or SOL. Limits stay in pounds; each payment is priced at the moment it is decided, and the wallet signs only after an allow.
Agents now pay for API calls, data and services in stablecoins. Immiscible treats a crypto payment as a payment like any other: it is decided before it happens, against a rule a person wrote, in money a finance head understands, and the decision is signed.
Immiscible decides; it never holds funds, private keys or seed phrases, and it never signs or broadcasts a transaction. Your wallet, your wallet provider or your x402 client does that, after Immiscible says yes. That keeps Immiscible a decision service, not a custodian and not an exchange.
#How a crypto payment is decided
The agent asks POST /v1/actions/authorize with a payment.crypto object instead of a money amount:
{
"type": "payment",
"summary": "Buy market quotes from Tidewater for the pricing study",
"payment": {
"crypto": {
"asset": "USDC",
"network": "base",
"amount": "12.50",
"recipient": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"recipientName": "Tidewater Data",
"protocol": { "kind": "x402", "resource": "https://api.tidewater-data.example/v1/quotes" }
}
},
"provenance": [{ "source": "user", "detail": "Sam asked for the quotes" }]
}asset: USDC, USDT, EURC, PYUSD, DAI, ETH, BTC, SOL, POL or AVAX.network: base, ethereum, arbitrum, optimism, polygon, avalanche, solana, bitcoin (and base-sepolia or solana-devnet for testing), or the CAIP-2 id x402 version 2 uses, such aseip155:8453.amount: a decimal string in the asset’s own units. A number is refused, so nothing is ever rounded.recipient: checked for its network before anything else. An EVM address with mixed case must carry its EIP-55 checksum (Keccak-256, implemented in plain JavaScript); a Solana address must be 32 bytes of base58; a Bitcoin address must pass its base58check, bech32 (BIP-173) or bech32m (BIP-350) checksum. An address that fails is refused as malformed: it was probably mistyped or altered.
Immiscible then:
- Prices it at the rate of the moment, in the currency of the agent’s crypto rule (pounds unless the rule says otherwise). See Rates and the “never guessed” rule.
- Judges it against the agent’s crypto rule and the risk signals below.
- Answers
allowwith a signed receipt,approval_required, ordeny, with plain reasons. The answer carriescrypto.value(the money value),crypto.rate(the rate, its time, its source and the stablecoin’s price against its peg) andcrypto.rateKind:askedwhile a person has still to decide (the rate the payment was asked at),decidedonce it is allowed (the rate at that moment). Both arenullwhen the payment could not be priced.
The rate is recorded on the action, the decision, the ledger record and the signed receipt. The receipt keeps its usual claims (amt and cur are the money value) and adds a cry claim:
"cry": { "ast": "USDC", "net": "base", "amt": "12.5", "to": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"fx": { "r": "0.7581", "cur": "GBP", "at": "2026-10-05T14:32:05.000Z", "src": "coinbase" },
"res": "https://api.tidewater-data.example/v1/quotes" }#Crypto rules
A crypto rule is a payment mandate with a crypto block. Its limits are the mandate’s own, in pounds:
| Option | Meaning |
|---|---|
perTransaction, perPeriod (month) | A maximum per payment and per month, in pence, checked against the converted amount. |
approveAbove | Above this many pence, a person decides. |
crypto.assets, crypto.networks | Only these assets, only on these networks. |
crypto.stablecoinsOnly | On by default. ETH, BTC and SOL are refused while it is on. |
crypto.recipients | Named addresses, each on its network: { name, network, address }. With a list, it is a closed list. |
crypto.newRecipient | An address the agent has not paid (or that is not on the list): approve (ask a person, the default), deny, or allow. |
crypto.perAddressHour | { count, value }: for x402 payments to a listed address, at most this many payments and this many pence in any hour before a person is asked. { count: 120, value: 5000 } by default. |
crypto.depegBps | How far a stablecoin may trade from its peg before a payment is held. 100 (1%) by default. |
A crypto rule covers only crypto payments, and a card or bank rule never covers a payment to an address. In the console, choose Crypto payments under Rules, Add a rule.
#Risk signals
Each reads as a sentence; the technical detail is behind Details.
| Signal | Effect | What it catches |
|---|---|---|
| Lookalike address | Stops it | Address poisoning: a recipient whose first three and last three characters match an address the agent knows (its list, or one it has paid) but whose middle differs. Wallets show 0x5aAe…eAed, and attackers make addresses that look the same at a glance, then plant them in the history. |
| Known address, different network | Asks a person | An address known on Base being paid on Ethereum. Funds sent on the wrong network can be lost. |
| Stablecoin off its peg | Asks a person | USDC trading more than the rule allows from one US dollar, priced from the source, never assumed. |
| New address | Per the rule | The first payment to an address. |
| Unusually many payments to one address | Asks a person | Pay per request: an x402 payment to an address the rule names, within its per-payment limit, is not held as a duplicate or a split payment (repeating the same small amount is what x402 is for), and the 10-in-10-minutes velocity line does not apply to it. Instead, more than crypto.perAddressHour (120 payments or £50 in an hour to that address, by default) asks a person: “Unusually many payments to Tidewater Data in the last hour”. Any other crypto payment keeps the duplicate, split-payment and velocity checks. |
| Far above its usual | Asks a person | More than five times the median of the agent’s last five or more crypto payments, in pounds. |
| Could not be priced | Asks a person | No rate within the timeout. The reason reads “Couldn’t price this payment right now.” |
The existing signals still apply: untrusted input steering a payment (the Rule of Two), manipulation language in the provenance an agent declares, declared provenance the gateway contradicts, duplicates, split payments, velocity and the monthly allowance. Words such as “send USDC to this wallet address” in an email or web page that shaped the request still count as manipulation language; the agent’s own summary of a declared crypto payment does not.
#Making the wallet wait for the answer
An allow is advisory until something on the payment path checks it. Three ways do, depending on the wallet:
| Where the agent pays | How the decision reaches the wallet | Page |
|---|---|---|
| Any wallet the agent’s code controls | The SDK’s decideThenSign (TypeScript) or decide_then_sign (Python) asks, checks the signed receipt covers exactly this transfer, and only then calls your signing function. | below |
| x402 resources (HTTP 402) | The SDK’s x402Fetch or x402_request asks with the server’s payment requirements and calls the x402 signer only on allow. | x402 |
| Fireblocks | The API Co-Signer calls Immiscible before it signs and signs only on approval. | Fireblocks |
| Coinbase CDP and AgentKit, Turnkey, Privy, Circle | No outside approval call exists; the agent asks first, through the SDK wrapper. | Coinbase CDP, Turnkey, Privy, Circle |
| A Stripe Issuing card funded from stablecoins | The card rail, as for any Stripe Issuing card. | The card rail |
#Decide, then sign
import { Immiscible, decideThenSign } from '@immiscible/sdk';
const to = '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed'; // Tidewater Data, on Base
const wallet = { // stands in for yours: sign, broadcast, return the network's hash (0x and 64 hex characters)
async sendUsdc(recipient: string, amount: string): Promise<string> { return '0x' + 'ab'.repeat(32); },
};
const immiscible = new Immiscible(); // IMMISCIBLE_AGENT_KEY, IMMISCIBLE_URL
await decideThenSign(immiscible,
{ asset: 'USDC', network: 'base', amount: '12.50', recipient: to, provenance: [{ source: 'user', detail: 'Sam asked for the quotes' }] },
async (decision) => ({ txHash: await wallet.sendUsdc(to, '12.50') }), // runs only after an allow
{ timeoutMs: 5 * 60_000 }, // how long to wait for a person; past it, ImmiscibleApprovalTimeoutError and nothing is signed
);from immiscible import Immiscible
from immiscible.crypto import decide_then_sign
to = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" # Tidewater Data, on Base
class Wallet: # stands in for yours: sign, broadcast, return the network's hash (0x and 64 hex characters)
def send_usdc(self, recipient, amount):
return "0x" + "ab" * 32
wallet = Wallet()
decide_then_sign(
Immiscible(),
{"asset": "USDC", "network": "base", "amount": "12.50", "recipient": to,
"provenance": [{"source": "user", "detail": "Sam asked for the quotes"}]},
lambda decision: {"txHash": wallet.send_usdc(to, "12.50")}, # runs only after an allow
timeout=300, # seconds to wait for a person; past it, ImmiscibleApprovalTimeoutError and nothing is signed
)While a person decides, the call waits, polling with backoff, for up to the timeout (ten minutes if you leave it out), then raises ImmiscibleApprovalTimeoutError; the approval stays open for the person. Pass wait: false (wait=False) to be told at once instead: it raises ImmiscibleApprovalRequiredError, whose decision carries the approval link, and nothing is signed.
The signing function runs only after an allow (waiting for a person if one is asked) whose receipt verifies against the published key set and whose cry claim names the same asset, network and recipient, for at least this amount. Anything else raises a refusal and nothing is signed. A returned txHash is reported back when the action is settled; Immiscible records it as reported and does not read the chain to confirm it.
This is only as strong as the agent’s own code path: an agent that holds a raw key can still sign without asking. Where that matters, use a wallet that enforces the decision itself (Fireblocks’ Co-Signer), or keep the key out of the agent and in a service that uses the wrapper.
#In the console
Spend, Crypto (shown once the workspace has a crypto rule or a crypto payment) lists every crypto payment asked, allowed and stopped, in pounds with the asset amount beside it, and the rate at that moment: “£9.48 · 1 USDC = £0.7581 at 14:32:05”. Details show the network, the full address, the transaction hash if the agent reported one, and the rate’s source. When your agents spend shows payments by hour of day and day of week, with a coral corner on any hour where one was stopped, and a small chart joins the rate captured at each decision with every payment marked on it.
An approval for a crypto payment shows the amount in pounds first, then the asset amount, then the recipient’s name (or its short address, with a copy button), then the reasons it was held. Slack, Teams and email approval messages read the same way.
#Limits of what is built
- Slack and Teams approvals keep the rate captured when the payment was asked for; the console and the phone app price it again at the moment of approval.
- Transaction hashes are as reported by the agent or the x402 server; Immiscible does not watch the chain.