Kuira SDK — Integration Guide¶
Alpha — 0.1.0-alpha05
Build a Midnight zero-knowledge dApp on Android by adding one dependency, declaring your own passkey domain, and hosting one tiny JSON file on it. That's the whole on-ramp.
This guide walks the recipe end-to-end. Two public reference apps consume the SDK exactly as described below — clone either and read along:
- Kuira Starter — the
minimal counter dApp. Sigil identity + wallet + a one-circuit Compact contract.
Clone it, set your
applicationId+rpId, run. (Also a GitHub template.) - BBoard — a shared on-chain bulletin board (post / take-down) showing the deploy → call → read flow.
The snippets below are extracted from those apps; when in doubt, the linked source is the ground truth.
What the SDK gives you¶
Adding the one line below brings:
- Sigil identity — a passkey-derived DID (
did:key+ Ed25519). One biometric, no recovery phrase, no maintainer dependency. Backed up to Google Block Store, encrypted with a key you can't read. - Wallet — Midnight HD wallet (unshielded + shielded NIGHT, Dust gas), with live balance, send, receive, and a drop-in Compose wallet panel if you want it.
- Contract surface — deploy / call / read state on any
.compactcontract; ZK proof generation runs on-device (no proof-server hop required). - Indexer + chain client — block / state / event subscription, backed by Midnight's official indexer.
- App-state cloud backup — your dApp's per-user data rides the sigil's Block Store backup automatically. The backup blob is PRF-encrypted client-side before Google's Block Store touches it.
Drop in the prebuilt UI, build your own on the same contracts, or go fully headless — see Choose your level below.
1. Prerequisites¶
| Android Gradle Plugin | ≥ 8.13 (matches the SDK build) |
| Kotlin | ≥ 2.3 |
compileSdk |
35+ |
minSdk |
30 — required (the SDK uses keystore APIs added in API 30: biometric ⊕ device-credential) |
| Hilt + KSP | Required — the SDK is Hilt-first; your app applies both |
| A web domain you control | Required for the passkey relying party (GitHub Pages is fine for dev) |
| Your app's signing-cert SHA-256 | Needed for assetlinks.json (step 4) |
Full toolchain (JDK 17, Android Studio, Docker, Node 20+, the mn CLI, compactc)
— see Prerequisites.
2. Add the dependency — one line¶
Choose your level¶
Kuira meets you at the level you want — and the dependency you pick reflects it:
- Drop in the prebuilt UI —
io.github.kuiralabs:dapp-ui. Ready-made, themeable Compose components — the wallet pill, the sigil pill, and the Settings panel (network, recovery phrase, lock, sign-out), all in onePanelBar— wired to SDK state out of the box with safe security defaults (FLAG_SECURE, biometric gates, an auto-clearing clipboard). Restyle with a colors object; you write no wallet logic. The fastest path — see Set up Sigil identity. - Build your own experience — also
dapp-ui(its ViewModels) ormidnight-sdk. Render your own screens on the same public contracts the pills are built on —WalletRecovery,WalletPanelViewModel,SigilPanelViewModel/SigilSession,MidnightContract,MidnightSdk. The SDK keeps the hard parts (crypto, secure vault, on-device proving, sync, session-lock); the screens are yours. Every bundled component is just the first consumer of a contract you can call directly — e.g. Reveal & restore the recovery phrase. - Go headless —
io.github.kuiralabs:midnight-sdk. The full wallet, identity, and contract surface with no Compose pulled in; you build the UI layer top-to-bottom. See § 7 Going headless.
Theming today, more tuning coming
The prebuilt components expose theming now plus a small set of behavioral knobs; richer per-component tuning hooks are on the roadmap. Until a knob exists, the build-your-own path gives full control of that surface — the contracts carry no UI policy.
Maven Central is already in every Android project's defaults, so there's no repo
config to add. In app/build.gradle.kts:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose") // only if your own UI is Compose
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
android {
namespace = "com.example.mydapp"
compileSdk = 36
defaultConfig {
applicationId = "com.example.mydapp"
minSdk = 30
targetSdk = 36
}
}
dependencies {
// ── Pick ONE Kuira entry (see "Choose your level" above) ──
//
// Prebuilt pills + the ViewModels behind them (Compose). Drop the wallet
// and sigil panels in as-is, OR build your own UI on the same contracts.
implementation("io.github.kuiralabs:dapp-ui:0.1.0-alpha05")
//
// OR — headless core, no Compose pulled in. For dApps building the whole
// UI layer top-to-bottom.
// implementation("io.github.kuiralabs:midnight-sdk:0.1.0-alpha05")
// Hilt processor — required, the SDK is Hilt-first
ksp("com.google.dagger:hilt-compiler:2.58")
// your own app code, your own Compose deps, etc.
}
That single Kuira line brings the whole consumer surface — passkey, sigil,
wallet, ZK proving, contract deploy/call, indexer, panel UI (if you picked
dapp-ui) — onto your compile classpath transitively. No per-module
redeclaration.
3. Declare YOUR passkey domain¶
The SDK provides no default PasskeyConfig — that's deliberate. A passkey
is bound to a domain, and that domain must be yours. If the SDK baked in a
default, every consumer would silently route through the maintainer's domain
and PRF would only work after the maintainer added them to a maintainer-hosted
assetlinks.json — i.e. the "open" SDK would actually be permissioned.
Add a tiny Hilt module (anywhere in your di/):
package com.example.mydapp.di
import com.midnight.kuira.core.identity.passkey.PasskeyConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object IdentityConfigModule {
@Provides
@Singleton
fun providePasskeyConfig() = PasskeyConfig(
rpId = "mydapp.example.com", // YOUR domain (matches assetlinks.json below)
rpName = "My dApp", // shown in the biometric prompt
)
}
If you forget this, you'll get a clear Dagger missing-binding error at build time. That's intentional — it forces you to declare your domain before shipping.
4. Host assetlinks.json on YOUR domain¶
Android's CredentialManager checks Digital Asset Links to verify your app
may use passkeys for your domain. Place this file at:
https://<your-domain>/.well-known/assetlinks.json
[{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.mydapp",
"sha256_cert_fingerprints": ["AB:CD:EF:01:23:45:67:89:..."]
}
}]
Get your cert SHA-256:
keytool -list -v -keystore <your-keystore> -alias <your-alias> | grep SHA256
~/.android/debug.keystore (alias
androiddebugkey, password android).
Without this file (or with a wrong package name / SHA-256), the biometric prompt fails and PRF can't derive your sigil. This is a one-time host; updates are needed only when you change signing certs.
5. Develop against a localnet¶
You don't need testnet tokens or a deployed contract to build. The UNDEPLOYED
network is a full Midnight stack — node + indexer + proof server — running
locally in Docker: instant, free, and ephemeral (reset it any time). It's the
recommended loop for day-to-day development; switch to PREPROD only when you want
the hosted chain.
PreProd's first sync replays from genesis — set up cloud backup first
Localnet is instant. PreProd is a long-lived chain with a large history, so
a wallet's first sync there replays from genesis — minutes, not seconds. To
make it a fast delta instead, set up
cloud backup before you switch
to PreProd. It needs a one-time Google OAuth client (your app's package +
signing SHA-1); without it, cloud sync silently fails
(UNREGISTERED_ON_API_CONSOLE) and PreProd keeps replaying from genesis on
every fresh sync — the wallet works, it's just slow.
5a. Start the localnet — the Midnight Wallet CLI (recommended)¶
The recommended way to run and fund a localnet is the Midnight Wallet CLI
(mn), installed from npm as midnight-wallet-cli. It wraps the Docker stack
and wallet funding behind a few commands:
# one-time — needs Docker running + Node 20+
npm install -g midnight-wallet-cli # installs `midnight` (alias `mn`)
mn localnet up # start node + indexer + proof server (Docker)
mn localnet status # check it's healthy
# … develop …
mn localnet down # tear it down
Tested localnet stack — pin the node to 0.22.5
Tested localnet stack: midnight-node:0.22.5 (on-chain Compact runtime
0.16.0). Older nodes (≤0.22.3) ship runtime 0.15.0 and reject a contract
compiled for 0.16.0. mn pulls the node image; if you run Docker by hand,
pin the node tag to 0.22.5.
Windows — mn localnet up can time out
On Windows, mn localnet up can fail with spawnSync … cmd.exe ETIMEDOUT.
Bring the stack up directly:
docker compose -f ~/.midnight/localnet/compose.yml up -d
Fund the wallet your app shows (copy its address from the wallet panel's receive screen):
mn airdrop 10000 --wallet mn_addr_undeployed1… --network undeployed # NIGHT for fees
Dust (gas) for the app's embedded wallet is enabled in-app — tap Register dust in the wallet panel.
mn dust register targets a named CLI wallet, not the app's wallet
mn dust register --wallet <name> needs a named wallet from
mn wallet generate; it cannot target the app's embedded wallet address.
Localnet is ephemeral —
mn localnet down(or a Docker restart) wipes all state, including funded balances. Re-airdrop after each freshup.
5b. Let the app reach the localnet¶
The SDK already maps UNDEPLOYED to the right host per device type
(NetworkConfig):
- Emulator —
10.0.2.2(the emulator's alias for your machine'slocalhost). Nothing to do.
!!! note "Emulator ABI — x86_64 works on 0.1.0-alpha04+"
x86_64 (Intel) and arm64 emulators both work on 0.1.0-alpha04+ (the
native lib ships arm64-v8a and x86_64). On alpha03 and earlier the lib
is arm64-only — an x86_64 AVD fails at launch with
java.lang.UnsatisfiedLinkError; use alpha04+ or an arm64 AVD.
- Physical device — 127.0.0.1, so the three localnet ports must be
tunnelled over the debug bridge. The SDK ships a Gradle plugin that does this
automatically on every installDebug — apply it once, no configuration:
// app/build.gradle.kts
plugins {
id("io.github.kuiralabs.localnet") version "<sdk-version>"
}
It registers an adbReverseLocalnet task wired ahead of installDebug, so
deploying to a connected physical device forwards ports 9944 (node RPC),
8088 (indexer), and 6300 (proof server) for you. Emulators use 10.0.2.2
and are skipped automatically.
The example apps apply it exactly this way — see them for a working reference: BBoard and the starter.
Required for physical devices — the app can't reach the localnet without
these three port forwards (the plugin runs them for you on installDebug; run
them by hand if you're not using the plugin):
adb reverse tcp:9944 tcp:9944 # node RPC
adb reverse tcp:8088 tcp:8088 # indexer
adb reverse tcp:6300 tcp:6300 # proof server
No "Install via USB" on the phone? Some devices hide it. Build and push
the APK over adb instead:
./gradlew :app:assembleDebug
adb push app/build/outputs/apk/debug/app-debug.apk /data/local/tmp/app.apk
adb shell pm install -r /data/local/tmp/app.apk
5c. Allow cleartext to the localnet (debug only)¶
Production hits HTTPS. Localnet is plain HTTP, so declare cleartext in a debug-only manifest — release builds stay HTTPS-clean:
<!-- app/src/debug/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:usesCleartextTraffic="true" />
</manifest>
Why debug-only: the SDK's release variants are HTTPS-clean and don't grant any cleartext allowance, so each app declares its own in a debug-only manifest — release stays cleartext-free.
6. Minimal "Hello World" — deploy + call¶
Your Application is @HiltAndroidApp, and your activity is a Hilt'd
FragmentActivity — AppCompatActivity is the usual choice, since the
panel hosts a biometric prompt that needs a FragmentActivity host:
@HiltAndroidApp
class MyApp : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Box(modifier = Modifier.fillMaxSize()) {
MyDappScreen() // your UI underneath
// Drop-in combined sigil + wallet UI: a pair of draggable
// floating chips that own their own state. The wallet pill
// builds the SDK (one biometric) the first time it shows, and
// owns network selection — its pick drives the SDK's durable
// NetworkPreferenceStore, which your screen can follow.
PanelBar(
floating = true,
network = selectedNetwork,
onNetworkChange = ::selectNetwork,
)
}
}
}
}
This is the canonical integration every reference app uses — see
Set up Sigil identity. (The lower-level
SigilStatusPanel / WalletStatusPanel are still available if you want to
place each panel inline yourself.)
Your dApp logic gets the SDK from the injected MidnightSdkProvider — observe
sdkProvider.sdk (a StateFlow<MidnightSdk?>, non-null once the wallet panel
has bootstrapped) or suspend on awaitSdk():
@HiltViewModel
class MyDappViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val sdkProvider: MidnightSdkProvider,
) : ViewModel() {
fun deployAndCall() = viewModelScope.launch {
val sdk = sdkProvider.awaitSdk() // waits for the panel to bootstrap it
// 1. Install your contract's proving keys — required before the first
// deploy/call (see §6.1), or proving fails.
ProvingKeyManager(context).installCircuitKeysFromAssets()
// 2. Build a write-capable handle. create() takes the SDK's config;
// contractJs is an InputStream from assets; deploy/call need the
// coin public key + each circuit's verifier-key bytes.
val verifier = context.assets
.open("keys/myCircuit.verifier").use { it.readBytes() }
val contract = MidnightContract.create(sdk.config) {
name = "mycontract"
contractJs = context.assets.open("runtime/mycontract-contract.js")
coinPublicKey = sdk.coinPublicKey
circuitVerifierKeys = mapOf("myCircuit" to verifier)
}
// 3. Deploy, then call a circuit. `call(name, …args)` is stringly-typed;
// the contract Gradle plugin also generates a typed `MyContract`
// facade so `MyContract(contract).myCircuit()` is checked at compile
// time (see the deploy-and-call recipe).
val address = contract.deploy().contractAddress
contract.call("myCircuit")
// 4. Read typed ledger state (lossless — no cell-hex parsing).
val readOnly = MidnightContract.create(sdk.config) {
contractJs = context.assets.open("runtime/mycontract-contract.js")
this.address = address
}
val count = MyContract(readOnly).ledger().count // typed facade; or untyped: readOnly.ledger().getUint64("count")
}
}
6.1 Install your contract's proving keys (required)¶
ZK proofs run on the device, so every circuit your contract calls needs its
proving keys + a BLS parameter set present before the first deploy/call —
otherwise the call fails at the proving step. Ship them in assets/ — sync them
in with the io.github.kuiralabs.contract Gradle plugin
(0.1.0-alpha05), the recommended path, which stages the
contract runtime and circuit keys into assets/ for you. Without the plugin, use
the hand-rolled Copy task shown in the
deploy-and-call recipe. Then
install once at runtime — it's idempotent, so call it before each action:
import com.midnight.kuira.core.compact.proving.ProvingKeyManager
// Discovers the circuit keys bundled in assets and stages them
// where the on-device prover looks.
ProvingKeyManager(context).installCircuitKeysFromAssets()
Imports — use the core.compact.proving package
ProvingKeyManager and ProvingMode both live in
com.midnight.kuira.core.compact.proving. There is a same-named
ProvingMode typealias in com.midnight.kuira.core.crypto.proving — do
not import that one; always reference
com.midnight.kuira.core.compact.proving.ProvingMode.
The shared wallet proving keys ship the full BLS parameter set
(bls_midnight_2p5–2p15), so a small contract circuit (e.g. a counter that
needs 2p5) finds the size it requires from the wallet keys — you do not
bundle BLS per contract. Just make sure the wallet keys are provisioned: they
are, during SDK bootstrap, and you can ship them in the APK to skip the first-run
download with kuiraContract { bundleWalletKeys = true } (see
On-device proving). The
Kuira Starter's
CounterContract shows the full pattern end-to-end.
For a larger, multi-step contract (commit / reveal, witness packing, indexer-state polling, retry, force-resync), the BBoard reference app shows the deploy → call → read flow end-to-end.
7. Going headless (no panel)¶
If you don't want the Compose wallet panel, swap the dep:
implementation("io.github.kuiralabs:midnight-sdk:0.1.0-alpha05") // no dapp-ui
You still own bootstrap + sigil forging — MidnightSdkProvider.ensureSdk(...)
throws SigilRequiredException until a sigil exists. Forge it through
PasskeyManager directly, or use the SigilSession helper.
midnight-sdk does not pull Compose — the headless entry stays small.
8. Shipping a release build¶
The starter ships isMinifyEnabled = false so newcomers see exactly what compiles. For a production release, turn on R8 and publish an App Bundle — both matter a lot, because the SDK carries a native on-device ZK-proving core.
android {
buildTypes {
release {
isMinifyEnabled = true
// The SDK ships its own consumer ProGuard rules — AGP applies them automatically.
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
}
./gradlew :app:bundleRelease # an .aab for Play — not a universal .apk
What it costs in size¶
Measured on the counter starter (one small circuit):
| Build | Size | Why |
|---|---|---|
| Universal APK, minify off | ~152 MB | both ABIs + an unstripped dex — worst case, never shipped |
| Universal APK, R8 on | ~93 MB | R8 strips the dex from ~65 MB → ~6 MB (most was SDK code your app never calls) |
| Per-device (App Bundle + R8) | ~60 MB | Play delivers one ABI per device, not both |
A real release lands around ~60 MB per device — comfortably inside Google Play's 200 MB app-bundle limit. The floor is the native proving library (~20–24 MB of Rust — the BLS/halo2 prover + QuickJS): R8 can't shrink it, because it's the cost of proving on the device rather than on a server. Bundling the wallet proving keys (bundleWalletKeys = true) adds ~33 MB on top; the default runtime download keeps them out of the binary.
Two settings do almost all the work
isMinifyEnabled = true (kills the dex bloat) + bundleRelease (one ABI per device). Everything after that is single-digit MB.
Common pitfalls¶
| Symptom | Fix |
|---|---|
| Dagger: "PasskeyConfig cannot be provided without an @Provides-annotated method" | Step 3 — declare your own PasskeyConfig module. |
| Runtime: "CLEARTEXT communication to 10.0.2.2 not permitted by network security policy" | Step 5 — add the debug manifest. |
| Biometric prompt fails / PRF returns null | Step 4 — assetlinks.json missing, wrong package_name, or wrong cert SHA-256. |
| Contract call fails at the proving step / "circuit keys not found" / "BLS params" | §6.1 — install your contract's proving keys before the first deploy/call (and bundle the right BLS set for small circuits). |
| App balance stays at 0 after an airdrop | The wallet's background subscription is live; check adb logcat for indexer connectivity. On localnet, state is ephemeral — restarting the localnet wipes funds. |
| Balance stays 0 / "Loading…" on a physical device | Run the three adb reverse commands (9944 node, 8088 indexer, 6300 proof server) or apply the io.github.kuiralabs.localnet plugin (§5b). |
java.lang.UnsatisfiedLinkError at launch on an x86_64 emulator |
The native lib is arm64-only before 0.1.0-alpha04. Upgrade to alpha04+ (ships x86_64) or use an arm64 AVD. |
Custom error: 168 or a dust-fee / time-to-dismiss failure on dust registration or the first tx |
Fixed in SDK 0.1.0-alpha04 — upgrade. An unexpected Custom error: N not in the known set usually means a client↔node ledger-version mismatch — confirm your node is 0.22.5. |
Could not resolve io.github.kuiralabs:… |
Ensure mavenCentral() is in your repositories and the version is actually published — check central.sonatype.com/artifact/io.github.kuiralabs/dapp-ui. |
Known limitations (alpha)¶
The alpha ships with one known dependency you should be aware of. It isn't a blocker for building, but it shapes what "alpha" means in practice and will evolve as Midnight matures.
Proving infrastructure downloads from a Midnight dev URL¶
On first launch the SDK fetches Midnight's protocol-level proving assets
— the universal BLS parameters and the wallet's shielded-spend / Dust circuit
keys — from
https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com.
This is the same bucket Midnight's own tooling uses; it's Midnight's
bucket, not the SDK maintainer's, and the URL is labeled -dev-. There is
no production SLA on it.
What this means in practice:
- If Midnight retires, renames, or restricts that bucket, every alpha dApp briefly can't generate proofs until the SDK is updated.
- The right party to publish a production URL is Midnight (the asset owner), not the SDK maintainer. So the alpha documents this dependency rather than self-hosting.
Your own .compact contract's proving keys are NOT affected by this. Those
download from a URL you supply when you call
ProvingKeyManager.downloadCircuitKeys(baseUrl = …, …) — e.g. BBoard hosts
BBoard's keys. You always own your contract's keys. This
limitation is only about the protocol-wide keys that every Midnight dApp
shares.
How it will evolve. When Midnight publishes a production URL — or, if
that's not in their immediate roadmap, when the SDK mirrors these files under
a kuiralabs-controlled URL with a version pin (ProvingKeyManager.CURRENT_VERSION
tracks the protocol version, currently 9) — the SDK swaps the constant and
re-publishes. The expected migration cost for a consumer is a single SDK
version bump; no app-side code change.
See also¶
- Kuira Starter — clone-and-run minimal dApp (also a GitHub template).
- BBoard — on-chain bulletin board; the deploy → call → read flow end-to-end.
- Home — install instructions and the full module list.
- Security — threat model, vulnerability reporting, signature verification.
- Maven Central —
io.github.kuiralabs— every published artifact (binary AAR, sources jar, javadoc jar, POM, PGP signature).