# Welcome

Setup your dGEN1 or explore our SDKs!

<figure><img src="/files/Nv1RddoS5ngDyggf1ps6" alt=""><figcaption></figcaption></figure>

### Where should I start?

If you're building an onchain app, we're happy you're here! \
The dGEN1 Dapp Directory launched with the devices in Fall 2025. Choose the Quickstart to get building quickly, or dive into how to use the dGEN1!

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Setup your dGEN1</strong></td><td>Quickstart guide to using</td><td><a href="/files/5hVeQ5IDqceMnLiQidZU">/files/5hVeQ5IDqceMnLiQidZU</a></td><td></td><td><a href="/pages/ZT3qwqXm35mCqzZA3JeZ">/pages/ZT3qwqXm35mCqzZA3JeZ</a></td></tr><tr><td><strong>Browse Features</strong></td><td>See what dGEN1 can do</td><td><a href="/files/pNdhzwbVMObBkBI5pVEg">/files/pNdhzwbVMObBkBI5pVEg</a></td><td></td><td><a href="/pages/6f8TxdTsv9U64h8sr5OC">/pages/6f8TxdTsv9U64h8sr5OC</a></td></tr><tr><td><strong>Wallet SDK</strong></td><td>Integrate the dGEN1 wallet</td><td><a href="/files/Wc6tKDlWhyEmN61nSgiv">/files/Wc6tKDlWhyEmN61nSgiv</a></td><td></td><td><a href="/pages/JjjojIyKxaiBPzzLwvtg">/pages/JjjojIyKxaiBPzzLwvtg</a></td></tr></tbody></table>

***

### Frequented Docs

* [Dev Quickstart](/build/quickstart)
* [Setup your dGEN1](/docs/setup-your-dgen1)
* [Recovery - dGEN1 Burner Card](/docs/setup-your-dgen1/recovery-dgen1-burner-card)
* [Claim your Airdrops](/docs/claim-your-airdrops)
* [Wallet SDK](/sdks/jvm-sdk)

***

### Support & Feedback

Reach out to us at any time for questions, issues, concerns, product ideas, or thoughts. Here are some ways to do so:

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>X</strong></td><td>Reach out anytime</td><td><a href="/files/u8m84TBQYf5s5sGqrUzT">/files/u8m84TBQYf5s5sGqrUzT</a></td><td></td><td><a href="https://x.com/FreedomFactory">https://x.com/FreedomFactory</a></td></tr><tr><td><strong>Discord</strong></td><td>Join the discussion</td><td><a href="/files/dJFGjAPeSp3iN48zQwHq">/files/dJFGjAPeSp3iN48zQwHq</a></td><td></td><td><a href="https://discord.com/invite/2WHw6UBmYn">https://discord.com/invite/2WHw6UBmYn</a></td></tr><tr><td><strong>Email</strong></td><td>hi@freedomfactory.io</td><td><a href="/files/BZjYjABIf765tDCy28rO">/files/BZjYjABIf765tDCy28rO</a></td><td></td><td><a href="mailto:hi@freedomfactory.io">mailto:hi@freedomfactory.io</a></td></tr></tbody></table>

***

Find our Open Source repository for the dGEN1 here:

{% embed url="<https://github.com/EthereumPhone>" %}

***


# Dev Quickstart

### Adding your app to the store:

As an app, there are a few steps to get integrated:

1. Setup an intro call with the team: <https://calendly.com/anthony-ff/30min>
2. Integrate the ethOS wallet with JVM or Expo SDKs below
3. Submit your Web app or mobile app: <https://forms.gle/EXx7VAUtjEfU1xbv5>
4. Review implementation with team
5. Get listed & promote :)

***

### Integrate dGEN1 wallet:

Easily integrate the dGEN1 system wallet in your android app:

[Wallet SDK](/sdks/jvm-sdk)

<figure><img src="/files/epwkNjrSLnX6hRwbGDxy" alt=""><figcaption></figcaption></figure>

***

### Browse dGEN1 SDKs

<table data-view="cards"><thead><tr><th data-type="content-ref"></th></tr></thead><tbody><tr><td><a href="/pages/aMgaSdMq5jRGPxQydWI6">/pages/aMgaSdMq5jRGPxQydWI6</a></td></tr><tr><td><a href="/pages/osT7gZs5mGMb5R11pQzn">/pages/osT7gZs5mGMb5R11pQzn</a></td></tr><tr><td><a href="/pages/4S1iLMDwpetFt1PyuyDV">/pages/4S1iLMDwpetFt1PyuyDV</a></td></tr><tr><td><a href="/pages/4S1iLMDwpetFt1PyuyDV">/pages/4S1iLMDwpetFt1PyuyDV</a></td></tr><tr><td><a href="/pages/mrNdfTl7RHbZVqIM4QwA">/pages/mrNdfTl7RHbZVqIM4QwA</a></td></tr><tr><td><a href="/pages/8qdA8cqYgMI2ouTsOceR">/pages/8qdA8cqYgMI2ouTsOceR</a></td></tr></tbody></table>

Feel free to reach out to us on Discord at <https://discord.gg/2CTuBzm9R3> and ask one of the @Wizards if you have questions!


# Airdrop to dGEN1 devices

dGEN1 devices can claim an equal share on the device

As you may know, Freedom Factory just shipped the dGEN1. The first onchain EDC that makes crypto a first-class citizen.

<figure><img src="/files/Wc6tKDlWhyEmN61nSgiv" alt=""><figcaption><p>le dGEN1 🤌</p></figcaption></figure>

The only website for the dGEN1 can be found here: <https://freedomfactory.io>

There is also a permissionless airdrop pool on Mainnet, Base, Optimism, Arbitrum and Polygon, where any tokens that are dropped, will be equally claimable by all devices.

Airdrop pool address: `0x476e48a832C593EE17317c008870a8aa4E649610`

