Create Your First User-Controlled Wallet

Use API requests and Circle’s sample app to create a user-controlled wallet.

This guide outlines how to initialize and create a user-controlled wallet by setting their PIN code and security questions. It utilizes Circle’s sample application in combination with API requests that can be done via Circle's API references or cURL requests. cURL requests are provided inline, while API references are linked from the API endpoint text. Instructions on using the API references are in this guide

You can create both Smart Contract Accounts (SCA) and Externally Owned Accounts (EOA) wallets. To learn more, see the Account Types guide.

Prerequisites

  1. Create a developer account and acquire an API key in the console.
  2. Install the Web3 Services SDKs, which is currently only available for Node.js. (optional)
  3. Set up one of the web, iOS, or Android sample applications locally.

1. Configure and Run the Sample App

Once you have one of the web, iOS, or Android sample applications set up locally, you will then:

  1. Run the sample app and simulator.
  2. Obtain your App ID. This can be done by one of two options
    1. Access the developer console and navigate to the configurator within user-controlled wallets. From there, copy the App ID.
    2. Make an API request to GET /config/entity and copy the App ID from the response body.
  3. Add the App ID to the sample app.

📘

App ID

AKA Application ID is a unique identifier assigned to your application. It serves as a key that allows you to configure and manage various settings specific to your User-Controlled Wallet integration. The App ID is essential for identifying your application and enabling communication with the Circle Platform APIs.

View of the simulator app on boot

View of the sample app on launch

2. Create a User

Make a request to POST /users to create a userId. This represents the user’s account and all associated wallets, assets, and transactions. The userId is recommended to be in the UUID format.

📘

We recommend that you maintain a mapping to associate the end-user profile usernames with the userId provided to our service/end-point. You can use a local database to maintain this mapping.

// Import and configure the user-controlled wallet SDK
const { initiateUserControlledWalletsClient } = require('@circle-fin/user-controlled-wallets');
const circleUserSdk = initiateUserControlledWalletsClient({
  apiKey: '<API_KEY>'
});

const response = await circleUserSdk.createUser({
  userId: '2f1dcb5e-312a-4b15-8240-abeffc0e3463'
});
curl --request POST \
     --url 'https://api.circle.com/v1/w3s/users' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --data '
{
  "userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463"
}
'

If the request is successful, you will receive an empty response body.

{}

3. Acquire a Session Token

Next, you will need to acquire a session token. To do this, you will make a request to the POST /users/token using the previously created userId in Step 2. The userToken is a 60-minute session token, which is used to initiate requests that require a user challenge (PIN code entry). After 60 minutes, the session expires, and a new userToken must be generated via the same endpoint.

From this response, you will acquire the encryptionKey and userToken which you should provide in the respective sample app fields. Additionally, you will use the userToken in Step 4.

const response = await circleUserSdk.createUserToken({
  userId: '2f1dcb5e-312a-4b15-8240-abeffc0e3463'
});
curl --request POST \
     --url 'https://api.circle.com/v1/w3s/users/token' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --data '
{
  "userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463"
}
'
{
  "data": {
    "userToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCC9.eyJkZXZlbG9wZXJFbnRpdHlFbnZpcm9ubWVudCI6IlRFU1QiLCJlbnRpdHlJZCI6IjRlMDdhOGM5LTIxOTAtNDVlNC1hNjc0LWQyMGFkNjg4MWI3YyIsImV4cCI6MTY5MDU1MjcwNywiaWF0IjoxNjkwNTQ5MTA3LCJpbnRlcm5hbFVzZXJJZCI6ImQ2ZjkzODliLWQ5MzUtNWFlYy1iOTVhLWNjNTk1NjA2YWM5NiIsImlzcyI6Imh0dHBzOi8vcHJvZ3JhbW1hYmxlLXdhbGxldC5jaXJjbGUuY29tIiwianRpIjoiMmE0YmJlMzAtZTdkZi00YmM2LThiODMtNTk0NGUyMzE2ODlkIiwic3ViIjoiZXh0X3VzZXJfaWRfOSJ9.dhfByhxZFbJx0XWlzxneadT4RQWdnxLu3FSN9ln65hCDOfavaTL1sc4h-jUR8i4zMmfdURw3FFcQIdSbm-BUg6M7FP_fp-cs9xBbNmRZa31gMd1aKdcajJ9SvlVrfUowYfGXM3VcNF8rtTFtW-gk1-KzU4u10U35XXbbMcW1moxE0Rqx_fKotDgk2VdITuuds5d5TiQzAXECqeCOCtNoDKktMkglltbnLxOaRl2ReZjGt-ctD2V0DbYNO4T_ndPSUDI6qD7dXQRed5uDcezJYoha3Qj3tFGBglEnox2Y6DWTbllqjwmfTGrU8Pr0yz4jQz7suGwmiCzHPxcpYxMzYQ",
    "encryptionKey": "Tlcyxz7Ts9ztRLQq5+pic0MIETblYimOo2d7idV/UFM="
  }
}

4. Initialize the User's Account and Acquire the Challenge ID

You have two options to initialize your user’s account:

For this guide, we will use option one to create a user and a wallet simultaneously.

1POST /user/initialize: Initialize a user account and create a walletThis call generates wallets for the specified blockchains at the time of account creation. Use this method if you know which blockchain the wallet will be created on.
2POST /user/pin: Initialize the user accountThis call generates an account without creating a wallet. Use this method if you are unsure when creating an account on which blockchain the wallet will be created on.

