Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt

Use this file to discover all available pages before exploring further.

This page has been updated for Wallets SDK V1. If you are using the previous version, see the previous version of this page or the V1 migration guide.
This quickstart integrates the Crossmint Kotlin SDK into an Android app to create a non-custodial wallet for a user, fund it with testnet USDC on Base Sepolia, and send USDC to another wallet.

Before you start

Set up your project and get an API key.

Android Demo App

See a full working example with a repository to clone.
1

Install the SDK

Add the Crossmint dependencies to your app’s build.gradle.kts file:
build.gradle.kts
dependencies {
    implementation(libs.crossmint.sdk)
    implementation(libs.crossmint.compose)
}
libs.versions.toml
[versions]
crossmint-sdk = "0.0.16"

[libraries]
crossmint-compose = { module = "com.crossmint:crossmint-compose", version.ref = "crossmint-sdk" }
crossmint-sdk = { module = "com.crossmint:crossmint-sdk", version.ref = "crossmint-sdk" }

Requirements

  • Minimum Android SDK: API 24 (Android 7.0+)
  • JDK 11 or newer
  • Jetpack Compose
2

Configure your API key

The Crossmint SDK requires a client API key for authentication.
Get your client API key using the Crossmint Console.
3

Initialize the Crossmint SDK

Initialize the SDK in your Application class, then wrap your composable tree with CrossmintNonCustodialSignerProvider to enable the non-custodial signing layer. This example uses Crossmint Auth but you can use any authentication provider of your choice.
MyApplication.kt
import android.app.Application
import com.crossmint.kotlin.Crossmint

val CROSSMINT_API_KEY = "ck_staging_your_key_here"

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        Crossmint.shared(apiKey = CROSSMINT_API_KEY, appContext = this)
    }
}
Then wrap your top-level composable with CrossmintNonCustodialSignerProvider:
@Composable
fun QuickstartApp() {
    CrossmintNonCustodialSignerProvider(sdk = Crossmint.instance) {
        // Your app content goes here
        AppContent()
    }
}
The environment (staging vs production) is automatically determined by your API key. Staging keys start with ck_staging_, production keys with ck_production_.

Access SDK instances

Inside the CrossmintNonCustodialSignerProvider, access SDK functionality using LocalCrossmintSDK.current:
import com.crossmint.kotlin.auth.CrossmintAuthManager
import com.crossmint.kotlin.compose.LocalCrossmintSDK

val sdk = LocalCrossmintSDK.current
val authManager = sdk.authManager as CrossmintAuthManager
val wallets = sdk.crossmintWallets
4

Authenticate users

Actions within the SDK are user-based, so first you need to authenticate a user. Choose your authentication method:
Use Crossmint’s built-in OTP authentication for easy, email-based login.
1

Send OTP to user's email

import com.crossmint.kotlin.auth.CrossmintAuthManager
import com.crossmint.kotlin.compose.LocalCrossmintSDK

val authManager = LocalCrossmintSDK.current.authManager as CrossmintAuthManager
val email = "user@example.com"

when (val result = authManager.sendOtp(email)) {
    is Result.Success -> {
        // OTP sent — show an input field for the code
    }
    is Result.Failure -> {
        // Handle error: result.error.message
    }
}
2

Enter the OTP sent to the user's email

import com.crossmint.kotlin.auth.CrossmintAuthManager
import com.crossmint.kotlin.auth.models.OTPAuthenticationStatus
import com.crossmint.kotlin.compose.LocalCrossmintSDK

val authManager = LocalCrossmintSDK.current.authManager as CrossmintAuthManager
val email = "user@example.com"
val otpCode = "123456" // code entered by the user

when (val result = authManager.verifyOtp(email, otpCode)) {
    is Result.Success -> {
        when (result.value) {
            OTPAuthenticationStatus.AUTHENTICATED -> {
                // User is authenticated, proceed to create wallet
            }
            OTPAuthenticationStatus.INVALID_CODE -> {
                // Wrong code — ask the user to try again
            }
            else -> { /* handle other states */ }
        }
    }
    is Result.Failure -> {
        // Handle error: result.error.message
    }
}
See the Android Demo App repository for a complete UI implementation example with ViewModels and Compose screens.
5

Create a wallet

After authentication, create the user’s wallet. This example uses Base Sepolia (Base testnet) but you can choose any supported chain.
import com.crossmint.kotlin.auth.CrossmintAuthManager
import com.crossmint.kotlin.compose.LocalCrossmintSDK
import com.crossmint.kotlin.signers.SignerType
import com.crossmint.kotlin.types.EVMChain