Stats for the pool found here: [https://airdrop.freedomfactory.io](https://airdrop.freedomfactory.io/)

***

More info on the pool can be found here: [Airdrop pool info](/docs/claim-your-airdrops/airdrop-pool-info)

If you airdropped to the airdrop pool, I'm sure our users will thank you!


# dApp Store

Adding your app to the only onchain dedicated device!

We're excited you want to bring your users to the dGEN1 :) This is the longer version of the [Quickstart guide](/build/quickstart), that outlines exactly how to integrate.

For now, the Freedom Factory team will manually review each application to ensure that apps meet quality and security standards, however, we are working on an approach to verify apps automatically in the future.

***

**Submission process & what to expect:**

1. **Integrating the ethOS Wallet**:
   * [**Book a call**](https://calendly.com/anthony-ff/30min) with the team for more context, or start integrating the wallet via the link below.
   * Follow the [**Wallet Quickstart Guide**](/build/quickstart) to add wallet support and ensure dapp integration works correctly with the ethOS wallet.
   * We are also working on an App Wrapper, so your web app can function as an app quickly.
   * Please include the APK or PWA on your submission form so we can test the funcionality.
2. **Fill out the dApp Submission Form**:
   * Begin by navigating to the [**dApp Store Submission**](https://forms.gle/d6GbuJYEfufjKhuw7) and submitting your apps details
   * If you haven't already, please refer to the [**quickstart guide**](/build/quickstart) to integrate the wallet before submitting.
3. **Review and Feedback:**
   * The Freedom Factory team will review your submission, verifying that it complies with store policies and security standards.
   * You may receive feedback regarding your submission, including recommendations for improving the security, functionality, or ethOS wallet integration.
   * Any necessary updates should be addressed and resubmitted for further review.
4. **Going Live**:
   * After approval, your dapp will be added to the Web3 Dapp Store and displayed publicly to users.
   * We are then happy to cross-promote, do co-marketing and feature your app in conversations around the store.&#x20;

***

We're excited that to feature your app in the only onchain dedicated device :)

**Let us know if you have any further questions, you can always contact the team at** [**hi@freedomfactory.io**](mailto:dappstore@freedomfactory.io) **for support.**


# dGEN1 Wallet Architecture

A primer on how the keystorage works

The dGEN1 wallet implements a robust security model to ensure private keys are protected and only used under authorized conditions. The private key exists in the Trusted Execution Environment (TEE) and can never be removed, even by software updates.&#x20;

***

<figure><img src="/files/yDY9xs0WaPW1vaOpvn9c" alt=""><figcaption></figcaption></figure>

**dGEN1 Wallet Architecture Overview**\
The dGEN1 wallet enables secure transaction signing by separating public request handling from private key operations in the Trusted Execution Environment (TEE). Apps interact with a public wallet service, while sensitive signing occurs only after biometric verification via the SystemUI.

**Workflow:**

1. **App Request** – The app sends a transaction request to the public wallet service.
2. **User Prompt** – The public wallet service triggers the SystemUI to prompt the user for signing approval.
3. **Biometric Verification** – SystemUI validates the user’s biometrics.
4. **Secure Signing** – The private wallet service in the TEE executes the signature.

***

**dGEN1 Account Abstraction / EOA Emulation**\
Along with having the same security guarantees as a hardware wallet, the way the wallet signs is separate from it being a smart contract wallet. We've implemented ERC-4337 to give the actual wallet a contract address that brings super powers that regular EOA’s don’t have. A few of those include gas sponsorship, full onchain recovery, multi-transactions, and support for signing in apps that also have 4337 support.

However we've added a few more UX upgrades we think you'll love:

* EOA emulation, meaning you can still connect to sites that haven't updated to support 4337. This also means your dGEN1 wallet isn't siloed to just a single app or 4337 apps. It's completely interoperable.
* Multi-chain / L2 login connections: The dGEN1 wallet is live on every chain at all times, auto-connecting to the L2 that you need. No need to switch networks, it just defaults to the one used by the app.
* Cross-chain Gas paymaster: 4337 opens the possibilty for a gas paymaster, but we've enabled a separate account to sponsor your gas on ALL L2s. As long as you fill your "gas account" with some funds, you never need to bridge or even have the native gas token of any L2.
* OS-Level App Transactions: Since the wallet is built into the OS, you don't need to switch apps to sign a transaction, or use an in-app browser anymore. The Terminal screen shows the current transaction and can be signed without leaving your app.&#x20;
* Browser & App Store support: Both the Firefox browser and Apps from the app store are able to connect to the OS-level wallet, so you have the option to connect directly in-app to the dGEN1 wallet.
* Human Readable Transactions: We've enabled transaction decoding on the device level, so any transaction shows changes to your wallet, or assets in/out. You no longer need to trust the app UI, you can just see the transactions on your device if needed.
* Light node: Not a part of the wallet, but worth noting that the local light node proxy, when active, defaults your transactions through this local RPC. If any service is down, you will always have a backup on your device.

We will soon be releasing an interactive video to show these features as well!

***

Contact the team for any questions on discord or email at <hi@freedomfactory.io>


# Andyclaw

An open-source AI assistant for Android that can control your device, manage crypto wallets, run Linux commands, and operate autonomously in the background. Built by [Freedom Factory](https://github.com/EthereumPhone) for the [dGEN1](https://dgen.gg/) and ethOS, but runs on any Android device.

### How It Works

AndyClaw is a single APK that operates in two modes depending on what device it's running on:

#### On ethOS (dGEN1)

When installed on a device running ethOS, AndyClaw automatically detects the OS wallet service and unlocks **privileged mode**. No API key needed — authentication is handled by signing a message with your ethOS wallet.

**What you get in privileged mode:**

* Wallet integration (send transactions, check balances, manage tokens across Ethereum, Optimism, Polygon, Arbitrum, Base, and more)
* XMTP messaging (send and receive onchain messages)
* Full device control (WiFi, Bluetooth, mobile data, audio, power)
* Phone calls and call management
* Calendar read/write
* App installation and management (install, uninstall, clear data, force stop)
* Device power controls (reboot, shutdown)
* Code execution
* Screen time and usage stats
* OS-managed heartbeat (the OS triggers the AI periodically, no foreground service needed)
* An autonomous sub-account wallet the AI controls for micro-payments and DeFi

#### On Stock Android

When installed on a regular Android device, AndyClaw runs in **open mode**. You bring your own [OpenRouter](https://openrouter.ai/) API key to power the LLM.

**What you get in open mode:**

* Chat with the AI assistant
* Device info (battery, storage, device details)
* Clipboard read/write
* Contacts read/write
* SMS send/read
* Camera capture
* Location and navigation
* App listing and launching
* Notification reading
* File system operations
* Shell commands
* Web search
* Long-term memory (the AI remembers things across sessions)
* Background heartbeat via foreground service
* Termux integration (if Termux is installed)
* ClawHub skills (install community-made skills)

**Setup:**

1. Install the APK
2. On first launch, enter your OpenRouter API key (get one at [openrouter.ai](https://openrouter.ai/))
3. Tell the AI what you want help with
4. Name your AI (or keep the default)
5. Share your values/priorities
6. Choose which skills to enable (or turn on YOLO mode for full access)

### Features

#### AI Agent Loop

AndyClaw uses an agentic tool-use loop. The AI can chain multiple tool calls together to accomplish complex tasks — checking your battery, looking up a contact, sending them a message, and logging what it did, all in a single conversation turn. Up to 20 tool iterations per request.

#### Heartbeat

The heartbeat is the AI's autonomous background pulse. It wakes up periodically (default: 30 minutes, configurable) and can:

* Check device status (battery, connectivity, storage)
* Review notifications and messages
* Execute pending tasks
* Log what it found and did

On ethOS, the OS triggers the heartbeat directly. On stock Android, a foreground service keeps it alive.

Heartbeat logs are viewable in-app so you can see exactly what the AI did while you weren't looking.

#### Skills System

Skills are modular capabilities the AI can use. There are 30 built-in skills covering everything from device info to crypto wallets. Skills are tier-aware — some are available on all Android devices, others require ethOS privileged access.

**ClawHub** lets you install community-created skills written as SKILL.md files. These can be instruction-only (the AI follows written procedures) or Termux-executable (the AI runs scripts in a Linux environment).

#### Termux Integration

If [Termux](https://termux.dev/) is installed, the AI gets a full Linux environment. It can run bash commands, install packages, execute scripts, and interact with the terminal. ClawHub skills can define Termux entrypoints that get synced and executed automatically.

#### Long-Term Memory

The AI has a semantic memory system backed by SQLite FTS4 and vector embeddings. It can store and retrieve memories across sessions — facts you tell it, things it learns, context from conversations. Memories are automatically injected into the system prompt when relevant.

#### Sessions

Chat history is persisted. You can resume previous conversations or start fresh ones.

#### Extensions

Third-party apps can register as AndyClaw extensions, providing additional skills that get discovered and loaded automatically.

### Requirements

* Android 15+ (API 35)
* An OpenRouter API key (stock Android only — get one at [openrouter.ai](https://openrouter.ai/))
* Optional: [Termux](https://termux.dev/) for Linux command execution

### Models

AndyClaw routes through [OpenRouter](https://openrouter.ai/) (stock Android) or a premium gateway (ethOS). The default model is `minimax/minimax-m2.5`. You can switch models in settings.

### Building From Source

```
git clone https://github.com/EthereumPhone/AndyClaw.git
cd AndyClaw
```

Create `local.properties` if it doesn't exist and add any optional keys:

```
# Optional — only needed for wallet/crypto features
BUNDLER_API=your_pimlico_bundler_key
ALCHEMY_API=your_alchemy_api_key

# Optional — override the premium LLM gateway URL
PREMIUM_LLM_URL=https://your-gateway.com/api/llm

# Optional — build as system app (for ethOS system image builds)
SYSTEM_APP=true
```

Build the APK:

```
./gradlew assembleRelease
```

The APK will be at `app/build/outputs/apk/release/`.

### Permissions

AndyClaw requests a wide range of permissions to support its full skill set. On stock Android, most privileged permissions (device power, package management, system settings) are not usable — they require system-level access that only ethOS provides. Standard permissions (contacts, SMS, camera, location, etc.) are requested at runtime when a skill needs them.

### Github Repo:

{% embed url="<https://github.com/EthereumPhone/AndyClaw/tree/main>" %}


# Expo SDK

When adding support make sure you have a native android codebase. You can have one created by running `npx expo run:android` in your codebase.

After that, install the expo-walletsdk using `npm install --save expo-walletsdk`

Right after installing you might need to do a rebuild, because it has some native code that needs to be compiled.

In the code you need to first import all the necessary functions by adding `import * as ExpoWalletsdk from 'expo-walletsdk';`

### **Sending a transaction**

Then, let’s say you have a function where you send a tx, you can simply add an if which checks if the system-wallet is present, and if yes then run it through the walletsdk. Something like this:

```jsx
if (ExpoWalletsdk.hasSystemWallet()){
  const transaction = {
    to: '0x3a4e6eD8B0F02BFBfaA3C6506Af2DB939eA5798c', // Receipient
    value: '10', // Value in wei
		chainId: 10, 
    chainRPCUrl: 'https://mainnet.optimism.io'
  };
  var txHash = ExpoWalletsdk.sendTransaction(transaction);
  console.log(txHash)
} else {
  console.log("No system wallet found")
}
```

Essentially, `to` and `value` is all you need to make a transaction, though it’s recommended to add a specific `chainId` and `chainRPCUrl` to make sure you are on the correct network. The sdk will automatically change chain to the one you specific and will broadcast the tx to the node that you specified. If you are making a tx to a contract, make sure to add a `data` field.

### Signing a message

Same principle with sending a tx also applies to signing a message, you first check if the system-wallet is present and then make the request.

```jsx
if (ExpoWalletsdk.hasSystemWallet()){
  const message = {
    message: "Hello World",
		type: "personal_sign" // Default is personal_sign
  };
  var signature = ExpoWalletsdk.signMessage(message);
  console.log(signature)
} else {
  console.log("No system wallet found")
}
```

By default the signMessage function will use personal\_sign to sign a given message in this format. If your message is in hex format, you can define a type `personal_sign_hex`. And if you are signing typed data, you need to provide the json string in the `message` field and as type you need to write `eth_signTypedData`.

***

If there are any other questions please feel free to reach out in our discord, or message me on telegram at @mhaas\_eth


# Test ethOS in an Emulator

For devs looking to test their app

If you've already integrated the system wallet, you may want to test how your app runs on ethOS.<br>

This emulator setup runs ethOS v3, and we will update to ethOS v4 for dGEN1 as we go. \
The components are essentially the same, only UI will change.

1. First download the zip file with the image : <https://storage.googleapis.com/ethos_binaries/lineage-eng.markus-linux-x86-img.zip>
   1. If you don't have android studio, you can start by [downloading it here](https://developer.android.com/studio).
2. Add it to Android Studio by going into the folder where your Android SDK is located.
3. Create a folder called “android-32” in the `system-images` folder.
4. Create a folder inside the “android-32” folder that is called “default”.
5. Extract the x86 folder from the zip into the “default” folder.
6. Once that is done you can create a new device in android studio with ethOS. It should be marked under "LineageOS" under x86 Images or “other images”.

***

Great! You should now be able to run ethOS in Android studio with all the feature's built in.

We will periodically update ethOS, so be on the lookout for changes to the Repo.


# Wallet SDK

## 1. Kotlin Coroutines

To add the WalletSDK to your app, please add this snippet of code to your settings.gradle.kts file:

```kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven("https://jitpack.io")
    }
}
```

Then just add to the project-level build.gradle & in the app-level build.gradle, like this:

{% tabs %}
{% tab title="Groovy" %}

```groovy
// Web3j needed for the WalletSDK
implementation 'org.web3j:core:4.9.4'
implementation 'com.github.EthereumPhone:WalletSDK:0.2.0'
```

{% endtab %}

{% tab title="KTS" %}

```kts
// Web3j needed for the WalletSDK
implementation("org.web3j:core:4.9.4")
implementation("com.github.EthereumPhone:WalletSDK:0.1.0")
```

{% endtab %}
{% endtabs %}

You can check whether the system-wallet is on the dGEN1 by checking `getSystemService("wallet") != null`.

### **How to initialize the SDK:**

```kotlin
val wallet = WalletSDK(
    context = context,
    bundlerRPCUrl = BuildConfig.BUNDLER_RPC_URL,
    // optional: override default web3 provider used for reads (eth_call, code, etc.)
    web3jInstance = Web3j.build(HttpService("https://base.llamarpc.com")/)
)
```

### **How to get the dGEN1 wallet address:**

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    val address = wallet.getAddress()
}
```

### **How to sign a message:**

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    val signature = wallet.signMessage(
        message = "Message to sign",
        chainId = 1, // required
        // type = "personal_sign" // optional (default)
    )
}
```

### **How to send a single transaction:**

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    val userOpHashOrError = wallet.sendTransaction(
        to = "0x3a4e6ed8b0f02bfbfaa3c6506af2db939ea5798c",
        value = "1000000000000000000", // wei
        data = "", // Empty string means regular eth send tx
        callGas = null,                // null → auto-estimate via bundler
        chainId = 1,
        rpcEndpoint = "https://rpc.ankr.com/eth" // optional, but needs to align with chainid and bundler rpc
    )
}
```

### **How to send a multi-action transaction:**

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    val txs = listOf(
        WalletSDK.TxParams(
            to = "0x...",
            value = "0",
            data = "0x1234"
        ),
        WalletSDK.TxParams(
            to = "0x...",
            value = "12345",
            data = ""
        )
    )
    val userOpHash = wallet.sendTransaction(
        txParamsList = txs,
        callGas = null,
        chainId = 1,
        rpcEndpoint = "https://rpc.ankr.com/eth"
    )
}
```

***

That’s all you should need to know for Coroutines. You should now be able to reference the system wallet to do transactions within your app!

{% embed url="<https://github.com/EthereumPhone/WalletSDK/tree/main>" %}

If there are any other questions please feel free to reach out in our discord, or message me on telegram at `@mhaas_eth`.


# dGEN SubAccount SDK

Android SDK for managing smart contract sub-accounts on [ethOS](https://ethosmobile.org/) devices. Uses local P-256 cryptography (Android KeyStore) combined with the ethOS system wallet service for recovery, supporting ERC-4337 Account Abstraction, ERC-6492 signatures, and EIP-712 typed data signing.

### Features

* **Local P-256 signing** via Android KeyStore (no private keys leave the device)
* **Smart wallet address computation** using CREATE2 (CoinbaseSmartWallet factory)
* **ERC-4337 UserOperations** — build, sign, and submit through any bundler
* **Message signing** — `personal_sign` (ERC-191) and `eth_signTypedData` (EIP-712)
* **ERC-6492** signature wrapping for undeployed wallets
* **Multi-chain support** — Ethereum, Base, Optimism, Polygon, Arbitrum, and more
* **Batch transactions** via `executeBatch`

### Requirements

* **Android minSdk 28** (Android 9+)
* **ethOS device** — the SDK requires the ethOS system wallet service. Throws `NoSysWalletException` on non-ethOS devices.
* **Bundler RPC endpoint** (e.g. [Pimlico](https://pimlico.io/), [Alchemy](https://alchemy.com/))
* **RPC endpoint** (e.g. Alchemy, Infura)

### Installation

#### JitPack

Add the JitPack repository to your **root** `settings.gradle.kts`:

```
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
```

Add the dependency to your **module** `build.gradle.kts`:

```
dependencies {
    implementation("com.github.EthereumPhone:DgenSubAccountSDK:0.1.0")
}
```

> Replace `0.1.0` with the [latest release tag](https://github.com/EthereumPhone/DgenSubAccountSDK/releases) or use `main-SNAPSHOT` for the latest commit.

#### Packaging conflicts

If you encounter META-INF conflicts from web3j/netty transitive dependencies, add this to your app module's `build.gradle.kts`:

```
android {
    packaging {
        resources {
            pickFirsts += listOf(
                "META-INF/versions/9/OSGI-INF/MANIFEST.MF",
                "META-INF/DISCLAIMER",
            )
            excludes += listOf(
                "META-INF/INDEX.LIST",
                "META-INF/DEPENDENCIES",
                "META-INF/LICENSE.md",
                "META-INF/NOTICE.md",
                "META-INF/io.netty.versions.properties",
                "META-INF/FastDoubleParser-*",
                "META-INF/BigDecimal*",
            )
        }
    }
}
```

If using Java 17 records from web3j (AGP 8.x app modules), set:

```
android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}
```

### Quick Start

#### Initialize the SDK

```
import org.ethereumphone.walletsdk.WalletSDK
import org.ethereumphone.walletsdk.model.NoSysWalletException
import org.web3j.protocol.Web3j
import org.web3j.protocol.http.HttpService

try {
    val sdk = WalletSDK(
        context = applicationContext,
        web3jInstance = Web3j.build(HttpService("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY")),
        bundlerRPCUrl = "https://api.pimlico.io/v2/1/rpc?apikey=YOUR_KEY"
    )
} catch (e: NoSysWalletException) {
    // Not running on an ethOS device
}
```

#### Get wallet address

```
val address: String = sdk.getAddress()
```

#### Sign a message

```
// ERC-191 personal_sign
val signature = sdk.signMessage(
    message = "Hello, Ethereum!",
    chainId = 1,
    type = "personal_sign"
)

// EIP-712 typed data
val typedSig = sdk.signMessage(
    message = typedDataJsonString,
    chainId = 1,
    type = "eth_signTypedData"
)
```

#### Send a transaction

```
val txHash = sdk.sendTransaction(
    to = "0xRecipientAddress",
    value = "1000000000000000000", // 1 ETH in wei
    data = "0x",
    callGas = null, // auto-estimate
    chainId = 1
)
```

#### Batch transactions

```
val txHash = sdk.sendTransaction(
    txParamsList = listOf(
        WalletSDK.TxParams("0xAddr1", "1000000000000000000", "0x"),
        WalletSDK.TxParams("0xAddr2", "2000000000000000000", "0x"),
    ),
    callGas = null,
    chainId = 1
)
```

#### Switch chains

```
sdk.changeChain(
    chainId = 8453,
    rpcEndpoint = "https://base-mainnet.g.alchemy.com/v2/YOUR_KEY",
    mBundlerRPCUrl = "https://api.pimlico.io/v2/8453/rpc?apikey=YOUR_KEY"
)
```

### API Reference

#### Constructor

```
WalletSDK(
    context: Context,
    web3jInstance: Web3j = Web3j.build(HttpService("https://rpc.ankr.com/eth")),
    factoryAddress: String = "0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a",
    entryPointAddress: String = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
    bundlerRPCUrl: String,       // required
    keyAlias: String = "p256_walletsdk"
)
```

| Parameter           | Description                                      |
| ------------------- | ------------------------------------------------ |
| `context`           | Android Context                                  |
| `web3jInstance`     | Web3j RPC client (pass an authenticated RPC)     |
| `bundlerRPCUrl`     | ERC-4337 bundler endpoint (required)             |
| `factoryAddress`    | CoinbaseSmartWallet factory address              |
| `entryPointAddress` | ERC-4337 EntryPoint v0.6 address                 |
| `keyAlias`          | Android KeyStore alias for the P-256 signing key |

#### Core Methods

| Method                                                                                    | Returns                         | Description                                           |
| ----------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------------------------- |
| `suspend getAddress()`                                                                    | `String`                        | Smart wallet counterfactual address                   |
| `suspend sendTransaction(to, value, data, callGas, chainId?, rpcEndpoint?, gasProvider?)` | `String`                        | Send a single transaction via UserOperation           |
| `suspend sendTransaction(txParamsList, callGas, chainId?, ...)`                           | `String`                        | Batch multiple transactions                           |
| `suspend sendTransaction(userOp, chainId?, ...)`                                          | `String`                        | Submit a pre-built UserOperation                      |
| `suspend signMessage(message, chainId, type?)`                                            | `String`                        | Sign message (`personal_sign` or `eth_signTypedData`) |
| `fun signTypedData(typedDataJson, chainId)`                                               | `String`                        | Sign EIP-712 typed data directly                      |
| `suspend changeChain(chainId, rpcEndpoint, bundlerRPCUrl)`                                | `String`                        | Switch RPC and bundler to a different chain           |
| `suspend getNonce(senderAddress, rpcEndpoint?)`                                           | `BigInteger`                    | Get EntryPoint nonce                                  |
| `fun isDeployed(address)`                                                                 | `Boolean`                       | Check if wallet contract is deployed                  |
| `suspend getPair()`                                                                       | `Pair<BigInteger, BigInteger>?` | Get P-256 public key coordinates (X, Y)               |
| `fun isEthOS()`                                                                           | `Boolean`                       | Always `true` (SDK requires ethOS)                    |

#### Data Classes

```
data class TxParams(val to: String, val value: String, val data: String)

data class UserOperation(
    val sender: String,
    val nonce: BigInteger,
    val initCode: String,
    val callData: String,
    val callGasLimit: BigInteger,
    val verificationGasLimit: BigInteger,
    val preVerificationGas: BigInteger,
    val maxFeePerGas: BigInteger,
    val maxPriorityFeePerGas: BigInteger,
    val paymasterAndData: String,
    var signature: String
)

data class GasEstimation(
    val preVerificationGas: BigInteger,
    val verificationGasLimit: BigInteger,
    val callGasLimit: BigInteger
)

data class GasPrice(
    val maxFeePerGas: BigInteger,
    val maxPriorityFeePerGas: BigInteger
)
```

#### Supported Chains

The SDK includes built-in RPC mappings via `WalletSDK.getRPCforChainId()`:

| Chain        | ID      |
| ------------ | ------- |
| Ethereum     | 1       |
| Optimism     | 10      |
| BNB Chain    | 56      |
| Polygon      | 137     |
| Arbitrum     | 42161   |
| Base         | 8453    |
| Base Sepolia | 84532   |
| Zora         | 7777777 |
| Avalanche    | 43114   |

Custom chains are supported by passing your own `web3jInstance` and `bundlerRPCUrl`.

### Demo App

The `app/` module contains a Compose-based demo app that exercises the SDK. To run it:

1. Add your API keys to `local.properties`:

```
ALCHEMY_API=your_alchemy_api_key
BUNDLER_API=your_pimlico_api_key
```

2. Build and install:

```
./gradlew :app:installDebug
```

The demo app provides:

* Chain selector dropdown
* SDK initialization with error handling
* Wallet address and public key display
* Message signing (personal\_sign)
* Transaction sending form
* Result log

### Architecture

```
┌─────────────────────────────────────────────┐
│                  Your App                    │
├─────────────────────────────────────────────┤
│               WalletSDK                      │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐ │
│  │ Local P-256│  │ Create2   │  │ UserOp    │ │
│  │ KeyStore  │  │ Address   │  │ Builder   │ │
│  └────┬─────┘  └──────────┘  └─────┬─────┘ │
│       │                             │        │
│  ┌────▼─────────────────────────────▼─────┐ │
│  │         Sign & Submit Pipeline          │ │
│  └────────────────┬───────────────────────┘ │
├───────────────────┼─────────────────────────┤
│  ethOS System     │         Bundler RPC      │
│  Service          │         (Pimlico, etc.)  │
│  (recovery addr)  │                          │
└───────────────────┴──────────────────────────┘
```

* **Local P-256 key** (Android KeyStore) — primary wallet owner, signs all operations
* **ethOS system service** — provides the device recovery address (second owner)
* **CoinbaseSmartWallet factory** — CREATE2 address computation and wallet deployment
* **ERC-4337 bundler** — gas estimation and UserOperation submission

### Github Repo:

{% embed url="<https://github.com/EthereumPhone/DgenSubAccountSDK/>" %}


# Paymaster SDK

Android library for interacting with the ethOS Paymaster system. Provides balance management, top-up flows (via Daimo), ERC-4337 gas estimation (via Pimlico), and token price lookups — all wrapped in a clean, synchronous Kotlin API designed for `Dispatchers.IO`.

#### Installation

**Step 1.** Add the JitPack repository to your project's `settings.gradle.kts` (inside the `dependencyResolutionManagement` block):

```
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
```

> If your project uses a root `build.gradle.kts` with `allprojects { repositories { ... } }` instead, add the maven line there.

**Step 2.** Add the dependency to your app module's `build.gradle.kts`:

```
dependencies {
    implementation("com.github.EthereumPhone:PaymasterSDK:0.1.0")
}
```

#### Quick Start

```
// 1. Create the SDK instance
val paymasterSDK = PaymasterSDK(
    context = applicationContext,
    bundlerApiKey = "your-pimlico-api-key"
)

// 2. Initialize (connects to the on-device PaymasterProxy service)
withContext(Dispatchers.IO) {
    val available = paymasterSDK.initialize()

    if (available) {
        // 3. Read the cached balance
        val balance = paymasterSDK.getBalance()   // e.g. "12.50"

        // 4. Trigger a fresh balance sync from the backend
        paymasterSDK.queryBalanceUpdate()

        // 5. Listen for balance changes
        paymasterSDK.registerBalanceObserver { newBalance ->
            // update your UI
        }

        // 6. Initiate a top-up (returns a Daimo checkout URL)
        val topUp = paymasterSDK.initiateTopUp(
            userId = "0xYourWalletAddress",
            amount = "10"
        )
        topUp?.let {
            // open it.checkoutUrl in a browser / WebView
        }

        // 7. Estimate gas for a UserOperation
        val gasEstimation = paymasterSDK.estimateGas(userOp, chainId = 8453)

        // 8. Fetch token prices for sponsorship
        val prices = paymasterSDK.fetchSponsorshipPrices(
            listOf("0xTokenAddress1", "0xTokenAddress2")
        )
    }
}

// 9. Clean up when done (e.g. in onDestroy)
paymasterSDK.cleanup()
```

#### Permissions

The SDK declares the following permissions in its own manifest (merged automatically):

| Permission             | Purpose                                            |
| ---------------------- | -------------------------------------------------- |
| `INTERNET`             | Communicate with the Paymaster backend and Pimlico |
| `ACCESS_NETWORK_STATE` | Check network availability                         |

No runtime permissions are required — both are normal permissions granted at install time.

#### API

**PaymasterSDK**

**Constructor**

```
PaymasterSDK(
    context: Context,
    bundlerApiKey: String,
    baseUrl: String = "https://api.markushaas.com/"
)
```

| Parameter       | Description                                                  |
| --------------- | ------------------------------------------------------------ |
| `context`       | Application or Activity context                              |
| `bundlerApiKey` | Your Pimlico API key (used for gas estimation & bundler URL) |
| `baseUrl`       | *(Optional)* Paymaster backend base URL                      |

**Methods**

| Method                                                 | Return Type        | Description                                                                                                                                                                         |
| ------------------------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initialize()`                                         | `Boolean`          | Connect to the on-device `PaymasterProxy` system service. Returns `true` if the service is available (ethOS device), `false` otherwise. **Must be called before any other method.** |
| `getBalance()`                                         | `String?`          | Returns the cached paymaster balance (e.g. `"12.50"`), or `null` if unavailable.                                                                                                    |
| `queryBalanceUpdate()`                                 | `Unit`             | Triggers an asynchronous balance refresh from the backend. Listen for the result with `registerBalanceObserver`.                                                                    |
| `registerBalanceObserver(callback: (String) -> Unit)`  | `Unit`             | Registers a callback that fires whenever the balance changes.                                                                                                                       |
| `initiateTopUp(userId: String, amount: String)`        | `TopUpResult?`     | Starts a top-up flow via Daimo. Returns a `TopUpResult` with a checkout URL, or `null` on failure.                                                                                  |
| `estimateGas(userOp: UserOperation, chainId: Int)`     | `GasEstimation`    | Estimates gas for an ERC-4337 `UserOperation` on the given chain using the Pimlico bundler. Falls back to safe defaults on failure.                                                 |
| `fetchSponsorshipPrices(tokenAddresses: List<String>)` | `List<TokenPrice>` | Fetches USD prices for the given token addresses. Returns an empty list on failure.                                                                                                 |
| `getBundlerUrl(chainId: Int)`                          | `String`           | Returns the Pimlico bundler RPC URL for the given chain ID.                                                                                                                         |
| `cleanup()`                                            | `Unit`             | Releases all internal resources. Call in `onDestroy()` or when the SDK is no longer needed.                                                                                         |

**Constants**

| Constant                       | Value                                          | Description                                          |
| ------------------------------ | ---------------------------------------------- | ---------------------------------------------------- |
| `PaymasterSDK.ERROR`           | `"error"`                                      | Sentinel value indicating an error response          |
| `PaymasterSDK.UNAVAILABLE`     | `"unavailable"`                                | Sentinel value indicating the service is unavailable |
| `PaymasterSDK.ENTRY_POINT_V06` | `"0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"` | ERC-4337 EntryPoint v0.6 contract address            |

**Data Classes**

**`PaymasterSDK.TopUpResult`**

Returned by `initiateTopUp()`.

| Property         | Type     | Description                                  |
| ---------------- | -------- | -------------------------------------------- |
| `daimoPaymentId` | `String` | Unique Daimo payment identifier              |
| `checkoutUrl`    | `String` | URL to open for the user to complete payment |

**`PaymasterSDK.UserOperation`**

Input for `estimateGas()`. Represents an ERC-4337 UserOperation (v0.6).

| Property               | Type         | Description                          |
| ---------------------- | ------------ | ------------------------------------ |
| `sender`               | `String`     | Smart account address                |
| `nonce`                | `BigInteger` | Account nonce                        |
| `initCode`             | `String`     | Init code (empty string if deployed) |
| `callData`             | `String`     | Encoded call data                    |
| `callGasLimit`         | `BigInteger` | Gas limit for the main execution     |
| `verificationGasLimit` | `BigInteger` | Gas limit for verification           |
| `preVerificationGas`   | `BigInteger` | Pre-verification gas                 |
| `maxFeePerGas`         | `BigInteger` | Maximum fee per gas                  |
| `maxPriorityFeePerGas` | `BigInteger` | Maximum priority fee per gas         |
| `paymasterAndData`     | `String`     | Paymaster address + data             |
| `signature`            | `String`     | Signature bytes                      |

**`PaymasterSDK.GasEstimation`**

Returned by `estimateGas()`.

| Property               | Type         | Description                      |
| ---------------------- | ------------ | -------------------------------- |
| `preVerificationGas`   | `BigInteger` | Estimated pre-verification gas   |
| `verificationGasLimit` | `BigInteger` | Estimated verification gas limit |
| `callGasLimit`         | `BigInteger` | Estimated call gas limit         |

**`PaymasterSDK.TokenPrice`**

Returned by `fetchSponsorshipPrices()`.

| Property   | Type      | Description                  |
| ---------- | --------- | ---------------------------- |
| `address`  | `String`  | Token contract address       |
| `symbol`   | `String?` | Token symbol (e.g. `"USDC"`) |
| `priceUsd` | `Double?` | Current price in USD         |

**Error Handling**

The SDK does **not** throw custom exceptions. Instead it uses safe return values:

| Scenario                                 | Behavior                                                                                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Service not available (non-ethOS device) | `initialize()` returns `false`                                                                                                 |
| Balance unavailable                      | `getBalance()` returns `null`                                                                                                  |
| Top-up request fails                     | `initiateTopUp()` returns `null`                                                                                               |
| Gas estimation fails                     | `estimateGas()` returns safe defaults (`preVerificationGas = 70000`, `verificationGasLimit = 400000`, `callGasLimit = 200000`) |
| Price fetch fails                        | `fetchSponsorshipPrices()` returns an empty list                                                                               |

> **Threading:** All public methods are synchronous and perform I/O. Call them from a background thread or wrap in `withContext(Dispatchers.IO)`.

#### Requirements

* Android API 33+ (minSdk 33)
* ethOS device (for balance & top-up features via `PaymasterProxy` system service)
* Pimlico API key (for gas estimation)

#### GitHub Repo

{% embed url="<https://github.com/EthereumPhone/PaymasterSDK>" %}


# Contacts SDK

An Android SDK for reading device contacts with Ethereum (ETH) address and ENS name support. Built for the [ethOS](https://ethereumphone.org/) ecosystem.

ETH addresses are stored in the `DATA15` field of Android's `ContactsContract`, and ENS names are stored in `SharedPreferences` — the same convention used by the ethOS Contacts app.

[![](https://camo.githubusercontent.com/3eb6d4131b294adfb3eb923edd888a23f898db8c3529553b9afaf0aea359cd14/68747470733a2f2f6a69747061636b2e696f2f762f457468657265756d50686f6e652f436f6e746163747353444b2e737667)](https://jitpack.io/#EthereumPhone/ContactsSDK)

### Setup

#### 1. Add JitPack repository

In your root `settings.gradle.kts`:

```
dependencyResolutionManagement {
    repositories {
        // ...
        maven { setUrl("https://jitpack.io") }
    }
}
```

#### 2. Add the dependency

In your module `build.gradle.kts`:

```
dependencies {
    implementation("com.github.EthereumPhone:ContactsSDK:0.1.0")
}
```

#### 3. Add permissions

Add to your `AndroidManifest.xml`:

```
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Only needed if you want to write ETH addresses to contacts -->
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
```

Make sure to request these permissions at runtime on Android 6.0+.

### Usage

#### Initialize

```
val contactsSDK = ContactsSDK(context)
```

#### Get all contacts

```
val contacts = contactsSDK.getContacts()

for (contact in contacts) {
    println("${contact.displayName}: ${contact.ethAddress ?: "no ETH address"}")
}
```

#### Get contacts with ETH addresses only

```
val ethContacts = contactsSDK.getContactsWithEthAddress()
```

#### Get contacts with ENS names only

```
val ensContacts = contactsSDK.getContactsWithEns()
```

#### Get contacts with any Ethereum data (ETH address or ENS)

```
val web3Contacts = contactsSDK.getContactsWithEthData()
```

#### Get a specific contact by ID

```
val contact = contactsSDK.getContactById("123")
contact?.let {
    println("Name: ${it.displayName}")
    println("ETH: ${it.ethAddress}")
    println("ENS: ${it.ensName}")
    println("Phone: ${it.phoneNumber}")
    println("Email: ${it.email}")
}
```

#### Write an ETH address to a contact

```
contactsSDK.setEthAddress(contactId = 123L, address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```

#### Write an ENS name to a contact

```
contactsSDK.setEnsName(contactId = 123L, ensName = "vitalik.eth")
```

#### Add a new contact with ETH data

```
val newContactId = contactsSDK.addContact(
    displayName = "Vitalik Buterin",
    phoneNumber = "+1234567890",
    email = "vitalik@ethereum.org",
    ethAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    ensName = "vitalik.eth"
)
```

### Contact Model

```
data class Contact(
    val contactId: String,
    val displayName: String,
    val phoneNumber: String?,
    val email: String?,
    val photoUri: String?,
    val ethAddress: String?,
    val ensName: String?
)
```

Helper properties:

* `contact.hasEthAddress` — `true` if the contact has a valid ETH address
* `contact.hasEns` — `true` if the contact has an ENS name

### How ETH data is stored

This SDK uses the same storage convention as the ethOS Contacts app:

| Data        | Storage Location                                                           |
| ----------- | -------------------------------------------------------------------------- |
| ETH Address | `ContactsContract.Data.DATA15` under `StructuredName` MIME type            |
| ENS Name    | `SharedPreferences` with key `"ENS_{contactId}"` in `"contact_prefs"` file |

If `DATA15` contains a valid ETH address (`0x` + 40 hex chars), it's parsed as an ETH address. Otherwise, if it contains a dot (e.g., `vitalik.eth`), it's treated as an ENS name.

### Github Repo:

{% embed url="<https://github.com/EthereumPhone/ContactsSDK>" %}


# dGEN1 Terminal SDK

Android library for controlling the dGEN1 **terminal screen** and the **3x3 LED array** via a clean, coroutine-based Kotlin API. Both subsystems communicate with hidden system services through reflection and gracefully no-op on non-dGEN1 hardware.

### Table of Contents

* [Installation](https://github.com/EthereumPhone/TerminalSDK#installation)
* [Quick Start](https://github.com/EthereumPhone/TerminalSDK#quick-start)
* [Terminal Buttons](https://github.com/EthereumPhone/TerminalSDK#terminal-buttons)
* [LED Patterns](https://github.com/EthereumPhone/TerminalSDK#led-patterns)
* [Flash Patterns](https://github.com/EthereumPhone/TerminalSDK#flash-patterns)
* [Custom LED Grid](https://github.com/EthereumPhone/TerminalSDK#custom-led-grid)
* [Building Custom Terminal Screens](https://github.com/EthereumPhone/TerminalSDK#building-custom-terminal-screens)
* [Custom Bitmaps](https://github.com/EthereumPhone/TerminalSDK#custom-bitmaps)
* [Terminal Screen Power Control](https://github.com/EthereumPhone/TerminalSDK#terminal-screen-power-control)
* [Lifecycle Management](https://github.com/EthereumPhone/TerminalSDK#lifecycle-management)
* [Architecture Patterns for Production Apps](https://github.com/EthereumPhone/TerminalSDK#architecture-patterns-for-production-apps)
* [API Reference](https://github.com/EthereumPhone/TerminalSDK#api-reference)
* [Requirements](https://github.com/EthereumPhone/TerminalSDK#requirements)
* [Contributing](https://github.com/EthereumPhone/TerminalSDK#contributing)

***

### Installation

**Step 1.** Add the JitPack repository to your project's `settings.gradle.kts` (inside the `dependencyResolutionManagement` block):

```
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
```

> If your project uses a root `build.gradle.kts` with `allprojects { repositories { ... } }` instead, add the maven line there.

**Step 2.** Add the dependency to your app module's `build.gradle.kts`:

```
dependencies {
    implementation("com.github.EthereumPhone:TerminalSDK:0.1.0")
}
```

> **Running from source:** If you've cloned or forked this repo, include the SDK module directly in `settings.gradle.kts`:
>
> ```
> include(":TerminalSDK")
> ```
>
> Then reference it from your app module:
>
> ```
> dependencies {
>     implementation(project(":TerminalSDK"))
> }
> ```

***

### Quick Start

```
class MainActivity : ComponentActivity() {

    private var terminal: TerminalSDK? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialise — safe on non-ethOS devices (display/led will be null)
        terminal = TerminalSDK(this)

        // Show a terminal button on the terminal screen
        lifecycleScope.launch {
            terminal?.showQrOrSend(
                onQrCode = { /* user tapped the QR area */ },
                onSend   = { /* user tapped the Send area */ }
            )
        }

        // Flash the success LED pattern
        terminal?.led?.flashSuccess()
    }

    override fun onPause() {
        super.onPause()
        // Restore the default terminal screen
        lifecycleScope.launch { terminal?.dismissDisplay() }
    }

    override fun onDestroy() {
        super.onDestroy()
        // Release all resources
        terminal?.destroy()
    }
}
```

***

### Terminal Button

The terminal screen is a 428x142 pixel display on the back of the dGEN1 device. The SDK ships with pre-built button layouts that render as bitmaps and register touch handlers automatically.

#### Dual-Button Layouts

```
// QR Scan / Send TXN — left half triggers onQrCode, right half triggers onSend
terminal?.showQrOrSend(
    onQrCode = { /* open QR scanner */ },
    onSend   = { /* open send flow */ }
)

// QR Scan / Send NFT — same split, right half triggers onSendNft
terminal?.showSendNft(
    onQrCode  = { /* open QR scanner */ },
    onSendNft = { /* open NFT send flow */ }
)
```

#### Single-Button Layouts

Each renders a full-width button and fires the callback on any tap:

```
terminal?.showCopy { /* copy address to clipboard */ }

terminal?.showCopiedAddress { /* show copied confirmation */ }

terminal?.showTopUp { /* open top-up flow */ }

terminal?.showSwap { /* open swap flow */ }

terminal?.showLog { /* open block explorer */ }

terminal?.showDetailLog { /* open TX detail in explorer */ }
```

#### Status Text

Display arbitrary text on a black background (no touch handler):

```
terminal?.showBlackText("SENDING TX...")
terminal?.showBlackText("CONFIRMING...")
```

#### Dismiss

Restore the default system display (status bar) and remove touch handlers:

```
terminal?.dismissDisplay()
```

> **All `show*` methods are `suspend` functions.** Call them from a coroutine scope (e.g. `lifecycleScope.launch { ... }`).

***

### LED Matrix Patterns

The 3x3 LED array on the front of the device supports a library of built-in patterns. Each accepts an optional hex colour string — when omitted, the system accent colour is used.

#### Displaying Patterns

```
val led = terminal?.led

// Branding
led?.displayChad()                    // ethOS logo, system accent colour
led?.displayChad("#FF00FF")           // ethOS logo, custom magenta

// Directional
led?.displayPlus()                    // + symbol
led?.displayMinus()                   // − symbol
led?.displaySend()                    // arrow up ↑
led?.displayReceive()                 // arrow down ↓
led?.displaySwap()                    // swap indicator

// Status feedback
led?.displaySuccess()                 // green checkmark ✓
led?.displayError()                   // red cross ✗
led?.displayWarning()                 // yellow warning ⚠
led?.displayInfo()                    // info indicator ℹ

// Signing
led?.displaySign()                    // signing indicator

// Clear all LEDs
led?.clear()
```

#### Display by Name

You can also display a pattern dynamically by its string name:

```
led?.displayPattern("success", "#00FF00")
led?.displayPattern("chad")

// Available names:
// "chad", "plus", "minus", "success", "error", "warning",
// "info", "arrowup", "arrowdown", "swap", "sign"
```

#### List Available Patterns

```
val patterns: List<String> = led?.getAvailablePatterns() ?: emptyList()
// ["chad", "plus", "minus", "success", "error", "warning",
//  "info", "arrowup", "arrowdown", "swap", "sign"]
```

***

### Flash Patterns

Flash patterns display a status indicator briefly (default 1 second), then automatically revert to the **chad** branding pattern. Useful for confirming actions like successful transactions or errors.

```
// Flash success for 1 second, then show chad
led?.flashSuccess()

// Flash error for 2 seconds with a custom colour
led?.flashError(color = "#FF0000", durationMs = 2000L)

// All flash variants:
led?.flashSuccess(color = null, durationMs = 1000L)
led?.flashError(color = null, durationMs = 1000L)
led?.flashWarning(color = null, durationMs = 1000L)
led?.flashInfo(color = null, durationMs = 1000L)
```

#### Practical Example: Transaction Flow

```
// 1. Show "sending" on the terminal screen + send pattern on LEDs
terminal?.showBlackText("SENDING TX...")
led?.displaySend()

// 2. Wait for the transaction result...
val result = sendTransaction(...)

// 3. Flash success or error based on outcome
if (result.isSuccess) {
    led?.flashSuccess()
    terminal?.showBlackText("TX CONFIRMED")
} else {
    led?.flashError()
    terminal?.showBlackText("TX FAILED")
}

// 4. After a delay, return to normal
delay(2000)
terminal?.dismissDisplay()
```

***

### Custom LED Matrix Grid

Beyond predefined patterns, you have full control over individual LEDs in the 3x3 grid.

#### Grid Layout

```
Hardware IDs:          Grid coordinates:
  0   1   2            [0,0] [0,1] [0,2]
  3   4   5            [1,0] [1,1] [1,2]
  6   7   8            [2,0] [2,1] [2,2]
```

#### Set a Single LED

```
// Set LED at row 0, column 1 to green
led?.setColor(0, 1, "#00FF00")

// Set LED with brightness (0–8)
led?.setColor(1, 1, "#FF0000", brightness = 4)
```

#### Set All LEDs to One Colour

```
// All LEDs blue at default brightness (6)
led?.setAllColor("#0000FF")

// All LEDs white at half brightness
led?.setAllColor("#FFFFFF", brightness = 3)
```

#### Custom 3x3 Pattern

Pass a 3x3 array of hex colour strings. Use `"#000000"` for off:

```
// X pattern
led?.setCustomPattern(arrayOf(
    arrayOf("#FF0000", "#000000", "#FF0000"),
    arrayOf("#000000", "#FF0000", "#000000"),
    arrayOf("#FF0000", "#000000", "#FF0000"),
))

// Diamond pattern with brightness
led?.setCustomPattern(
    pattern = arrayOf(
        arrayOf("#000000", "#00FFFF", "#000000"),
        arrayOf("#00FFFF", "#000000", "#00FFFF"),
        arrayOf("#000000", "#00FFFF", "#000000"),
    ),
    brightness = 8
)
```

#### Adjust Brightness

```
// Global brightness (0–8)
led?.setBrightness(5)

// Adjust colour brightness programmatically (0–100%)
val dimRed = led?.applyBrightness("#FF0000", 50)  // returns "0x7F0000"
```

#### Colour Format

The SDK accepts colours in any of these formats — they're normalised internally:

| Format       | Example        |
| ------------ | -------------- |
| `#RRGGBB`    | `"#FF0000"`    |
| `#AARRGGBB`  | `"#FFFF0000"`  |
| `0xRRGGBB`   | `"0xFF0000"`   |
| `0xAARRGGBB` | `"0xFFFF0000"` |

***

### Building Custom Terminal Screens

The SDK ships with pre-built button layouts, but you can design your own terminal screen with custom buttons, icons, text, and touch regions. The terminal screen is 428x142 pixels — everything you show is an XML layout rendered into a bitmap.

#### How It Works

1. Create an XML layout sized to **428x142 px**
2. Render it into a `Bitmap` using `LayoutRenderer` (or manually)
3. Push the bitmap to the terminal screen with `showOnDisplay()`
4. Register a touch handler to make regions interactive

#### Step 1: Create Your XML Layout

Create a layout file in your app's `res/layout/` directory. The root must be **428px wide** and **142px high** with a black background to match the terminal screen:

```
<!-- res/layout/my_custom_terminal.xml -->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="428px"
    android:layout_height="142px"
    android:background="#000000">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Left button: APPROVE -->
        <ImageView
            android:id="@+id/approve_icon"
            android:layout_width="16dp"
            android:layout_height="16dp"
            android:src="@drawable/ic_check"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/approve_label"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintHorizontal_bias="0.25" />

        <TextView
            android:id="@+id/approve_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="APPROVE"
            android:textColor="#00FF00"
            android:fontFamily="@font/monomaniac"
            android:textSize="17sp"
            android:textAllCaps="true"
            android:letterSpacing="0.12"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/approve_icon"
            app:layout_constraintTop_toTopOf="parent" />

        <!-- Right button: REJECT -->
        <ImageView
            android:id="@+id/reject_icon"
            android:layout_width="16dp"
            android:layout_height="16dp"
            android:src="@drawable/ic_close"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/reject_label"
            app:layout_constraintStart_toEndOf="@+id/approve_label"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintHorizontal_bias="0.75" />

        <TextView
            android:id="@+id/reject_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="REJECT"
            android:textColor="#FF0000"
            android:fontFamily="@font/monomaniac"
            android:textSize="17sp"
            android:textAllCaps="true"
            android:letterSpacing="0.12"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/reject_icon"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
```

**Design tips:**

* Always use a `#000000` background (OLED-friendly, matches the device)
* Use the included `@font/monomaniac` font for the authentic terminal aesthetic, or apply the `@style/terminal_text` style
* Keep text uppercase with letter spacing for readability at small sizes
* Use the system accent colour for icons/labels (see Step 2)
* The ConstraintLayout dependency is already included in the SDK

#### Step 2: Render the Layout to a Bitmap

Inflate your layout, apply the system accent colour, and convert it to a bitmap:

```
fun renderCustomLayout(context: Context): Bitmap {
    val inflater = LayoutInflater.from(context)
    val view = inflater.inflate(R.layout.my_custom_terminal, null)

    // Apply the system accent colour to icons and labels
    val accentColor = Settings.Secure.getInt(
        context.contentResolver,
        "systemui_accent_color",
        0xFFFF0000.toInt()  // default red
    )

    view.findViewById<ImageView>(R.id.approve_icon)
        ?.setColorFilter(accentColor, PorterDuff.Mode.SRC_IN)
    view.findViewById<TextView>(R.id.approve_label)
        ?.setTextColor(accentColor)
    view.findViewById<ImageView>(R.id.reject_icon)
        ?.setColorFilter(accentColor, PorterDuff.Mode.SRC_IN)
    view.findViewById<TextView>(R.id.reject_label)
        ?.setTextColor(accentColor)

    // Measure and layout at the terminal screen resolution
    val width = 428
    val height = 142
    val wSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
    val hSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
    view.measure(wSpec, hSpec)
    view.layout(0, 0, width, height)

    // Draw into a bitmap
    val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
    val canvas = Canvas(bitmap)
    view.draw(canvas)
    return bitmap
}
```

> **Shortcut:** You can also use the SDK's built-in `LayoutRenderer` as a reference or extend it. The renderer in `terminal.renderer` already handles inflation, measuring, accent colouring, and bitmap conversion for the built-in layouts.

#### Step 3: Display It with Touch Handling

Push the bitmap to the terminal screen and register touch zones:

```
lifecycleScope.launch {
    val bitmap = renderCustomLayout(context)

    // Display the custom screen with a split touch handler
    terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, y, action ->
        if (action != MotionEvent.ACTION_DOWN) return@OnTouchListener

        // Split the 428px width into left/right tap zones
        if (x < 214f) {
            // Left half — APPROVE tapped
            handleApprove()
        } else {
            // Right half — REJECT tapped
            handleReject()
        }
    })
}
```

#### Step 4: Clean Up

Always dismiss your custom screen when leaving:

```
// Restore the default terminal screen
terminal?.dismissDisplay()
```

#### Full Example: Custom Approve/Reject Screen

Putting it all together in a ViewModel-driven flow:

```
class SigningViewModel(
    private val terminal: TerminalSDK?,
    private val context: Context
) : ViewModel() {

    fun showApprovalScreen(onApprove: () -> Unit, onReject: () -> Unit) {
        viewModelScope.launch {
            val bitmap = renderCustomLayout(context)

            terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, _, action ->
                if (action != MotionEvent.ACTION_DOWN) return@OnTouchListener
                if (x < 214f) onApprove() else onReject()
            })

            // Show the sign LED pattern while waiting for user input
            terminal?.led?.displaySign()
        }
    }

    fun dismissApprovalScreen() {
        viewModelScope.launch {
            terminal?.dismissDisplay()
            terminal?.led?.displayChad()
        }
    }
}
```

#### Advanced: Three-Zone Touch Layout

You're not limited to a left/right split. Divide the 428px width into any number of zones:

```
terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, _, action ->
    if (action != MotionEvent.ACTION_DOWN) return@OnTouchListener

    when {
        x < 143f  -> handleLeftButton()    // first third
        x < 286f  -> handleMiddleButton()  // middle third
        else      -> handleRightButton()   // last third
    }
})
```

#### Advanced: Programmatic Bitmap (No XML)

If you prefer to skip XML entirely, draw directly onto a Canvas:

```
fun renderProgrammatic(): Bitmap {
    val bitmap = Bitmap.createBitmap(428, 142, Bitmap.Config.RGB_565)
    val canvas = Canvas(bitmap)
    canvas.drawColor(Color.BLACK)

    val paint = Paint().apply {
        color = Color.GREEN
        textSize = 36f
        isAntiAlias = true
        typeface = Typeface.MONOSPACE
        textAlign = Paint.Align.CENTER
    }

    canvas.drawText("CONNECTED", 214f, 82f, paint)
    return bitmap
}
```

***

### Custom Bitmaps

For any fully custom content, render any `Bitmap` to the terminal screen:

```
val bitmap: Bitmap = ... // your 428x142 bitmap

// Display with no touch handler
terminal?.showOnDisplay(bitmap)

// Display with a touch handler
terminal?.showOnDisplay(bitmap, MiniDisplayTouchHandler.OnTouchListener { x, y, action ->
    if (action == MotionEvent.ACTION_DOWN) {
        if (x < 214f) {
            // Left half tapped
        } else {
            // Right half tapped
        }
    }
})
```

You can also use the built-in `LayoutRenderer` to render the SDK's pre-built layouts manually:

```
val renderer = terminal?.renderer

val bitmap = renderer?.renderQrOrSend()
val bitmap = renderer?.renderBlackText("CUSTOM MESSAGE")
```

***

### Terminal Screen Power Control

Control the terminal screen power state directly:

```
// Turn the terminal screen on/off
terminal?.display?.screenOn()
terminal?.display?.screenOff()

// Check current state
val isOn: Boolean = terminal?.display?.isScreenOn() ?: false
```

***

### Lifecycle Management

Proper lifecycle management prevents resource leaks and ensures the terminal screen returns to its default state when your app is backgrounded or destroyed.

#### Activity Lifecycle

```
class MyActivity : ComponentActivity() {

    private var terminal: TerminalSDK? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        terminal = TerminalSDK(this)
    }

    override fun onPause() {
        super.onPause()
        // Restore the system display when app loses focus
        lifecycleScope.launch {
            terminal?.dismissDisplay()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // Release all resources — clears LEDs, cancels coroutines,
        // destroys touch handlers
        terminal?.destroy()
    }
}
```

#### Jetpack Compose Lifecycle

Use `DisposableEffect` tied to the composable lifecycle:

```
@Composable
fun TerminalAwareScreen(terminal: TerminalSDK?) {
    val scope = rememberCoroutineScope()
    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> {
                    scope.launch { terminal?.showCopy { /* handle tap */ } }
                    terminal?.led?.displayChad()
                }
                Lifecycle.Event.ON_PAUSE -> {
                    scope.launch { terminal?.dismissDisplay() }
                }
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}
```

#### ViewModel Scoped Usage

If you use the SDK from a ViewModel, clean up in `onCleared()`:

```
class MyViewModel(
    private val terminal: TerminalSDK?
) : ViewModel() {

    fun showSendButton() {
        viewModelScope.launch {
            terminal?.showQrOrSend(
                onQrCode = { /* ... */ },
                onSend   = { /* ... */ }
            )
        }
    }

    override fun onCleared() {
        super.onCleared()
        terminal?.destroy()
    }
}
```

***

### Architecture Patterns for Production Apps

For larger apps (like the ethOS Wallet Manager), structure your terminal interactions through a repository layer with dependency injection.

#### Repository Pattern

Wrap the SDK in a repository to decouple UI from hardware control and emit touch events as flows:

```
sealed interface TerminalEvent {
    object CopyTapped : TerminalEvent
    object QrTapped : TerminalEvent
    object SendTapped : TerminalEvent
    object SwapTapped : TerminalEvent
    object TopUpTapped : TerminalEvent
    object LogTapped : TerminalEvent
}

@Singleton
class TerminalRepository @Inject constructor(
    private val terminal: TerminalSDK?,
    @ApplicationContext private val context: Context
) {
    private val _events = MutableSharedFlow<TerminalEvent>()
    val events: SharedFlow<TerminalEvent> = _events

    private val scope = CoroutineScope(Dispatchers.IO)

    suspend fun showSendButtons() {
        terminal?.showQrOrSend(
            onQrCode = { scope.launch { _events.emit(TerminalEvent.QrTapped) } },
            onSend   = { scope.launch { _events.emit(TerminalEvent.SendTapped) } }
        )
    }

    suspend fun showCopy() {
        terminal?.showCopy {
            scope.launch { _events.emit(TerminalEvent.CopyTapped) }
        }
    }

    suspend fun dismiss() {
        terminal?.dismissDisplay()
    }
}
```

#### Dependency Injection with Hilt

Provide the SDK as a nullable singleton — it will be `null` on non-dGEN1 devices:

```
@Module
@InstallIn(SingletonComponent::class)
object TerminalModule {

    @Provides
    @Singleton
    fun provideTerminalSDK(
        @ApplicationContext context: Context
    ): TerminalSDK? {
        return try {
            val sdk = TerminalSDK(context)
            if (sdk.isAvailable) sdk else null
        } catch (e: Exception) {
            null
        }
    }
}
```

#### Collecting Events in a ViewModel

```
@HiltViewModel
class SendViewModel @Inject constructor(
    private val terminalRepository: TerminalRepository
) : ViewModel() {

    init {
        // React to terminal touch events
        viewModelScope.launch {
            terminalRepository.events.collect { event ->
                when (event) {
                    TerminalEvent.QrTapped  -> openQrScanner()
                    TerminalEvent.SendTapped -> navigateToAmountInput()
                    else -> {}
                }
            }
        }
    }

    fun onScreenVisible() {
        viewModelScope.launch {
            terminalRepository.showSendButtons()
        }
    }

    fun onScreenHidden() {
        viewModelScope.launch {
            terminalRepository.dismiss()
        }
    }
}
```

#### Screen-Aware Terminal Updates

Update the terminal screen as the user navigates between screens:

```
@Composable
fun SendScreen(
    terminal: TerminalSDK?,
    viewModel: SendViewModel = hiltViewModel()
) {
    val scope = rememberCoroutineScope()
    val lifecycleOwner = LocalLifecycleOwner.current

    // Update terminal screen when this screen gains/loses focus
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> viewModel.onScreenVisible()
                Lifecycle.Event.ON_PAUSE  -> viewModel.onScreenHidden()
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    // ... your UI ...
}
```

***

### API Reference

#### TerminalSDK

| Property / Method                       | Description                                                      |
| --------------------------------------- | ---------------------------------------------------------------- |
| `display: TerminalDisplay?`             | Terminal screen controller (`null` if unavailable)               |
| `led: TerminalLED?`                     | LED array controller (`null` if unavailable)                     |
| `renderer: LayoutRenderer`              | Renders XML layouts to bitmaps for the terminal screen           |
| `isAvailable: Boolean`                  | `true` if either display or LED is available                     |
| `isDisplayAvailable: Boolean`           | `true` if the terminal screen is available                       |
| `isLedAvailable: Boolean`               | `true` if the LED array is available                             |
| `showQrOrSend(onQrCode, onSend)`        | Show QR/Send dual-button (suspend)                               |
| `showSendNft(onQrCode, onSendNft)`      | Show QR/Send NFT dual-button (suspend)                           |
| `showCopy(onCopy)`                      | Show Copy button (suspend)                                       |
| `showCopiedAddress(onTap)`              | Show Copied confirmation (suspend)                               |
| `showTopUp(onTopUp)`                    | Show Top Up button (suspend)                                     |
| `showLog(onLog)`                        | Show View on Explorer button (suspend)                           |
| `showDetailLog(onDetailLog)`            | Show View TX in Explorer button (suspend)                        |
| `showSwap(onSwap)`                      | Show Swap button (suspend)                                       |
| `showBlackText(text)`                   | Show text on black background (suspend)                          |
| `showOnDisplay(bitmap, touchListener?)` | Push any bitmap to the terminal screen (suspend)                 |
| `dismissDisplay()`                      | Restore default terminal screen, remove touch handlers (suspend) |
| `destroy()`                             | Release all resources — call in `onDestroy()`                    |

#### TerminalLED

| Method                                           | Description                              |
| ------------------------------------------------ | ---------------------------------------- |
| `isAvailable: Boolean`                           | Whether the LED subsystem is available   |
| `displayChad(color?)`                            | Show ethOS branding pattern              |
| `displayPlus(color?)` / `displayMinus(color?)`   | Show +/− pattern                         |
| `displaySend(color?)` / `displayReceive(color?)` | Show arrow up/down pattern               |
| `displaySwap(color?)` / `displaySign(color?)`    | Show swap/sign pattern                   |
| `displaySuccess(color?)`                         | Show success checkmark                   |
| `displayError(color?)`                           | Show error cross                         |
| `displayWarning(color?)`                         | Show warning symbol                      |
| `displayInfo(color?)`                            | Show info symbol                         |
| `displayPattern(name, color?)`                   | Show pattern by string name              |
| `flashSuccess(color?, durationMs?)`              | Flash success then revert to chad        |
| `flashError(color?, durationMs?)`                | Flash error then revert to chad          |
| `flashWarning(color?, durationMs?)`              | Flash warning then revert to chad        |
| `flashInfo(color?, durationMs?)`                 | Flash info then revert to chad           |
| `setColor(row, col, color)`                      | Set single LED colour (row/col 0-2)      |
| `setColor(row, col, color, brightness)`          | Set single LED with brightness (0-8)     |
| `setAllColor(color, brightness?)`                | Set all LEDs to one colour               |
| `setCustomPattern(pattern, brightness?)`         | Set arbitrary 3x3 colour pattern         |
| `setBrightness(brightness)`                      | Set global brightness (0-8)              |
| `applyBrightness(hexColor, brightnessPercent)`   | Adjust colour brightness (0-100%)        |
| `getAvailablePatterns(): List<String>`           | List all predefined pattern names        |
| `getSystemColor(): String?`                      | Get cached system accent colour          |
| `refreshSystemColor()`                           | Re-read the system accent colour         |
| `clear()`                                        | Turn off all LEDs                        |
| `destroy()`                                      | Clear LEDs and cancel pending coroutines |

#### TerminalDisplay

| Method                              | Description                                        |
| ----------------------------------- | -------------------------------------------------- |
| `isAvailable(): Boolean`            | Whether the terminal screen is available (suspend) |
| `isScreenOn(): Boolean`             | Check if terminal screen is on (suspend)           |
| `screenOn()` / `screenOff()`        | Power the terminal screen on/off (suspend)         |
| `refresh(bitmap, layerId): Boolean` | Push a bitmap to a display layer (suspend)         |
| `resume(layerId)`                   | Restore a display layer (suspend)                  |
| `registerTouchListener(listener)`   | Register a touch callback                          |
| `destroyTouchHandler()`             | Remove the current touch callback (suspend)        |
| `destroyTouchHandlerSync()`         | Remove touch callback synchronously                |
| `finish()`                          | Restore status bar + destroy touch handler         |

#### Display Layer Constants

| Constant           | Description                             |
| ------------------ | --------------------------------------- |
| `ID_STATUSBAR`     | System status bar layer                 |
| `ID_INCOMINGCALL`  | Incoming call layer                     |
| `ID_NOTIFICATIONS` | Notification layer                      |
| `ID_CLOCK`         | Clock layer                             |
| `ID_GOOGLEBYE`     | Always-on display (AOD) layer           |
| `ID_PERSISTENT`    | Persistent layer (stays until replaced) |

***

### Requirements

* **Android API 33+** (minSdk)
* **dGEN1 device** for hardware features — the SDK gracefully no-ops on standard Android devices
* **Kotlin Coroutines** — display methods are suspend functions

***

### Contributing

1. Fork or clone the repository
2. Open the project in Android Studio
3. The `app` module is a demo app that exercises every SDK feature — run it on an dGEN1 device to test
4. The `TerminalSDK` module is the library — make your changes there
5. Submit a pull request

#### Project Structure

```
TerminalSDK/
├── app/                          # Demo / sample application
│   └── src/main/java/.../
│       └── MainActivity.kt       # Full interactive demo
├── TerminalSDK/                  # Library module
│   └── src/main/java/.../
│       ├── TerminalSDK.kt        # Main entry point
│       ├── display/
│       │   ├── TerminalDisplay.kt      # Terminal screen controller
│       │   ├── LayoutRenderer.java     # XML → Bitmap renderer
│       │   └── MiniDisplayTouchHandler.java  # Touch event handling
│       └── led/
│           ├── TerminalLED.kt    # High-level LED controller
│           ├── LedPattern.kt     # Built-in pattern definitions
│           └── LedManager.kt     # Low-level LED proxy
└── README.md
```

#### GitHub Repo:

{% embed url="<https://github.com/EthereumPhone/TerminalSDK>" %}


# XMTP Messenger SDK

## MessengerSDK

Android library for communicating with the XMTP Messenger app via AIDL IPC. Wraps service binding, threading, and permissions into a clean coroutine-based Kotlin API.

### Installation

**Step 1.** Add the JitPack repository to your project's `settings.gradle.kts` (inside the `dependencyResolutionManagement` block):

```kotlin
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
```

> If your project uses a root `build.gradle.kts` with `allprojects { repositories { ... } }` instead, add the maven line there.

**Step 2.** Add the dependency to your app module's `build.gradle.kts`:

```kotlin
dependencies {
    implementation("com.github.EthereumPhone:MessengerSDK:0.2.0")
}
```

### Quick Start

```kotlin
// Get the SDK instance (throws if Messenger is not installed)
val sdk = MessengerSDK.getInstance(context)

// Bind to the messaging service
sdk.messaging.bind()
sdk.messaging.awaitConnected()

// Send a message as the logged-in user
val messageId = sdk.messaging.sendMessage("0xRecipientAddress", "Hello!")

// Clean up
sdk.unbindAll()
```

### Permissions

Only the **MessagingClient** (send as user) requires a runtime permission. The **IdentityClient** (isolated identity) is permission-free — any app can bind and create its own identity.

| Permission             | Required by       | Purpose                                         |
| ---------------------- | ----------------- | ----------------------------------------------- |
| `SEND_MESSAGE_AS_USER` | `MessagingClient` | Send messages as the Messenger's logged-in user |

Check and request the permission:

```kotlin
if (!MessengerPermissions.hasSendPermission(context)) {
    ActivityCompat.requestPermissions(
        activity,
        arrayOf(MessengerPermissions.SEND_MESSAGE_AS_USER),
        REQUEST_CODE
    )
}
```

### API

#### MessengerSDK

| Method                 | Description                                                |
| ---------------------- | ---------------------------------------------------------- |
| `getInstance(context)` | Get singleton instance (throws if Messenger not installed) |
| `messaging`            | `MessagingClient` for sending as the logged-in user        |
| `identity`             | `IdentityClient` for isolated identity operations          |
| `unbindAll()`          | Unbind all services                                        |

#### MessagingClient

| Method                                   | Description                                |
| ---------------------------------------- | ------------------------------------------ |
| `bind()` / `unbind()`                    | Bind/unbind the messaging service          |
| `awaitConnected()`                       | Suspend until connected                    |
| `connectionState`                        | `StateFlow<ConnectionState>`               |
| `sendMessage(address, body)`             | Send a DM, returns message ID              |
| `sendGroupMessage(conversationId, body)` | Send to a conversation, returns message ID |
| `isClientReady()`                        | Check if XMTP client is ready              |
| `getUserAddress()`                       | Get the logged-in user's address           |
| `getInboxId()`                           | Get the logged-in user's inbox ID          |

#### IdentityClient

| Method                       | Description                             |
| ---------------------------- | --------------------------------------- |
| `bind()` / `unbind()`        | Bind/unbind the identity service        |
| `awaitConnected()`           | Suspend until connected                 |
| `connectionState`            | `StateFlow<ConnectionState>`            |
| `createIdentity()`           | Create or retrieve an isolated identity |
| `hasIdentity()`              | Check if an identity exists             |
| `getIdentityAddress()`       | Get the isolated identity's address     |
| `getInboxId()`               | Get the isolated identity's inbox ID    |
| `sendMessage(address, body)` | Send from the isolated identity         |

#### Error Handling

All SDK exceptions extend `SdkException`:

* `MessengerNotInstalledException` - Messenger app not found
* `PermissionNotGrantedException` - Required permission not granted
* `ServiceNotConnectedException` - Service not bound (call `bind()` first)
* `RemoteCallException` - IPC call failed

### Requirements

* Android API 28+
* XMTP Messenger app installed on the device

### Gihub Repo Below:

{% embed url="<https://github.com/EthereumPhone/MessengerSDK>" %}


# Setup your dGEN1

All you need to know to start using your dGEN1

## Unbox your dGEN1&#x20;

Inside the the box, you will be greeted by:&#x20;

* Your dGEN1, located inside its leather pouch&#x20;
* A silver colored recovery card. This card allows you to retrieve your digital assets in case of theft or damage of the device. For further information, please consult the [Recovery - dGEN1 Burner Card](/docs/setup-your-dgen1/recovery-dgen1-burner-card)
* USB-C cable with integrated physical switch to disable or enable data transfers&#x20;
* USB-C earbuds. These earbuds lack ANC support which allows users to listen to AM/FM Radio when plugged to the dGEN1&#x20;
* An information leaflet&#x20;
* Stickers!

<img src="/files/uAcCJEzPdBPI2vr2R0hJ" alt="" data-size="original">

## Prepare the Device&#x20;

1. Retrieve the dGEN1 from its box and sleeve,
2. Unpeel the protective spacer on the side of the device
3. Turn your dGEN1 on by pressing the the blue, pill-shaped button on the right side of the device.

## The Setup Process&#x20;

1. Once the boot sequence is complete, you will be greeted with the welcome screen. Press the "GET STARTED" button to initiate the device setup:
   1. (Optional) Connect to a Wi-Fi network.&#x20;

#### Initialize your recovery card

1. Take the recovery card, and press it against the back of the device.
2. Dial your 6-digit pin one prompted.
3. Re-enter the same PIN to confirm it.
4. Press the recovery card against the device to initialize the card.&#x20;

#### Setup manually with NFC

1. Press on "SKIP"
2. When prompted, select the "I UNDERSTAND" option
3. Visit this website on a smartphone with NFC capability and select the "VIEW CARD ADDRESS" option: <https://recovery.freedomfactory.io/>
4. Press the recovery card against the device until a QR code appears
5. Scan the QR code with your dGEN1 to populate the text field
6. Confirm by pressing "NEXT" on the top right&#x20;

#### Import an ENS or eth address

1. Press on "SKIP"
2. When prompted, select the "I UNDERSTAND" option
3. Now insert your ens or / eth-address either via the text field or via QR-Code scanning.
4. Confirm by pressing "NEXT" on the top right

#### (Optional) Create device PIN

1. When prompted, insert your 6-digit pin.
2. Re-Enter the same PIN to confirm it. For security reasons, do not use the same PIN for your device and recovery card

#### Register your fingerprint

1. Tap the circular finger print sensor on the back of your device, until the square is filled To maximize fingerprint recognition, move your finger so that it fills the entire sensor

#### (Optional) generate your XMTP identity&#x20;

XMTP is the official messaging protocol used for the onboard messenger. To use it you first need to enroll your wallet.

For more information, check the [dGEN1 Features](/docs/dgen1-features) section&#x20;

## FAQ&#x20;

#### Where do I manage my private keys?&#x20;

You don't. Keys are generated on-device and are never exposed, not even to us developers. This is by design&#x20;

#### If I can't manage my private keys, how can I recover my funds in case of loss of the device?

The dGEN1 runs a "smart account". Compared to the usual private-key managed accounts, you can add multiple signers to it, meaning that if you lose one signer (in this case, the dGEN1), you can still access all your funds via your recovery option (in this case, the recovery card or recovery address). As of right now you can only have one recovery option, but multiple could be theoretically added in a future update.&#x20;

#### I need to reset my device. Will I lose my funds if I do?&#x20;

No, you can still recovery your funds right here: <https://recovery.freedomfactory.io/> on any device equipped with an NFC reader (i.e. Most smart phones)&#x20;

#### I lost my recovery card. What can I do?

Treat your recovery card like your seed phrase, when you lose it and your dGEN1, just like your seed phrase, consider it gone.


# Recovery - dGEN1 Burner Card

Included in your dGEN1 package, you received a recovery Burner Card, which acts as a recovery mechanism for your dGEN1 wallet.

<figure><img src="/files/wbWt33TW5HaAhs04FsEv" alt="" width="375"><figcaption></figcaption></figure>

In the dGEN1 Onboarding, you will be prompted to tap your burner card on the NFC chip behind the camera. The card prompts the dGEN1 wallet address to be created, and the burner card can be used anytime to recover your funds on the device, just like you would with a seed phrase.

To recover your dGEN1 wallet:

1. Go to [https://recovery.freedomfactory.io](https://recovery.freedomfactory.io/) on your phone browser
2. Press the "CONNECT WITH RECOVERY CARD" button.
3. Select OK when prompted
4. Tap to connect your Burner card
5. When prompted, input the 6 digit pin that you used to secure the card the first time you set it up.\
   BE AWARE: You only 20 attempts in TOTAL, meaning that if you mistyped 5 times, you will only have 15 attempts, even after unlocking the card.
6. You will see a list of devices that have been previously connected to recovery card. Select the device that you wish to recover.
7. Select the network that you wish to inspect.
8. Select the assets (Network currency and ERC20 Tokens) that you wish to recover.
9. (Optional) repeat for the remaining networks.
10. That's it! You've recovered your funds.

***

A few notes on Recovery setup with the NFC Burner Card in the box:

* When you setup with your burner card, tapping on the dGEN1 generates your dGEN1 address, and designates your Burner card as your Backup wallet address.
* Your Burner card address will be different than your dGEN1 address. This is correct. Since the dGEN1 is an Account Abstracted wallet, your Burner acts as a "Seed Phrase" of sorts to recover your assets to your Burner wallet in case of emergency.
* If you want to recover your assets, go to <https://recovery.freedomfactory.io/> and follow the steps with your Burner Card on your phone.
* If you factory-reset your dGEN1, you will create a new address. You can still use your Recovery card as the recovery wallet. In the onboarding you need to click on "SKIP" at the Card setup step, then click "I understand" then "Scan QR Code". Then, on your phone, go to <https://recovery.freedomfactory.io/>, click "View Card Address" and scan your recovery card. You will then be shown a QR Code, scan this QR code using the dGEN1 and you are setup as normal.
* Recovery with ENS and other wallet addresses is coming.


# Operating dGEN1

The dGEN1 is easy to operate if you've had any experience with a iOS or Android device. In this section we will discuss peculiarities that are not found on similar devices

## Navigation

The devices uses a modified gesture-based navigation system. Here is what you need to know:

## Terminal screen

The dGEN1 includes a secondary, smaller display at the bottom of the device, called the **Terminal Screen**. This touch-enabled screen can be used to interact with supported apps. When it’s not actively in use, it shows useful system information.\
Whenever you send a transaction on the dGEN1, the **transaction digest** is displayed on the Terminal Screen. On the right side, you’ll see an **info (ℹ️) icon** — tap it to open a dialog on the main screen with full transaction details.

On the left, you’ll see either:

* a **fingerprint icon**, if you have a fingerprint registered, or
* a **right-pointing arrow**, if you don’t.

To confirm the transaction, either:

* **press the fingerprint sensor** on the back of the device, or
* **swipe from left to right** on the terminal screen.

<div><figure><img src="/files/OY3wryp1BDzszJHAkHyy" alt=""><figcaption></figcaption></figure> <figure><img src="/files/ISmQZV1nhU9MmftdODq3" alt=""><figcaption></figcaption></figure></div>

## Lock-Screen

To dismiss the lock screen, swipe horizontally or in an upward motions. If you have a pin setup, you will prompted to type it out and confirm by pressing the bottom right button. You can skip this by just pressing the finger print sensor, if you have a fingerprint enrolled on the system.

## Home screen navigation

The home screen is divided in 3 tabs:

* Home: All favorite apps
* Index: All apps
* Active: Currently running apps

An arrow on the left of the tab indicates the currently active tab. Change the current tab by pressing on the items.\
\- To add/remove apps to the home tab, just long press the app name.\
\- To search across your apps, press on the search icon on the top right.

## App navigation

While using an app, **swipe right from the left** edge of the screen to go back to the previous screen. To return to the home screen, **swipe left from the right edge** of the screen.

While using official dGEN1, pay attention to the terminal screen as interaction options might be displayed there instead of the primary screen.

<figure><img src="/files/4IfsE6txtVjl70lB4T2E" alt="" width="540"><figcaption></figcaption></figure>

## Status bar & quick settings

To access the status bar, swipe in a downward motion, preferably from the upper edge of the screen. Swipe upwards again to expand the quick setting tiles list. On the bottom right you will be presented with 3 icons:

* Stylus icon: Edit setting tiles
* gear icon: Settings options
* Power icon: Power options

You can summon the status bar by also double tapping on the middle the secondary screen.

![](/files/4XaGjy3ziABuKYKmxYGX)

## LED-Matrix

The led matrix is a 3x3 rgb led array that functions as your heads-up display. It will display notification or context dependent images either as static pixel art or animations.\
You can modify matrix behavior in the Settings app by selecting "Your dGEN1" top level setting, followed by the "LED Matrix" voice.

## Activate AI assistant

At any point, double press the power button summon the AI assist. You can interact with it either via the chat on top of the screen or vocally by holding the red button.\
You can dismiss it by pressing the X icon on the top right.

## Change device system colors

Open the settings app and select the "Your dGEN1" top level setting. Under the "System color" voice, you'll be able to pick between these colors:

* Red
* Green
* Aqua (default)
* Ochre
* Gray

## Miscellaneous actions

**Take a screenshot**: Press the volume down and power button simultaneously.\
**Activate the laser:** Hold the button pressed on the second tap to keep the laser running.


# How to update your dGEN1

Your guide to over-the-air (OTA) updates

We will periodically release new Software updates to ethOS v4 on your dGEN1. These updates will come in the form of an over-the-air or OTA update, meaning that they will be downloaded to your device automatically when you're connected to the wifi, however, you will be able to choose whether to install it or not.

On your device, in the notifications center, you should see this notification:

<p align="center"><img src="/files/9PsCQSuGTO3LBzpIoMZD" alt=""></p>

That notification means the device has downloaded the newest OTA, but has not yet installed it. You can dismiss it if you don't wish to update, but if you want the newest software, click reboot.

***

## Manual System Update Guide

* Open the "SETTINGS" app
* Navigate down until you see the "**System & Updates**" and select it
* Navigate down until you see "**System Updates**" item and select it
* (Optional) Select a **release channel**:
  * Stable (recommended): best for most users
  * Other channels may include features still in development. Proceed at your own risk.
* Tap **Check for updates**.
* A small confirmation message (toast) will appear.
* If an update is found, a **notification** will appear at the top of the screen.
* (optional) Swipe down to expand the status bar and monitor download and installation progress.
* Once the update is finished, restart your device to apply it.

Downloading and applying an update can take up to multiple hours depending on your internet connection. We highly suggest to allow fer extra time for updates to process.&#x20;

The OS checks for new OTA updates once every 6 hours, so you can always be sure to have the latest software versions.

<p align="center"><img src="/files/i4u51Cdz2010ieZ33ahB" alt=""><img src="/files/UDo6TIgjIQLqHIXzxJ3G" alt=""></p>

## FAQ

#### Q: The system is giving me an "update could not be located" error. What should I do?

You're probably on the beta release channel. Currently it's not in use. Switch to Stable or Alpha (not recommended) and try again

#### Q: Do updates take long to install?

Some of the earlier updates where not distributed as differential updates, making them much larger and slower to apply.\
Since we can’t reliably know which version your device is currently running, we always recommend allowing extra time for the update process. If the dGEN1 confirms that the update was applied successfully, you don’t need to wait any longer.


# dGEN1 Features

A short list :)

## Laser

To activate the laser, quickly press the red button twice. Hold the button pressed on the second tap to keep the laser running.

The laser will emit a sound when running. There is a 3% chance for a special sound to trigger on each laser activation. Try it to find out :)

## Local Voice-to-Text

Whenever a text input field is focused, hold the red button to activate voice-to-text. Release the button to insert the transcribed text.

Voice recognition runs entirely on local Whisper models that support multiple languages, so no audio data leaves the device.<br>

## Onboard light clients

To do anything on Ethereum (like checking your balance or sending a transaction), your wallet has to talk to a Node; basically a server that provides blockchain data and sends your transactions to the network.

In theory, anyone can run their own node, but in practice most people can’t (or don’t want to). So wallets usually rely on third-party nodes.

That’s a problem, because most apps trust the node blindly and a bad or manipulated node can:

* Lie about blockchain data (fake balance, fake transaction status, fake “you received assets” notifications)
* Delay or censor transactions (your transaction might “not go through” even though it should)
* Exploit your swaps using MEV / sandwich attacks, which can make you lose money during trades
* Trick you into signing dangerous transactions
* Track your activity and harm your privacy

To defend against this, you can use a light client, which verifies Ethereum data independently, so you can confirm that the information you’re getting from nodes is real and untampered.

On the dGEN1, you can enable the onboard light client anytime via Quick Settings. Currently you can choose between Nimbus and Helios by long pressing the light client item.

## AI Overlay

At any time, press the power button twice in quick succession to summon the onboard AI assistant. If allowed to, It can see what’s currently on your screen and execute commands either through a chat prompt (displayed at the top of the screen) or via voice input by holding the device’s red button.\
With the AI assistant, you can:

* Buy and send crypto from anywhere on the device
* Open and interact with apps
* Get real-time explanations of what’s on your screen
* Ask general questions too, just like a regular AI assistant<br>

## Onboard wallet

Every dGEN1 comes with a "smart wallet" that is generated on the device on first boot and is stored in the TEE (Trusted Secure Environment) of the device. This wallet implementation means that users don't need to worry about their mnemonic phrases or raw private keys and allows for recovery via the provided recovery card.

For a more in-detail explanation of the underlying architecture, proceed to [dGEN1 Wallet Architecture](/build/dgen1-wallet-architecture)

For a more in-detail explanation of all the wallet capability, proceed to [Wallet App](/docs/dgen1-features/wallet-app)

## C.H.A.D. AI Assistant

CHAD (Computer Helping A Degen) is the main app to converse with your AI assistant. We built all our apps to expose additional features to this AI agent, enabling a lot of cool features like:

* Send, trade and swap assets directly
* Open and interact with applications
* Get an overview of your current assets and positions
* Crawl your wallet history

## Paymaster system

The "Paymaster" system is a functionality that was first introduced with the advent of smart wallets. This system allows for a variety of features that are not possible on traditional keypair wallets.\
On the dGEN1, the paymaster is currently used to:<br>

* Interact with chains without needing to have eth on each of them
* Unlock better models for the CHAD AI assistant
* To learn more about how to manage and top up your paymaster, check [Paymaster Guide](/docs/paymaster-guide)<br>

## XMTP Messenger

The dGEN1 comes with it's own private messenger powered by the XMTP protocol. Every dGEN1 user has the option to enroll their XMTP identity on first boot or when starting the app for the first time and chat across multiple clients like the base app or other dGEN1 users.

The device is able serve notifications of new messages without needing to rely on third party push notification services, preserving users anonymity.

## App Directory

This app contains a list of curated apps submitted by the wider community and approved by us. This app contains both PWA (Web apps repacked in a way that they run as if they were native apps on the dGEN1) or real native apps.

We are working hard to expand the list of apps. If you wish for your app to be featured, feel free to contact us over our social medias (X: [@FreedomFactory](https://x.com/FreedomFactory)) or via email: <hi@freedomfactory.io>

## Token launcher

The dGEN1 includes a built-in token launchpad that lets users create and deploy tokens easily via Clanker on the base chain.

When launching a token, creators allocate at least 5% of the token’s initial supply to the dGEN1 airdrop pool, rewarding the broader dGEN1 community. The launchpad also tracks tokens deployed by other dGEN1 users, making it easy to discover, ape, and trade community tokens directly from the device.

## Airdrop pool

Each dGEN1 is automatically enrolled to our official airdrop pool. The amount is split evenly across every device.

If you want to view what's currently on the pool or donate to it, you can do so via the official address: 0x476e48a832C593EE17317c008870a8aa4E649610. Currently the airdrop pool is supported on mainnet, base and polygon.

For a more in-depth overview of the airdrop pool, check [Airdrop pool info](/docs/claim-your-airdrops/airdrop-pool-info)<br>

## Modified Firefox Browser

The dGEN1 ships with a modified Firefox browser that lets the integrated wallet connect to dApps directly from within the browser.

This enables the system to detect when a dApp is being used and, if the user chooses, automatically log them in. Most modern dApps will recognize the dGEN1 as a supported wallet option.

If a dApp doesn’t list the dGEN1 explicitly, users can simply select “Connect via MetaMask” as a fallback; The dGEN1 intercepts the MetaMask connection request and injects the integrated wallet instead. For more info, check [Connect your dGEN1 wallet to apps](/docs/browsing-onchain/connect-your-dgen1-wallet-to-apps)


# Wallet App

![](/files/aLjtAa7GGnII0KmTlFt0)

## Asset overview

Assets are visually represented as cards in a scrollable list. On this view, assets that share a representation on multiple chains (for instance USDC lives both on mainnet as well as Optimism, Polygon, Arbitrum, etc...) will be grouped together, meaning that having 10 USDC on Base and 10 USDC Polygon will be displayed as a single card with a balance of 20 USDC. If you want to see the exact breakdown, press on the "SEND" button on the bottom right of the card.

## Send screen

Here you will be able to initiate a transaction. You will be prompted to input a target address, the amount and optionally the choice of which chain to utilize (This only comes into play for tokens that have a representation on multiple chains like USDC).

You can switch between an raw token amount and USD representation by pressing on the switch right above the send amount. Right next to it, you will also see the option to select the maximal amount of the token.

By pressing the icon next to the "TARGET ADDRESS" title, you'll be able to select a contract with a valid ens or eth address from your saved contacts. You can manage the contacts from the "CONTACTS" app.

On the secondary screen, you will see two intractable options: "SCAN QR" (see \[\[#Payment Request URL (EIP-681)]]) and "SEND TXN"

## Payment Request URL (EIP-681)

This standard allows for a unified way to generate payment requests. The dGEN1 is able to decode such requests, and auto fill the send parameters automatically on the send screen of the wallet app by scanning a EIP-681 QR-codes

## Paymaster screen

Here you can see your current paymaster balance and the top up options. The top-up is powered by daimo, which allows users to utilize any token that they currently own for the payment. Learn more on this guide: [Paymaster Guide](/docs/paymaster-guide)&#x20;


# Claim your Airdrops

For this guide you'll need:

* A dGEN1 on the latest patch
* A internet connection
* 0.001 eth on mainnet
* (Optional) enrolled fingerprint

## Guide

1. Make sure that you're on one of the latest updates. If you don't know how to do that, follow the [How to update your dGEN1](/docs/setup-your-dgen1/how-to-update-your-dgen1) guide or watch this video: <https://youtube.com/shorts/up1SD9Tn8Lc?si=l92_j0zpNaau02TQ>
2. On the home screen, search for the "Token claim app" and select it
3. If you're opening the app for the first time, you will be greeted with an information dialog. Press the "LET'S GO!" button to continue
4. Select the network that you wish to claim from Currently:&#x20;
   1. Ethereum&#x20;
   2. Base&#x20;
   3. Polygon
5. (Optional) Explore the current airdrop breakdown
6. Start the claim process by pressing the "CLAIM" item located on the terminal screen![](/files/JQjYX4FAxK5dewZh5TBa)
7. If you enrolled a fingerprint, press that finger on the fingerprint sensor to execute it, otherwise, perform a swipe motion on the terminal screen to confirm.
8. Your tokens should now be claimed. repeat the steps for the remaining networks if you want to.
9. (Optinal) Go on the wallet to see your new Tokens. Execution can take some time, so don't be afraid if it takes a couple of minutes

### FAQ:

#### Q: I'm getting a "409" Error message when claiming. How can I fix this?

It's a known issue. If you're experiencing this, please fill out this [form](https://forms.gle/Ly2ohgdsDPm6XDUu9) and we will re-enable your access manually. This might take a bit as we need to make sure that it's not a "double dipping" attempt.

#### Q: Is there a way to see what's the current state of the pool?

Yes, We created a dashboard for it. You can check at <https://airdrop.freedomfactory.io/>

#### Q: When are each of the airdrop Epochs?

Epoch 1 is open from December 8th, 2025 to March 1st, 2026. Subsequent Epochs will be 1 month long and include all unclaimed tokens from the last epoch split equally between all 6000 devices!


# Airdrop pool info

#### **General information**

The dGEN1 airdrop pool is a contract by the address 0x476e48a832C593EE17317c008870a8aa4E649610 that lives on:

* Mainnet
* Arbitrum
* Base
* Optimism
* Polygon

#### **How is the airdrop fueled**

The airdrop is fueled by individual contributions and tokens created by the token launcher app

#### **What mechanism is used for airdrop eligibility**

We utilize device identifiers to prove that a device is a genuine dGEN1.

#### **Airdrop distribution**

The airdrop is split equally across all manufactured dGEN1 devices. Here is an example of how that would look like:

Let's assume:

* **15,000** dGEN1 devices exist
* The Epoch 1 airdrop pool contains:
  * $250,000 worth of Token 1
  * $500,000 worth of Token 2

Then each device is entitled to:

* Token 1 per device: **$250,000 / 15,000 = $16.66**
* Token 2 per device: **$500,000 / 15,000 = $33.33**

So on Epoch 1, each dGEN1 holder can redeem:

* **16.66 Token 1**
* **33.33 Token 2**

If only **50% of holders redeem** in Epoch 1, then:

* **half of the pool remains unclaimed**
  * Token 1 remaining: **$125,000**
  * Token 2 remaining: **$250,000**

Assuming no additional funds are added during Epoch 1, the remaining tokens are **rolled over into Epoch 2** and again split equally across all 15,000 devices.

So in Epoch 2, each device is eligible for:

* Token 1 per device: **$125,000 / 15,000 = $8.33**
* Token 2 per device: **$250,000 / 15,000 = $16.66**

So in Epoch 2, each dGEN1 holder can redeem:

* **8.33 Token 1**
* **16.66 Token 2**

**Epoch duration**

The first epoch is 3 months long until March 1st, 2026. All subsequent epochs will be 1 month long. New epochs are triggered by the core team.

## FAQ

### Q: Do I need ETH to claim?

The claim is sponsored on Base and Polygon. For mainnet, you indeed need at least 0.001 ETH to execute the transaction.

### Q: I missed out on a Epoch. Is there a way to still claim my portion?

No

### Q: I'm trying to claim, but I get a "409: Already claimed error", even though I didn't. What should I do?

It's a known issue. If you're experiencing this, please fill out this [form](https://forms.gle/Ly2ohgdsDPm6XDUu9) and we will re-enable your access.

### Q: What's currently on the pool?

You can check at <https://airdrop.freedomfactory.io/>


# Launch a Token

For this guide you'll need:

* A dGEN1 on the latest patch
* A internet connection
* Either:
  * some ETH on Base,&#x20;
  * or a some balance on your dGEN1 paymaster (More info on [Paymaster Guide](/docs/paymaster-guide))
* (Optional) enrolled fingerprint<br>

## Launch Guide

* Open the "Token Launcher" app
* Press the "Launch button" on the top right corner
* Provide a:
  * Token name
  * Ticker name
  * Token image
* Percentage that goes to the shared [Airdrop pool info](/docs/claim-your-airdrops/airdrop-pool-info) (5% minimum)
* Confirm by pressing the "Launch" button on the terminal screen
* Sign the transaction (More info on [Browsing Onchain](/docs/browsing-onchain))<br>

## Discover Guide

* Open the "Token Launcher" app
* You'll be presented a list of tokens, feel free to explore it
* (Optional) Press anywhere on a token to get more Info about it\
  Press buy to initiate the swap
* Select the wished token to use for the swap, as well as the amount
* Confirm swap by pressing "BUY" on the terminal screen
* Sign the transaction (More info on [Browsing Onchain](/docs/browsing-onchain))<br>

## FAQ

#### Q: What launched is used for the token launcher

We use clanker.world. It's a Token launchpad on the Base network

#### Q: Where do the Tokens in the discovery page come from?

All tokens from the discovery page are tokens that have been created by other dGEN1 users. We do not index other tokens.

#### Q: Can I opt-out from the airdrop pool share?

No

#### Q: Are tokens launched fairly?

Yes. Once a token hits clanker, everyone can buy and trade it.

#### Q: What are the tokenomics of launching a token?

Every swap of the tokens incur a 1.2% swap fee. The 1.2% is split as of the following:

* 0.2% to the Clanker launchpad
* 0.4% to the token creator
* 0.3% to the shared airdrop pool
* 0.3% to the dGEN1 team


# Paymaster Guide

The paymaster is a system that sponsors transactions on your behalf on all supported networks, that makes it possible to interact with the network without needing to have the network's balance.

On the dGEN1, it can also be leveraged to upgrade your CHAD AI assistant to use more powerful models.

## Check paymaster balance

1. Open the wallet app
2. Tap **GAS** in the bottom-right corner.

## Top up your paymaster

For this guide you'll need:

* Your dGEN1
* A stable internet connection
* At least 10 dollars worth of tokens<br>

<div align="left" data-full-width="false"><figure><img src="/files/BpY9gNsjGkeJTzANF1Qx" alt="" width="375"><figcaption></figcaption></figure></div>

**Steps**:

1. Open the wallet app
2. Tap **GAS** in the bottom-right corner.
3. At the bottom of the screen, choose a **top-up** **option**.
4. Confirm by tapping **TOP UP** on the terminal screen.
5. You’ll be redirected to the browser. On the new page, tap **PAY WITH CRYPTO**.
6. A dialog will appear showing assets that can cover the selected top-up amount. Pick one.
7. To view all eligible tokens, tap **More available**.
8. Sign the transaction (See [Browsing Onchain](/docs/browsing-onchain) for details.)<br>

## FAQ

#### Q: Can I see my paymaster usage history?

Not yet, but it's in the works

#### Q: Can I go lower than 10 dollars?

Not at the moment. That's a limitation of the system.

#### Q: What powers the top up?

Daimo Pay


# NFTs on the dGEN1

## View your NFTs

* Go to the official gallery app
* Select the "NFT" album
* Press the refresh button on the terminal screen to fetch any missing NFT
* To see all the hidden NFTs, press the eyeball icon
* (Optional) to hide/unide NFTs, long press the NFT and select the "HIDE/UNHIDE" option<br>

## Mint an NFT from the dGEN1

For this guide you'll need:

* A dGEN1 on the latest patch
* A internet connection
* Either:
  * some ETH on Base, or a some
  * balance on your dGEN1 paymaster (More info on [Paymaster Guide](/docs/paymaster-guide))
* (Optional) enrolled fingerprint

## Steps

* Open the official camera app
* On the terminal screen, swipe either left or right until the "MINT" mode is visible
* Take a picture
* If you're happy with the preview, confirm by tapping on the terminal screen, otherwise dismiss by pressing the X icon on the top right corner
* Provide a title and description for your NFT
* Confirm by pressing on the terminal screen
* Sing the transaction (More info on [Browsing Onchain](/docs/browsing-onchain))
* Your NFT should now be viewable on the Gallery, under the NFT

## FAQ

#### Q: On which networks are NFTs minted?

Currently you can only mint on Base

#### Q: I don't see my NFT even after refreshing several times. What should I do?

Please send us an email at <hi@freedomfactory.io> stating as such. Please provide detailed info and your dGEN1 wallet address so that we can test and find what's happening. You can also reach out to our [Discord Server](https://discord.com/invite/T6fqFzdC)

#### Q: Some NFTs do not display the correct content / no content at all. What should I do?

Please send us an email at <hi@freedomfactory.io> stating as such. Please provide detailed info and your dGEN1 wallet address so that we can test and find what's happening. You can also reach out to our [Discord Server](https://discord.com/invite/T6fqFzdC)


# Browsing Onchain

On the dGEN1, you mainly interact via dApps either via the modified Firefox browser that ships on the device, or via the dedicated dApps found on the dGEN1 app directory.

Regardless of the option that you choose, you'll have have to interact with the terminal screen.

When initiating any transaction or signature, the **terminal screen** will display a **transaction digest** for review.

What you see depends on whether a fingerprint is enrolled:

* **Fingerprint enrolled:**  ![](/files/OY3wryp1BDzszJHAkHyy)
* **No fingerprint enrolled:**  ![](/files/ISmQZV1nhU9MmftdODq3)

Regardless of your configuration, you can always press the **info (ℹ️) button** on the right side of the terminal screen to open a **detailed breakdown** of the transaction you’re about to sign, for example: ![](/files/dMvq1Ul8VwiUGToc4sDv)

To **decline** the transaction, press **DECLINE** on the bottom-left of the main screen. To **sign** the transaction:

* use your **fingerprint sensor** (if enrolled), or
* **swipe horizontally** on the terminal screen.

### Browser considerations

When using the modified Firefox browser that ships with the device, dGEN1 will automatically detect dApps and connect the built-in wallet for you.

If auto-connect doesn’t trigger, simply connect manually like you would on any other device:

* Select **dGEN1 Wallet** if it’s listed.
* If it isn’t listed, select **MetaMask** as a fallback. In both cases, the system will intercept the request and route it through the **dGEN1 wallet**.

***

### Use your dGEN1 on other systems

The dGEN1 can sign and execute transactions on any dApp that supports **QR-code wallet login**. This allows you to interact with dApps opened on other devices (such as a laptop or another phone) while securely signing transactions directly on your dGEN1.

#### Step by step guide

1. Open the app that you want to use and press on the connect button
2. A dialog window should now pop up, that provides login option like this
   1. <img src="/files/GlVOnAw05kaPjpbhcR1c" alt="" data-size="original">
3. Press on "WalletConnect" option:
   1. &#x20;![](/files/X1lipNjvF08FfjdFMv31)
4. A QR-code will now be presented to you, like this:
   1. &#x20;![](/files/x3Rey2xurSt2vcpgWIVr)
5. On your dGEN1, go to the quick settings and select the "Connect wallet" option:
   1. ![](/files/ldXuOqwRFufi3q6VrPJt)
6. Scan the QR-Code
7. To confirm the connection, press open with wallet
   1. &#x20; ![](/files/WyrGXkaYSr6WkInLo2kP)
8. You should now see that the wallet is connected to the website.&#x20;
   1. ![](/files/73ZAFBgNAcbUV02brVoy)&#x20;
   2. You can also monitor and end the connection on the device in the notification bar&#x20;

      ![](/files/ZmNZHK6xsuPX2jfzzbuw)
9. Interact with the app as you would normally would from your device.
10. Once you initiate a transaction, the tx will be piped to the dGEN1. Causing the tx info to appear on the main screen
    1. &#x20;![](/files/z6cfBzZld8unyMp4IDFE)
11. Sign the transaction as shown
    1. &#x20;![](/files/dMvq1Ul8VwiUGToc4sDv)


# Connect your dGEN1 wallet to apps

On App Store apps or through Firefox Browser

One of the big pain points using crypto apps on mobile, is the inability to connect directly thru your browser or within a mobile app. High fees and restrictions on Apple and Google keep it that way.

On dGEN1, it's different. We've modified apps and Firefox browser to connect directly to our onboard wallet, and it's super simple.

Within apps from the App Store, the dGEN1 wallet should automatically connect. If it doesn't or gets disconnected, select one of the following options:

<p align="center"><img src="/files/WyDRSmxEYDt1d45bb5fG" alt=""></p>

* ethOS wallet
* Browser wallet
* Detected wallet
* Metamask
* Wallet Connect, then Metamask

On Firefox, the wallet should not auto-connect, however your options to connect the wallet remain the same as above.&#x20;

You may need to sign a message to connect to a few sites, but that's as easy as tapping the fingerprint sensor on the back.

Now you can connect your onboard smart wallet to apps directly thru the browser, or apps directly and eliminate all the app-switching and failed transactions.


# Changelog

Every ota is going to bring new bug fixes, updates and features. We're just getting started.

July 1st, 2026 - ethOS v4.4

Wallet Manager: Added apechain and Monad chain&#x20;

Andyclaw:

* Local LLM (BYO-GGUF)
* Custom providers support: (Ollama/LM Studio/vLLM/llama.cpp/LocalAI)
* Silent-billing fix: heartbeat defaults off, executive summary gating fixed
* Hardened backup/restore: atomic per-section restore, back up AI-created skills, rebuild FTS
* Fixed provider mislabel (OpenRouter/ethOS errors no longer labeled "Anthropic")
* Fixed subagent model-id override on the ToolSearch path

Launcher: Added the UI for the new andyclaw settings&#x20;

System: Added secondary screen dimming & fixed the pin skip in the setup wizard

March 27th, 2026 - ethOS v4.2

Major Andyclaw changes:

* Your agent persistent on the OS level, visually alive
* Tool and model smart routing for 75% token reduction
* Parallel and sub-agent task handling
* New models (Gemini, Brok, Deepseek, Qwen) and ability to add your own keys
* Shortcuts and tool expansion actions
* Andyclaw can create it's own tools and skills
* Added soul.md&#x20;
* Upgrades and cost-reductions for virtual screen usage
* Agent wallet, paymaster and transaction visibility
* Executive summaries when new notifications come in
* Smarter voice input and keyboard inputs

Other updates:

* Fixed app duplication in launcher
* Fixed virtual display settings and terminal glitches
* Setup wizard updates
* Light client UI update

March 6th, 2026 - ethOS v4.1.8

* Added agent virtual screen so agent can interact with apps
* Telegram bot access
* Added option to insert own API key for LLMs
* Paymaster app separation in home list&#x20;
* Paymaster refunds unused gas estimation overages
* Update security settings:
  * Blocks requests for reading env files or secrets on termux
  * Prompt injection protection
  * High risk clawhub skill protection
* Clawhub installation and management fixes

February 25th, 2026 - ethOS v4.1.7

* Added Andyclaw - Opensource kotlin-android Openclaw agent
  * Andyclaw on dGEN1 enshrines privacy: every prompt is end-to-end encrypted to a TEE by Tinfoil.sh that runs the model, preventing data harvesting by any misaligned model or company.
  * Your agent gets its own wallet sub-account, send it out to do tasks without worrying about unauthorized spend.
  * Your paymaster account automatically pays for your API calls, so you don't have to worry about model routing.
  * The project is also now completely open-sourced on github
* NFTs display as cards in the wallet app
* XMTP group messaging updates

January 7th, 2026 - ethOS v4.1.4

* Swapping in CHAD
* Push to voice red button
* Double click on blue button now opens CHAD
* Turn off internet access for an app
* New tab in Launcher, Home tab. You can add apps via long-press
* DAppStore now stability upgrades
* ETHGlobal eSIM in DAppStore, with Quick install
* Group messaging in XMTP
* Reactions in XMTP

December 8th, 2025 - ethOS v4.1.3

* Token Claim app
* XTP message notifications
* Connect wallet fixes
* Apks in App Directory
* New apps in the directory: Trackgoodai, Trebleswap, Baseflip, Tickr, Bitrefill, Rumour by altlayer, Session messenger, Signal messenger

November 14th, 2025 - ethOS v4.1.2

* Connect your dGEN1 to desktop apps
* Native swapping within the Wallet app
* Pull down to refresh the dGEN1 App Directory to see new apps added
* Select a contact in the wallet to send to
* Hold to copy messages from XMTP
* BSC Chain & Avalanche support in the wallet for assets
* Token Launcher updates

November 3rd, 2025 - ethOS v4.1.1

Updates and bug fixes:

* dGEN1 Launched tokens Discovery
* Swap Discovered Tokens
* Send XMTP messages to BaseNames
* Turn LED Matrix on/off
* Increase dGEN1 corner sensitivity for touch
* Refresh NFTs in Gallery
* Favorite photos folder
* 24 hour clock enabling
* Battery percentage enabling

September 26th, 2025 - ethOS v4.0.13\
\
Updates and bug fixes:

* Fix some bugs with PWA apps manager
* Terminal notification icon tap for notifications
* Add Hardware attestation app
* Crash report button - reports to team
* Messenger updates
* Wallet gas paymaster updates&#x20;
* Firefox native browser updates
* Fix to Stopwatch
* Gallery Update for NFTs

September 1st, 2025\
dGEN1 with ethOS v4.0

The initial release includes all our basic native apps to get started:

* Alarm
* Calculator
* Camera
* CHAD
* Claim
* Contacts
* dGEN App Store
* Firefox
* Gallery
* Notes
* Play Store (Google)
* Settings
* Stopwatch
* Timer
* Token Launcher
* Wallet
* XMTP Messenger


