Non-custodial accounts are available in sandbox only. Production non-custodial
accounts are on the roadmap.
Prerequisites
Before you begin, ensure that you’ve:- Obtained a Digital Asset Accounts sandbox API key with non-custodial accounts enabled. Contact your Circle representative for access. Keep the key on your backend. Never put it in your mobile application.
- Onboarded the end customer, and waited until their first account is active.
See
Onboard customers.
Store their
clientEntityIdon your backend. - Set up Google sign-in in your application. Google is the only supported
provider. Your sign-in must let you pass a custom nonce and return an ID
token. On Android, pass your Google Web application client ID as
serverClientId. Before production, you register your client IDs in Circle Hub. See Register your Google client IDs. - Installed the Digital Asset Accounts React Native SDK, its native SDKs, and
expo-crypto. See Install. - Set up a physical iPhone or Android-powered device with a screen lock. On an Android-powered device, also enroll a strong biometric, such as a fingerprint or face unlock. The authorization key is hardware-backed, so simulators and emulators without secure key hardware can’t create one.
Step 1: Set up a backend relay
The SDK never holds your API key. Your application gives the SDK a transport that sends each request to your backend. Your backend adds your API key and forwards the request to the Circle API.Allow only the SDK’s routes
Sign-in and writes
These operations sign the end customer in and change data. The SDK adds the end customer’s signature in theX-Device-Authorization header where it’s required.
Reads
Reads don’t carry the end customer’s signature, and your API key can read data for all of your end customers.Read routes
Read routes
Forward the exact bytes
The end customer signature covers the exact request body. If anything parses the body, changes its format, or serializes it again, Circle rejects the request. Most web frameworks parse JSON bodies by default, so read the body as raw bytes. This Express relay authenticates your application’s session, checks the route, adds your API key, and forwards everything else unchanged:relay.ts
authenticateAppSession is a stub that rejects every request. Replace it with
your own session check, and set res.locals.clientEntityId from your session.
The ownership rules cover the requests in this quickstart, in
Send crypto transfers, and in
Wire deposits and withdrawals.
Add a rule for any other read that your application uses. The relay rejects
reads that no rule covers.
Keep ID tokens, signatures, and request and response bodies out of your logs.
Bank account responses include sensitive fields such as billingDetails,
bankAddress, and routingNumber.
Step 2: Connect your application
Point the SDK at your relay, then create the SDK client.Send the SDK’s requests to your relay
In your application, implement the SDK’sDaaTransport interface. Pass the
bytes in bodyBytes through without changing them, and send the same session
credential that authenticateAppSession checks on your relay:
transport.ts
prepareSocialLogin,
completeSocialSession, or complete), the SDK passes your error through
unchanged. For every other call, the SDK throws a DaaError whose detail.kind
is transport, and keeps your error as cause. Its detail.idempotencyKey is
the key to reuse when you retry.
Create the SDK client
Create oneDaaClient when your application starts:
appId: a stable identifier that you choose for your application, such as its bundle ID. Use 1 to 41 characters fromA-Z,a-z,0-9,.,_, and-. The SDK uses it only on the device, to keep your application’s authorization keys separate from other apps. It isn’t sent to Circle. If you change it, the SDK can’t find existing authorization keys, and end customers must sign in again.keyPolicy: must beDEVICE_KEY_POLICY. The SDK rejects any other value.
appId, environment, or
keyPolicy is invalid. An appId in the wrong format throws a
contract_violation error.
Step 3: Sign in the end customer
Signing in links the end customer to their onboardedclientEntityId and
registers an authorization key on their device. It takes two parts: social
sign-in, then key registration.
Complete social sign-in
The SDK prepares a login and returns a nonce and a state value. Your application runs its own Google sign-in with those values and passes the ID token back to the SDK. The SDK doesn’t show any sign-in screens.prepared.oidcState before you continue. Keep your existing cross-site request
forgery (CSRF) protection.
Expected result: completeSocialSession returns a pending registration. A
prepared login expires after 5 minutes. If it does, the SDK throws a
login_request_expired error, and you start again with a new
prepareSocialLogin call. The device registration that follows expires after 5
minutes too. If the end customer backs out of the provider’s sign-in, call
await prepared.abandon(). Discarding prepared doesn’t clear the sign-in
state that the SDK keeps on the device.
Register the authorization key
Complete the registration. The SDK registers the authorization key that it prepared on the device during sign-in:registration.status is ACTIVE, and
registration.context.clientEntityId matches the value you sent.
A new device for an existing end customer follows the same steps: social login,
then registration. So does a reinstall of your application. The new key is
added. Earlier keys stay registered.
Registration doesn’t create an account. You create it in the next step.
Step 4: Create the account
Create a non-custodial account on Arc. The device asks the end customer to confirm with biometrics, or the screen lock on an iPhone, before the SDK signs the request:custodyType,type, andpurpose: must beself,third_party, andcustody.clientEntityId: must match the end customer linked at sign-in.description: required. A label for the account, up to 255 characters.blockchain: the account’s home blockchain. Optional, and defaults toARC. You can’t change it later.idempotencyKey: a UUID v4. Store it before you send the request, and reuse it if you retry.
status.kind of
pending. Read the account until it’s active:
active.status.kind is active. If it’s rejected, the
account can’t be used.
If the end customer cancels the prompt, user.accounts.create throws a
DaaError whose detail.kind is local_user_authentication_required, and
Circle doesn’t receive the request. Ask the end customer to try again, and reuse
the same idempotencyKey.
Step 5: Get the deposit address
After the account is active, get the address where it receives funds on its home blockchain:currency:USDrequests the USDC address. The API names stablecoins by the currency they track, soUSDmeans USDC.
depositAddress.address holds the account’s address, and
depositAddress.chain names its home blockchain.
This call doesn’t need an end customer signature, because it doesn’t move funds.
It returns the address of the account’s home wallet. It doesn’t create a new
address. Send only USDC on the home blockchain to this address.
Test your integration
Before you build on the account, check that the relay and the account behave as expected in sandbox:- The relay rejects other routes: send a request for a route that isn’t in
the table, such as
POST /daa/v1/addresses/recipient, through your relay. Expect HTTP404from your relay, with no request sent to Circle. - The relay forwards writes only for the user’s non-custodial account: sign
in as one end customer. Through your relay, send a transfer whose
sourceis another end customer’s account. Then send one whosesourceis a custodial account. Expect HTTP404from your relay both times, with no request sent to Circle. - The relay keeps end customers apart: sign in as one end customer, and
request another end customer’s account through your relay, such as
GET /daa/v1/accounts/{otherAccountId}. Expect HTTP404from your relay. Do the same for wire instructions on one of your own bank accounts with another end customer’s account, such asGET /daa/v1/banks/wires/{bankAccountId}/instructions?accountId={otherAccountId}. - The relay forwards bytes unchanged: create the account in Step 4. If any
part of your relay changes the body, Circle rejects the request and the SDK
throws a
contract_violationerror. - The account receives funds: get testnet USDC on Arc Testnet from the Circle Faucet, and send it to the deposit address from Step 5. Then check the address on an Arc Testnet block explorer.
Next steps
The account is ready to receive funds. From here:- To send funds from the account, see Send crypto transfers.
- To withdraw to a bank account, see Wire deposits and withdrawals.
- To handle SDK errors, see Errors.
- Before you go live, see Going to production.