Get a deposit address
How it works
-
You get an address
It is derived from the destination you gave, so it exists before anyone deploys anything, it never changes, and anyone can ask the chain for the same answer without going through us.
-
Someone sends USDC to it
An exchange withdrawal, an on-ramp, another wallet. Native Circle USDC on that chain, like to any other address.
-
The contract forwards it
The contract burns the USDC through Circle's Cross-Chain Transfer Protocol, naming your wallet as the only possible recipient. Circle attests the burn, and the same amount is minted to you on the destination chain. About half an hour, measured end to end.
What we never hold
Not your keys
The deposit address is a contract, not a wallet. There is no private key for it, on our servers or anywhere else. Nothing to steal, nothing to lose, nothing to hand over.
Not your funds
USDC sitting in the contract can only leave one way: to the destination written into it when it was created. Our servers cannot redirect it, hold it back, or take more than the fee cap written in the code. There is no function for that.
What our server actually does: it pays the network fees. It calls the contract's single public function when USDC arrives, and it presents Circle's attested message on the destination chain. That is the whole job. If it disappears, anyone, including you, can call the same function and the money still goes to you.
There is no server between you and your money
This page is a static file. It talks to the blockchain directly, with several public endpoints in reserve, and it works the same served from anywhere, including IPFS.
your browser
|
+-- asks the factory contract for your deposit address (eth_call)
+-- asks the departure chain whether the route exists (eth_call)
+-- derives your Solana token account, then checks it (getAccountInfo)
|
+-- optionally tells our keeper there is gas to pay (can fail, nothing breaks)
- The address comes from the contract, not from us. The page calls
predict()on the factory and shows you what the chain answered. We could not show you a different address even if we wanted to: you can run the same call yourself against any endpoint and compare. - Nothing you type reaches our servers to get an address. The wallet you paste goes to a public blockchain endpoint, exactly as it would in any wallet application.
- The only call to us is optional. We are told that a deposit address is worth watching, so that we pay the gas when money lands. If that call fails, or if we stop running, the address keeps working and anyone can trigger the forward.
How you can check it
- The contract is short. One immutable destination, one immutable fee cap, one function named
sweep(). No owner, no withdraw, no pause, no way to change where the money goes. The source is in the repository. - The fee cap is part of the address. It is compiled into the contract, and the contract code is part of the CREATE2 address. Changing the cap produces different addresses, so the terms of an address you already hold can never change, not even by us.
- You only pay when we pay. The fee covers the gas we advance on two chains, plus a margin. It is an argument to
sweep(), never a calculation the contract does, because no contract can know what gas costs in dollars on another chain. What the contract enforces is that it can never exceed the cap, and that it always goes to the same fixed address. Callsweep()yourself, with no argument, and the fee is zero. - Anyone can trigger the forward.
sweep()is public and gives its caller nothing. You do not depend on us to get your money. - Circle moves the money, not us. A standard CCTP burn and mint. On the standard path Circle charges no fee, measured on 2026-09-18 into Optimism from Base, Arbitrum, Ethereum, Avalanche, Polygon and Solana.
Supported chains
Where a deposit address can live, and where it can deliver. Every chain below was probed twice: the chain names its own CCTP domain, and another chain recognises it.
| Chain | Chain id | CCTP domain | Can send from | Can receive on |
|---|---|---|---|---|
| loading | ||||
Sonic and Sei can receive but cannot send: their native USDC contract has not been measured, and a guessed token address is an address where money dies.
The API, for anything that is not a browser
The page above does not need it. It exists so that a program can get the same answer in one call.
GET /api/address?from=base&to=optimism&wallet=0xYourWallet
from and to accept a name (base, optimism, solana), an alias (op, arb, sol), an EVM chain id (8453), or a CCTP domain written as a domain (domain:6).
A bare number is always a chain id, never a domain. The two numberings collide: 1 is Ethereum as a chain id and Avalanche as a domain, 10 is Optimism as a chain id and Unichain as a domain. A service that tried one then the other would one day deliver on the wrong chain without a single error on screen. So bare numbers are chain ids, domains must be written domain:n, and anything ambiguous is refused rather than guessed.
{
"depositAddress": "0x...",
"depositChain": { "name": "Base", "chainId": 8453, "domain": 6 },
"send": { "token": "USDC", "contract": "0x8335...2913" },
"destination": {
"chain": { "name": "Optimism", "chainId": 10, "domain": 2 },
"wallet": "0xYourWallet",
"mintRecipient": "0x000...YourWallet"
},
"fee": { "maxMicroUsdc": 13000, "maxUsdc": 0.013 },
"delivery": "about half an hour",
"verifyYourself": "call predict(2, 0x000...) on 0xFactory over any RPC"
}
Other endpoints: GET /api/chains lists the chains and every accepted spelling, GET /api/status reports what the service is doing. Both are free of the rate limit.
Rate limit
One request per IP per minute on /api/address. Over it you get 429 with a Retry-After header and the number of seconds to wait. The limit protects our work, not your money: the address is deterministic, so you can compute it yourself, or read predict() on the factory, and never call us at all.
When it refuses, and why
unknown_chain | the name or code matches no chain. No fallback guess is made. |
same_chain | departure and arrival are the same chain. No bridge is needed, and such a contract would have no way out. |
invalid_destination | the wallet is not a valid address for the receiving chain. |
destination_account_missing | Solana only: your USDC token account does not exist yet. See below. |
route_not_supported | the departure chain does not register that CCTP destination. Read live from the chain, not from a table. |
route_not_operated | no gas is held on that arrival chain yet, so the delivery could not be finished. Refused rather than left half done. |
no_factory | no factory is deployed on that departure chain yet. |
Receiving on Solana
Solana works differently, and the difference costs money if you get it wrong, so here it is in the open.
- USDC does not live at your wallet address. It lives in a separate token account owned by your wallet, one per token. When Solana is the destination, the CCTP message names that token account, not your wallet. This page asks the chain which account your wallet owns, and shows you the one it will use.
- That account must already exist. The instruction that mints your USDC on Solana has no way to create an account: it has neither the system program nor the token account program in its account list. A transfer to a wallet whose token account does not exist yet would be attested and stuck until someone creates it. So we check, and refuse to hand you an address until it exists.
- Making it exist costs 1 488 440 lamports, about 0.00149 SOL, measured on 2026-09-19 on two independent endpoints. It is a rent deposit, not a fee: it comes back if the account is ever closed. Receiving any USDC on that wallet once, from any wallet or exchange, creates it. Most exchanges create it for you as part of the withdrawal.
- There are no sub accounts. One token account per owner per token: every distinct owner needs its own, so the deposit is paid once per owner, not once per service.
Sending from Solana is not available yet: it needs its own on chain program, specified in the repository, not written.
Limits, stated plainly
- The forward is one way, to the destination fixed at creation. A different destination means a different address.
- Only native USDC is forwarded. Any other token sent to the address stays there.
- Some exchanges refuse withdrawals to contract addresses. Check yours before sending a large amount.
- Circle can pause CCTP. The USDC then stays in the contract and leaves when CCTP resumes. It does not go anywhere else.
- We pay the gas on both chains. If we stop, nothing is lost: anyone can call
sweep(), and anyone can present the attested message on the destination chain. - Addresses are shown in lower case, exactly as the contract returned them. That is the same address as any mixed case version of it.
Status
| Phase | What | State |
|---|---|---|
| 1 | The contracts, compiled and tested | written, not yet deployed |
| 2 | This page and the API | written, not yet running |
| 3 | The keeper that pays the gas | written, not yet running |
| 4 | First real transfer, with measured costs | not started |
| 5 | Sending from Solana | specified, not written |
Nothing here handles real money yet. This page will say so until it does.