val sdk = LocalCrossmintSDK.current
val authManager = sdk.authManager as CrossmintAuthManager
val crossmintWallets = sdk.crossmintWallets

val email = authManager.authState.value.email ?: return

when (val result = crossmintWallets.createWallet(
    chain = EVMChain.BaseSepolia,
    recovery = SignerType.Email(email)
)) {
    is Result.Success -> {
        val wallet = result.value // Wallet is ready to use!
    }
    is Result.Failure -> {
        // Handle error: result.error.message
    }
}
Use getWallet(EVMChain.BaseSepolia) for returning users to fetch their existing wallet without creating a new one.
6

Check wallet balance

Before sending tokens, check the wallet balance:
when (val result = wallet.balances()) {
    is Result.Success -> {
        val balances = result.value

        // Native token (ETH on Base Sepolia)
        println("ETH: ${balances.nativeToken.amount}")

        // USDC balance
        println("USDC: ${balances.usdc.amount}")

        // All other tokens in this wallet
        for (token in balances.tokens) {
            println("${token.name}: ${token.amount}")
        }
    }
    is Result.Failure -> {
        // Handle error: result.error.message
    }
}
7

Fund the wallet

Before sending USDC, you need to get some into the wallet.Option 1: Use the staging fund method (staging only)
when (val result = wallet.fund(token = "usdxm", amount = 10, chain = "base-sepolia")) {
    is Result.Success -> { /* Funded! */ }
    is Result.Failure -> {
        // Handle error: result.error.message
    }
}
Option 2: Use the USDC FaucetGet the wallet address and use the USDC Faucet:
val walletAddress = wallet.address
Navigate to the faucet, select Base Sepolia, and enter the wallet address.Circle faucet — Base Sepolia selected
8

Send USDC

Send USDC to another wallet using the wallet.send function:
val recipient = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2"
val tokenLocator = "base-sepolia:usdc"
val amount = 0.01

when (val sendResult = wallet.send(recipient, tokenLocator, amount)) {
    is Result.Failure -> {
        // Handle failure
    }
    is Result.Success -> {
        when (val approveResult = wallet.approve(sendResult.value.id)) {
            is Result.Success -> {
                // Transaction successful
            }
            is Result.Failure -> {
                // Handle failure
            }
        }
    }
}
The very first time that a wallet sends a transaction on this device, an OTP will be required, sent to the user via email.Following transactions sent from the same device will not need OTP verification.To handle the OTP flow, collect the isOTPRequired flow from the SDK in your composable:
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import com.crossmint.kotlin.Crossmint
import com.crossmint.kotlin.compose.CrossmintNonCustodialSignerProvider
import com.crossmint.kotlin.compose.LocalCrossmintSDK
import kotlinx.coroutines.launch

@Composable
fun QuickstartApp() {
    CrossmintNonCustodialSignerProvider(sdk = Crossmint.instance) {
        val sdk = LocalCrossmintSDK.current
        val otpRequired by sdk.isOTPRequired.collectAsState(initial = false)
        val scope = rememberCoroutineScope()

        AppContent()

        if (otpRequired) {
            OTPDialog(
                onOTPSubmit = { otp -> scope.launch { sdk.submit(otp) } },
                onDismiss = { scope.launch { sdk.cancelTransaction() } }
            )
        }
    }
}
Here is a simple OTP dialog component you can use:
@Composable
fun OTPDialog(
    onOTPSubmit: (String) -> Unit,
    onDismiss: () -> Unit,
) {
    var otpCode by remember { mutableStateOf("") }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("Enter OTP Code") },
        text = {
            TextField(
                value = otpCode,
                onValueChange = { otpCode = it },
                label = { Text("Enter OTP") },
                singleLine = true,
            )
        },
        confirmButton = {
            Button(onClick = { onOTPSubmit(otpCode) }) {
                Text("Submit")
            }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text("Cancel")
            }
        },
    )
}
And that is it! You just created a non-custodial wallet for your user and made a USDC transaction on Base Sepolia.

Launching in Production

For production, the steps are almost identical, but some changes are required:
  1. Create a developer account on the production console
  2. Create a production client API key on the API Keys page with the API scopes users.create, users.read, wallets.read, wallets.create, wallets:transactions.create, wallets:transactions.sign, wallets:balance.read, wallets.fund
  3. Update your local.properties with the production API key
  4. Use your own authentication provider: For production applications, Crossmint recommends using third-party authentication (Option 2 from Step 4) with providers like Auth0, Firebase, or Supabase, rather than Crossmint Auth (OTP). Configure JWT authentication in the Crossmint Console under API Keys → JWT Authentication.