Skip to main content
Non-custodial accounts are available in sandbox only. Production non-custodial accounts are on the roadmap.
By the end of this quickstart, an end customer has signed in from your React Native application, registered an authorization key on their device, and approved the creation of a non-custodial account on Arc. You also get the account’s deposit address. For background, see Non-custodial accounts. You build the flow in five steps. Step 1 runs on your backend, and the rest run in your application.

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 clientEntityId on 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.
Your API key can act for all of your end customers, and your relay adds it to every request that it forwards. Your relay must:
  • Authenticate the end customer: accept only requests from a signed-in user of your application.
  • Allow only the SDK’s routes: forward only the operations in the following tables, and reject everything else.
  • Forward writes only for the end customer’s own non-custodial account: Circle checks the end customer’s signature only when the source is a non-custodial account. Your API key alone can create accounts and debit custodial accounts. Forward account, transfer, and withdrawal requests only if they’re signed. They must be for a non-custodial account of the signed-in end customer.
  • Check ownership of unsigned requests: reads, social sign-in, and deposit address requests don’t carry the end customer’s signature. Before you forward one, check that the account or resource in the request belongs to the clientEntityId of the signed-in end customer.
Otherwise, anyone who can reach your relay could use your API key, and one end customer could read another end customer’s data or move their funds.

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 the X-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.
Before you forward a read, check that the account or resource in the request belongs to the clientEntityId of the signed-in end customer. Otherwise, one end customer could read another end customer’s data through your relay.

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’s DaaTransport interface. Pass the bytes in bodyBytes through without changing them, and send the same session credential that authenticateAppSession checks on your relay:
transport.ts
If your transport throws during sign-in (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 one DaaClient when your application starts:
  • appId: a stable identifier that you choose for your application, such as its bundle ID. Use 1 to 41 characters from A-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 be DEVICE_KEY_POLICY. The SDK rejects any other value.
The constructor throws if a field is missing, or if 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 onboarded clientEntityId 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.
When the provider returns, check that the state value matches 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.
Circle permanently links the signed-in end customer to the clientEntityId that you send. You can’t change this link later. Always read clientEntityId from your own backend session for the signed-in user. Never accept it from user input.

Register the authorization key

Complete the registration. The SDK registers the authorization key that it prepared on the device during sign-in:
Expected result: 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, and purpose: must be self, third_party, and custody.
  • 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 to ARC. You can’t change it later.
  • idempotencyKey: a UUID v4. Store it before you send the request, and reuse it if you retry.
Account creation is asynchronous. The account starts with a status.kind of pending. Read the account until it’s active:
Expected result: 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: USD requests the USDC address. The API names stablecoins by the currency they track, so USD means USDC.
Expected result: 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 HTTP 404 from 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 source is another end customer’s account. Then send one whose source is a custodial account. Expect HTTP 404 from 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 HTTP 404 from your relay. Do the same for wire instructions on one of your own bank accounts with another end customer’s account, such as GET /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_violation error.
  • 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: