> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Quickstart

> Authenticate a user, create an embedded wallet, and send your first transaction using the Rain Android SDK.


This guide walks you through a complete wallet integration in Kotlin, from initializing the SDK and authenticating a user to retrieving their wallet address and sending tokens on Base Sepolia.

### Before you begin

[Install the Rain SDK](/sdks/embedded-wallets/installation#android) and configure a Base Sepolia RPC endpoint. All wallet operations in this quickstart run directly in your Android application. You do not need a Rain API key.

**Note:** Network operations use Kotlin suspend functions and must be called from a coroutine.

<Steps>
  <Step title="Create the provider">
    `RainProvider` is the Rain wallet: it owns login, wallet creation, and sessions. The wallet backend's identity is embedded in the SDK, so the default configuration is the product. It needs your `Application` for secure storage. Create one provider per app and keep it for the app's lifetime.

    ```kotlin theme={null}
    import com.rain.sdk.wallet.RainProvider

    val wallet = RainProvider(application)
    ```

    To react to a session that dies and can't be refreshed, or to enable passkeys, pass a `RainWalletConfig`. The hook is held for the provider's whole life, so don't capture an `Activity` or `ViewModel` in it.

    ```kotlin theme={null}
    import com.rain.sdk.wallet.RainWalletConfig

    val wallet = RainProvider(
        application,
        RainWalletConfig(
            passkeyDomain = "example.com",   // null (default) leaves passkeys off
            onSessionExpired = { /* hop to the main thread, then show the login screen */ },
        ),
    )
    ```
  </Step>

  <Step title="Log in: restore a session or start a login">
    On launch, wait for the SDK to restore a persisted session. A returning user skips the code entirely. The first authentication call of a launch also runs the wallet backend's one-time initialization.

    ```kotlin theme={null}
    wallet.awaitSessionRestore()

    if (wallet.hasActiveSession()) {
        // Returning user: go straight to the wallet.
    } else {
        // New device or logged out: run the one-time-code flow below.
    }
    ```

    `wallet.authState` is a `Flow<RainWalletAuthState>` (`Loading`, `Authenticated`, `Unauthenticated`) and `wallet.currentAuthState()` is its snapshot, so a Compose login screen can collect it instead of polling.
  </Step>

  <Step title="Send a one-time code">
    Send a login code to the user's email address or phone number. The contact is the account's identity: a first login with it signs the user up, and later logins with the same contact find the same account.

    ```kotlin theme={null}
    import com.rain.sdk.wallet.RainWalletContact

    wallet.sendLoginCode(RainWalletContact.Email("user@example.com"))
    // or by SMS:
    // wallet.sendLoginCode(RainWalletContact.Phone("+15551234567"))
    ```

    Calling `sendLoginCode` again for the same contact issues a new code and replaces the pending one, which is how you implement **Resend code**. A blank email or a phone number outside E.164 throws `RainError.InvalidConfig` before anything leaves the device.
  </Step>

  <Step title="Confirm the code and create the wallet">
    Pass the code the user typed. On a first login the SDK signs the user up and creates **one wallet holding an Ethereum account and a Solana account**, from a single seed, in the same request. On a returning login it finds the existing account.

    ```kotlin theme={null}
    import com.rain.sdk.error.RainError

    try {
        wallet.confirmLoginCode(code)
    } catch (e: RainError.InvalidLoginCode) {
        // RAIN_203: wrong, expired, or already-used code. Keep the screen up and let the user retype it.
    }
    ```

    A wrong code keeps the pending challenge, so the user can retype it without requesting a new one. Codes expire after 5 minutes and lock after 3 wrong attempts.

    <Check>
      The user is logged in and their wallet exists. A successful login also ends the user's sessions on other devices; the signed-out device's `onSessionExpired` fires on its next call.
    </Check>
  </Step>

  <Step title="Build the SDK and resolve the wallet">
    Register the provider with the chains you want to use and build the SDK. Then resolve the wallet-bound `RainClient`. Resolution suspends once, while the SDK materializes the wallet, and is cached afterwards.

    ```kotlin theme={null}
    import com.rain.sdk.RainChain
    import com.rain.sdk.RainSdk
    import com.rain.sdk.provider.ProviderId

    val rain = RainSdk.builder()
        .rpcEndpoints(
            mapOf(
                RainChain.BASE_SEPOLIA to "https://sepolia.base.org",
                RainChain.SOLANA_DEVNET to "https://api.devnet.solana.com",
            )
        )
        .register(wallet)
        .build()

    val client = rain.provider(ProviderId.RAIN)

    val evmAddress = client.getWalletAddress()                          // "0x…"
    val solanaAddress = client.getWalletAddress(RainChain.SOLANA_DEVNET) // base58
    ```

    Resolving before a session is live throws `RainError.TokenExpired` (`RAIN_201`). Send the EVM address (and the Solana address, if you use Solana) to your backend: creating the user's Rain application requires it. See [Wallet creation](/docs/embedded-wallets/wallet-creation).
  </Step>

  <Step title="Send a transaction">
    Fund the wallet with testnet USDC first (the [Circle faucet](https://faucet.circle.com/) supports Base Sepolia), then send some of it to another address you control. Amounts are `BigDecimal` in human units; the SDK resolves the token's decimals and converts exactly.

    ```kotlin theme={null}
    import com.rain.sdk.models.Token
    import java.math.BigDecimal

    val usdc = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"   // Base Sepolia USDC
    val recipient = "0x..."                                     // an address you control

    val balance = client.getBalance(RainChain.BASE_SEPOLIA, Token.contract(usdc))
    println("${balance.formatted} ${balance.symbol}")   // "10 USDC"

    val result = client.sendToken(
        chainId = RainChain.BASE_SEPOLIA,
        contractAddress = usdc,
        to = recipient,
        amount = BigDecimal("1.5"),
    )
    println(result.transactionHash)
    ```

    Gas is sponsored by default, so the wallet doesn't need ETH to send. Native sends use `client.sendNative(chainId, to, amount)`.

    <Check>
      You've logged in, created a wallet, and sent a token. Everything else on `RainClient` follows the same shape: [balances and history](/docs/embedded-wallets/balances-and-history), [receiving funds](/docs/embedded-wallets/receiving-funds), and [card funding](/docs/embedded-wallets/card-funding-and-settlement).
    </Check>
  </Step>
</Steps>

## Log in with a passkey instead

Users can log in with an existing passkey or add a passkey to their account for future authentication.

Configure `passkeyDomain` during SDK setup and host `https://<domain>/.well-known/assetlinks.json` for your app. Each passkey call takes the foreground `Activity` the system passkey sheet is presented from and suspends until the sheet closes, so run it in a scope that survives configuration changes, such as a `ViewModel` scope.

```kotlin theme={null}
// Returning user with a passkey on this account:
wallet.loginWithPasskey(activity)

// Logged in with a code? Register a passkey for next time:
wallet.addPasskey(activity)
```

Use `signUpWithPasskey(activity)` only when creating a new account. Returning users should use `loginWithPasskey(activity)` to access their existing wallet.

See [**Authentication options**](/docs/embedded-wallets/authentication) for the association file shape and recovery guidance.

## Log out and clean up

Use `logout()` to end the user's session and `close()` to release SDK resources.

```kotlin theme={null}
wallet.logout()   // clears the stored session; safe when none exists
rain.close()      // tears down resolved clients and stops the provider's session watcher
```

Calling `rain.close()` permanently closes the SDK instance. Create a new `RainSdk` instance before the next login. Use `rain.reset()` instead if you only need to clear resolved clients while keeping the provider active.

## Using a third-party wallet provider

If your application already uses Portal, Privy, or your own Turnkey organization, register the corresponding provider adapter instead of `RainProvider`. Authentication and session management remain with your existing provider, while subsequent wallet operations use the same `RainClient` interface.

```kotlin theme={null}
import com.rain.sdk.portal.PortalConfig
import com.rain.sdk.portal.PortalProvider

val rain = RainSdk.builder()
    .rpcEndpoints(mapOf(RainChain.BASE_SEPOLIA to "https://sepolia.base.org"))
    .register(PortalProvider(PortalConfig(sessionToken = portalSessionToken)))
    .build()

val client = rain.provider(ProviderId.PORTAL)
```

For provider configuration, authentication requirements, and supported capabilities, see [Third-party wallet providers](/sdks/embedded-wallets/third-party-providers)

## What's next

<Columns cols={3}>
  <Card title="Features" icon="table" href="/sdks/embedded-wallets/features">
    What each platform and chain supports.
  </Card>

  <Card title="Testing" icon="vial" href="/sdks/embedded-wallets/testing">
    Testnets, test tokens, and the pre-go-live checklist.
  </Card>

  <Card title="Error reference" icon="circle-exclamation" href="/sdks/embedded-wallets/error-reference">
    Every `RAIN_*` code and how to handle it.
  </Card>
</Columns>
