Flutter / Dart Shield SDK
The akedly_shield package provides Proof-of-Work solving and Turnstile captcha for Akedly V1.2 in Flutter and Dart. Supports background computation via Dart Isolates and a built-in Turnstile widget.
Distributed via GitHub
akedly_shield is not published on pub.dev yet. Install it as a Git dependency from the official repository.
Installation
Add the Git dependency to your pubspec.yaml:
dependencies:
akedly_shield:
git:
url: https://github.com/Akedly-Org/akedly-shield-dart.git
ref: main
Pin to a specific release by passing a tag instead of main:
dependencies:
akedly_shield:
git:
url: https://github.com/Akedly-Org/akedly-shield-dart.git
ref: v1.0.0
Then fetch the dependency:
flutter pub get
Not on pub.dev
The Flutter SDK is distributed as a GitHub repository only. flutter pub add akedly_shield will not work -- use the Git dependency syntax above.
Quick Start
Never ship your API key to a Flutter app
Flutter builds are shippable binaries — anything embedded becomes public. Keep APIKey and pipelineID on your backend, expose a thin proxy (/auth/akedly/challenge, /auth/akedly/send, /auth/akedly/verify), and have Flutter call the proxy. The Backend tab below shows the minimal Node.js server; the Flutter 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 Flutter 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 Dart — 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.
Import the package and use solvePowInIsolate for Flutter apps (recommended -- runs in a background Isolate to keep the UI responsive).
For Turnstile, add the AkedlyTurnstile widget to your widget tree and receive the token via the onToken callback. The PoW solver and Turnstile widget run on the device; only non-sensitive proofs travel to your backend.
Quick Start
// Express proxy — put this on your server, not in the Flutter 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
solvePow(challenge, difficulty)
Asynchronous PoW solver that yields to the event loop every 10,000 iterations. Runs on the main thread.
- Name
challenge- Type
- String
- Description
64-character hex string from the server challenge response.
- Name
difficulty- Type
- int
- Description
Number of leading hex zeros required.
Returns Future<int> (the nonce).
solvePowInIsolate(challenge, difficulty)
Executes the PoW solver in a separate Dart Isolate. Recommended for Flutter apps to prevent UI thread blocking.
- Name
challenge- Type
- String
- Description
64-character hex string from the server challenge response.
- Name
difficulty- Type
- int
- Description
Number of leading hex zeros required.
Returns Future<int> (the nonce).
AkedlyTurnstile Widget
Flutter widget that loads a Cloudflare Turnstile bridge page via WebView for token generation.
- Name
siteKey- Type
- String (required)
- Description
Cloudflare Turnstile site key from the challenge response.
- Name
onToken- Type
- Function(String) (required)
- Description
Callback that receives the Turnstile token when verification completes.
- Name
onError- Type
- Function(String)?
- Description
Error callback. Called if Turnstile verification fails.
- Name
bridgeDomain- Type
- String?
- Description
Bridge page domain. Defaults to
turnstile.akedly.io.
Complete Example
Full Flutter widget implementing the V1.2 OTP flow against a backend proxy (see the Quick Start for the Node.js backend):
Flutter Widget
import 'package:flutter/material.dart';
import 'package:akedly_shield/akedly_shield.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
// Your backend base URL — keeps APIKey and pipelineID server-side.
const backendUrl = 'https://yourapp.com';
class OTPScreen extends StatefulWidget {
final String phoneNumber;
const OTPScreen({
super.key,
required this.phoneNumber,
});
@override
State<OTPScreen> createState() => _OTPScreenState();
}
class _OTPScreenState extends State<OTPScreen> {
String? _transactionReqID;
String _otp = '';
bool _loading = false;
String? _error;
String? _turnstileSiteKey;
Map<String, dynamic>? _challengeData;
Future<void> _sendOTP({String? turnstileToken}) async {
setState(() { _loading = true; _error = null; });
try {
// 1. Get challenge from YOUR backend
final challengeRes = await http.get(
Uri.parse('$backendUrl/auth/akedly/challenge'),
);
final data = jsonDecode(challengeRes.body)['data'];
// Check if Turnstile is needed and we don't have a token yet
if (data['turnstile']?['required'] == true && turnstileToken == null) {
setState(() {
_challengeData = data;
_turnstileSiteKey = data['turnstile']['siteKey'];
_loading = false;
});
return; // Wait for Turnstile token via widget callback
}
// 2. Solve PoW in background isolate
final nonce = await solvePowInIsolate(
data['challenge'] as String,
data['difficulty'] as int,
);
// 3. Send proof via YOUR backend
final sendRes = await http.post(
Uri.parse('$backendUrl/auth/akedly/send'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'phoneNumber': widget.phoneNumber,
'powSolution': {
'challengeToken': data['challengeToken'],
'nonce': nonce,
},
if (turnstileToken != null) 'turnstileToken': turnstileToken,
}),
);
final result = jsonDecode(sendRes.body);
setState(() { _transactionReqID = result['data']['transactionReqID']; });
} catch (e) {
setState(() { _error = e.toString(); });
} finally {
setState(() { _loading = false; });
}
}
Future<void> _verifyOTP() async {
setState(() { _loading = true; _error = null; });
try {
final res = await http.post(
Uri.parse('$backendUrl/auth/akedly/verify'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'transactionReqID': _transactionReqID, 'otp': _otp}),
);
final result = jsonDecode(res.body);
if (result['status'] == 'success') {
if (mounted) Navigator.of(context).pop(true);
} else {
setState(() { _error = 'Invalid OTP'; });
}
} catch (e) {
setState(() { _error = e.toString(); });
} finally {
setState(() { _loading = false; });
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Verify Phone')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_turnstileSiteKey != null && _transactionReqID == null)
SizedBox(
height: 1,
child: AkedlyTurnstile(
siteKey: _turnstileSiteKey!,
onToken: (token) => _sendOTP(turnstileToken: token),
onError: (err) => setState(() { _error = err; }),
),
),
if (_transactionReqID == null) ...[
Text('Send OTP to ${widget.phoneNumber}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loading ? null : () => _sendOTP(),
child: Text(_loading ? 'Sending...' : 'Send OTP'),
),
] else ...[
TextField(
onChanged: (v) => setState(() { _otp = v; }),
decoration: const InputDecoration(labelText: 'Enter OTP'),
keyboardType: TextInputType.number,
maxLength: 6,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loading || _otp.length < 6 ? null : _verifyOTP,
child: Text(_loading ? 'Verifying...' : 'Verify'),
),
],
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(_error!, style: const TextStyle(color: Colors.red)),
),
],
),
),
);
}
}
Platform Support
The Flutter Shield SDK works on Android, iOS, Web, and Desktop (macOS, Windows, Linux). solvePowInIsolate is available on all platforms. AkedlyTurnstile requires WebView support (Android, iOS, Web).