Make a request to POST /user/initialize using the userToken returned from Step 3. This call returns a Challenge ID, which is used with the Circle Programmable Wallet SDK to have the user set their PIN code and security questions.

Make sure to provide a Testnet blockchain such as ETH-SEPOLIA, MATIC-AMOY, and AVAX-FUJI.

Amoy example

The following code samples show how to create an SCA wallet on Amoy and the response.

const response = await circleUserSdk.createUserPinWithWallets({
  userToken: '<USER_TOKEN>',
  accountType: 'SCA', 
  blockchains: ['MATIC-AMOY']
});
curl --request POST \
     --url 'https://api.circle.com/v1/w3s/user/initialize' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --header 'X-User-Token: <USER_TOKEN>' \
     --data '
{
"idempotencyKey": "49e3f455-60a2-4b5e-9e9e-9400b86e5f34",
"accountType": "SCA",
"blockchains": [
    "MATIC-MUMBAI"
  ]
}
'
{
  "data": {
    "challengeId": "0d1b5f41-1381-50af-983b-f54691415158"
  }
}

Solana example

The following code samples show how to create an EOA wallet on Solana and the response.

const response = await circleUserSdk.createUserPinWithWallets({
  userToken: '<USER_TOKEN>',
  accountType: 'EOA',
  blockchains: ['SOL-DEVNET']
});
curl --request POST \
     --url 'https://api.circle.com/v1/w3s/user/initialize' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --header 'X-User-Token: <USER_TOKEN>' \
     --data '
{
"idempotencyKey": "a6f4c8d2-33b1-43b0-8a46-5b14efe063d8",
"accountType": "EOA",
"blockchains": [
    "SOL-DEVNET"
  ]
}
'
{
  "data": {
    "challengeId": "0d1b5f41-1381-50af-983b-f54691415158"
  }
}

5. Create a Wallet in the Sample App

At this point, you should be ready to execute your first request through the sample app. Once you’ve entered the required fields indicated in Step 4, click Execute to continue. 

The sample application takes you through the end user initialization process, which includes setting up the user’s PIN code and security questions and having the user confirm their configuration.

6. Check User Status

Once you have completed all the steps in the sample app, you can then check the user status by making a request to GET /user providing the userToken to retrieve the status of the user’s account.

To understand the current state of the user, inspect the following values:

  1. PIN Status: This parameter indicates whether the end-user has successfully set a 6-digit PIN. If the user has set the PIN successfully, the pinStatus value will be enabled.

  2. Security Question Status: This parameter provides information about the user's recovery method status, specifically related to the defined security questions. If the end-user has successfully established a recovery method by defining their security questions, the securityQuestionStatus will be set to enabled.

📘

Additional information provided will include the number of failed attempts for both the pinStatus and the security questions. If the end-user enters an incorrect PIN or security answers more than three times, the pin entry or recovery method will be locked, and they will need to wait 30 minutes for it to be unlocked.

const response = await circleUserSdk.getUserStatus({
  userToken: '<USER_TOKEN>'
});
curl --request GET \
     --url 'https://api.circle.com/v1/w3s/user' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --header 'X-User-Token: <USER_TOKEN>'
{
  "data": {
    "id": "2f1dcb5e-312a-4b15-8240-abeffc0e3463",
    "status": "ENABLED",
    "createDate": "2023-07-26T15:27:32Z",
    "pinStatus": "ENABLED",
    "pinDetails": {
      "failedAttempts": 0
    },
    "securityQuestionStatus": "ENABLED",
    "securityQuestionDetails": {
      "failedAttempts": 0
    }
  }
}

7. Check Wallet Status

Additionally, you can make an API request to GET /wallets using the userToken to see the user’s newly created wallets.

const response = await circleUserSdk.listWallets({
  userToken: '<USER_TOKEN>'
});
curl --request GET \
     --url 'https://api.circle.com/v1/w3s/wallets' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --header 'X-User-Token: <USER_TOKEN>' 

Amoy sample response

{
  "data": {
    "wallets": [
      {
        "id": "01899cf2-d415-7052-a207-f9862157e546",
        "state": "LIVE",
        "walletSetId": "01899cf2-d407-7f89-b4d9-84d63573f138",
        "custodyType": "ENDUSER",
        "userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463",
        "address": "0x075e62c80e55d024cfd8fd4e3d1184834461db57",
        "addressIndex": 0,
        "blockchain": "MATIC-AMOY",
        "accountType": "SCA",
        "updateDate": "2023-07-28T14:41:47Z",
        "createDate": "2023-07-28T14:41:47Z"
      }
    ]
  }
}

Solana sample response

{
  "data": {
    "wallets": [
      {
        "id": "8a79c80b-4d4f-4032-971a-8bb9f9b0254f",
        "state": "LIVE",
        "walletSetId": "c43221d3-9db1-4cbf-8b18-e1dcae16b55d",
        "custodyType": "ENDUSER",
        "userId": "d8c8f832-5d4f-4123-9a7f-60120c2da5f0",
        "address": "8UFfxP3zzSeqdkZ5iLTmUGzpHPRGnydZ1Vnq5GkzKTep",
        "addressIndex": 0,
        "blockchain": "SOL-DEVNET",
        "accountType": "EOA",
        "updateDate": "2023-07-28T14:43:48Z",
        "createDate": "2023-07-28T14:43:48Z"
      }
    ]
  }
}


What’s Next

Congratulations! You’ve successfully set up your first user-controlled wallet. To learn how to send tokens to the wallet see the next guide.