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.

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"))
}

Quick Start

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

KOTLIN
// 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.

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

KOTLIN
OTPScreen.kt
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

KOTLIN
OTPActivity.kt
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()
    }
}

Was this page helpful?