Android / Kotlin Shield SDK
The Akedly Shield SDK for Android provides coroutine-based Proof-of-Work solving and Turnstile token retrieval for Akedly V1.2. Includes a sync variant for Java interop.
Distributed via GitHub
The Android SDK is not published to Maven Central yet. Install it from GitHub using JitPack, or clone the repository as a local module.
Installation
Option 1: JitPack (recommended)
Add JitPack to your settings.gradle.kts (or settings.gradle):
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
maven { url = uri("https://jitpack.io") }
}
}
Then add the dependency to your app's build.gradle.kts:
dependencies {
implementation("com.github.Akedly-Org:akedly-shield-kotlin:1.0.0")
}
Replace 1.0.0 with main-SNAPSHOT to track the latest commit, or use a specific tag or commit hash for a pinned build.
Option 2: Local module
Clone the repository alongside your project and include it as a local Gradle module:
git clone https://github.com/Akedly-Org/akedly-shield-kotlin.git
In settings.gradle.kts:
include(":akedly-shield")
project(":akedly-shield").projectDir = file("../akedly-shield-kotlin/shield")
Then in your app's build.gradle.kts:
dependencies {
implementation(project(":akedly-shield"))
}
Not on Maven Central
The Android SDK is distributed as a GitHub repository only. The previously documented com.akedly:shield Maven coordinate is not published -- use JitPack or a local module instead.
Quick Start
Never ship your API key to an Android app
APKs and AABs can be unpacked — anything compiled in is public. Keep APIKey and pipelineID on your backend and expose a thin proxy (/auth/akedly/challenge, /auth/akedly/send, /auth/akedly/verify) for Android to call. The Backend tab below shows the minimal Node.js server; the Kotlin tab shows the matching client.
Optional: per-end-user-IP rate limiting
Per-IP rate limiting is opt-in. If you want it, forward the end user's IP via the x-end-user-ip header — the IP that hit your backend from the Android device, not your server's IP. The Express example below uses req.ip, which only returns the real client IP after app.set('trust proxy', 1) is configured for your reverse proxy (Cloudflare, AWS ALB, Nginx, Fly.io). Do not read the device's public IP from within the Android app — clients cannot reliably know their own public IP and can lie. Skip this entirely if you don't need per-IP limiting. See the V1.2 API reference for Next.js, Flask, and PHP extraction patterns.
Use solvePow within a coroutine scope. It runs on Dispatchers.Default and yields every 10,000 iterations. For Turnstile, use AkedlyTurnstile which creates an invisible WebView. PoW and Turnstile run on-device; only non-sensitive proofs travel to your backend.
Quick Start
// Express proxy — deploy this on your server, not inside the Android app.
import express from 'express';
const app = express();
app.use(express.json());
// Optional — only needed if you want per-end-user-IP rate limiting.
// Drop this line and the x-end-user-ip header below if you don't.
// Makes req.ip the real client IP behind a reverse proxy (1 = trust first hop).
app.set('trust proxy', 1);
app.get('/auth/akedly/challenge', async (_req, res) => {
const r = await fetch(
`https://api.akedly.io/api/v1.2/transactions/challenge` +
`?APIKey=${process.env.AKEDLY_API_KEY}` +
`&pipelineID=${process.env.AKEDLY_PIPELINE_ID}`
);
res.status(r.status).json(await r.json());
});
app.post('/auth/akedly/send', async (req, res) => {
const { phoneNumber, powSolution, turnstileToken } = req.body;
const r = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-end-user-ip': req.ip,
},
body: JSON.stringify({
APIKey: process.env.AKEDLY_API_KEY,
pipelineID: process.env.AKEDLY_PIPELINE_ID,
verificationAddress: { phoneNumber },
powSolution,
turnstileToken,
}),
});
res.status(r.status).json(await r.json());
});
app.post('/auth/akedly/verify', async (req, res) => {
const { transactionReqID, otp } = req.body;
const r = await fetch('https://api.akedly.io/api/v1.2/transactions/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transactionReqID, otp }),
});
res.status(r.status).json(await r.json());
});
API Methods
suspend fun solvePow(challenge: String, difficulty: Int): Int
Coroutine-based PoW solver. Yields every 10,000 iterations to prevent blocking. Use within any coroutine scope.
- Name
challenge- Type
- String
- Description
64-character hex string from the server.
- Name
difficulty- Type
- Int
- Description
Number of leading hex zeros required.
Returns Int (the nonce).
fun solvePowSync(challenge: String, difficulty: Int): Int
Synchronous blocking solver for Java interop or background thread execution.
- Name
challenge- Type
- String
- Description
64-character hex string from the server.
- Name
difficulty- Type
- Int
- Description
Number of leading hex zeros required.
Returns Int (the nonce). Blocks until found.
Java Interop
solvePowSync is designed for Java code that cannot use coroutines. Always call on a background thread.
AkedlyTurnstile(context, bridgeDomain?)
Creates an invisible WebView to retrieve Cloudflare Turnstile tokens.
- Name
context- Type
- Context
- Description
Android application or activity context.
- Name
bridgeDomain- Type
- String?
- Description
Bridge page domain. Defaults to
turnstile.akedly.io.
- Name
getToken(siteKey: String)- Type
- suspend method
- Description
Loads the Turnstile bridge page and returns the token. Must be called from the Main dispatcher.
Jetpack Compose Example
Compose
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.akedly.shield.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.net.URL
// Calls YOUR backend proxy; see Quick Start for the Node.js server.
const val BACKEND_URL = "https://yourapp.com"
@Composable
fun OTPScreen(
phoneNumber: String,
onSuccess: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var transactionReqID by remember { mutableStateOf<String?>(null) }
var otp by remember { mutableStateOf("") }
var loading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (transactionId == null) {
Text("Send OTP to $phoneNumber")
Button(
onClick = {
scope.launch {
loading = true
error = null
try {
val json = withContext(Dispatchers.IO) {
URL("$BACKEND_URL/auth/akedly/challenge").readText()
}
val data = JSONObject(json).getJSONObject("data")
val nonce = solvePow(
data.getString("challenge"),
data.getInt("difficulty")
)
var turnstileToken: String? = null
val ts = data.optJSONObject("turnstile")
if (ts?.optBoolean("required") == true) {
turnstileToken = withContext(Dispatchers.Main) {
AkedlyTurnstile(context).getToken(ts.getString("siteKey"))
}
}
val body = JSONObject().apply {
put("phoneNumber", phoneNumber)
put("powSolution", JSONObject().apply {
put("challengeToken", data.getString("challengeToken"))
put("nonce", nonce)
})
turnstileToken?.let { put("turnstileToken", it) }
}
// POST body to "$BACKEND_URL/auth/akedly/send"
// transactionReqID = result.data.transactionReqID
} catch (e: Exception) {
error = e.message
}
loading = false
}
},
enabled = !loading
) {
Text(if (loading) "Sending..." else "Send OTP")
}
} else {
OutlinedTextField(
value = otp,
onValueChange = { if (it.length <= 6) otp = it },
label = { Text("Enter OTP") }
)
Button(
onClick = {
scope.launch {
loading = true
error = null
// POST to "$BACKEND_URL/auth/akedly/verify" with { transactionReqID, otp }
// On success: onSuccess()
loading = false
}
},
enabled = !loading && otp.length >= 6
) {
Text(if (loading) "Verifying..." else "Verify")
}
}
error?.let {
Text(it, color = MaterialTheme.colorScheme.error)
}
}
}
XML / Activity Example
Activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.akedly.shield.*
import kotlinx.coroutines.*
// Calls YOUR backend proxy; see Quick Start for the Node.js server.
const val BACKEND_URL = "https://yourapp.com"
class OTPActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var transactionReqID: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_otp)
// sendButton.setOnClickListener { sendOTP() }
// verifyButton.setOnClickListener { verifyOTP() }
}
private fun sendOTP() {
scope.launch {
val json = withContext(Dispatchers.IO) {
java.net.URL("$BACKEND_URL/auth/akedly/challenge").readText()
}
val data = org.json.JSONObject(json).getJSONObject("data")
val nonce = solvePow(
data.getString("challenge"),
data.getInt("difficulty")
)
var turnstileToken: String? = null
val ts = data.optJSONObject("turnstile")
if (ts?.optBoolean("required") == true) {
turnstileToken = AkedlyTurnstile(this@OTPActivity)
.getToken(ts.getString("siteKey"))
}
// POST to "$BACKEND_URL/auth/akedly/send" with { phoneNumber, powSolution, turnstileToken }
// Save transactionReqID from response: result.data.transactionReqID
}
}
private fun verifyOTP() {
val otp = "" // Get from EditText
scope.launch {
// POST to "$BACKEND_URL/auth/akedly/verify" with { transactionReqID, otp }
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}