What it does
aegis_bip instruments a mobile app so it can tell a genuine, present user apart from
someone else operating the session — a stolen device, a remote-access scam, a bot — without ever
asking the user to do anything extra. It passively observes how the app is used (touch pressure and
rhythm, typing cadence, navigation pattern, how long screens take to react to, device motion, session
timing, environment and security signals) and turns that into a continuous behavioural profile. At
transaction time, a single checkpoint() call scores the current session against that
profile and returns a verdict your app acts on: allow, step up, or block.
Backend contract is framework-agnostic. Every client SDK — Flutter and native
Android today, React Native once built — talks the same event schema and checkpoint API described in
DATA_DICTIONARY.md. Switching the SDK above shows how each platform's client code
differs; the signals collected and the verdicts returned are identical.
Quick start
import 'package:aegis_bip/aegis_bip.dart';
await AegisBIP.init(AegisConfig(
clientId: 'YOUR_CLIENT_ID',
backendUrl: 'https://your-backend.example.com',
checkpointUrl: 'https://your-backend.example.com/checkpoint',
useProtobuf: false,
));
AegisBip.init(this, AegisConfig(
clientId = "YOUR_CLIENT_ID",
clientVersion = "1.0.0",
backendUrl = "https://your-backend.example.com/v1/sessions",
checkpointUrl = "https://your-backend.example.com/v1/checkpoint",
authToken = BuildConfig.AEGIS_TOKEN,
))
AegisBip.attachToApplication(this) // Category A + C, one call
import { AegisBip, AegisBipCaptureRoot } from 'react-native-aegis-bip';
await AegisBip.init({
clientId: 'YOUR_CLIENT_ID',
clientVersion: '1.0.0',
backendUrl: 'https://your-backend.example.com/v1/sessions',
checkpointUrl: 'https://your-backend.example.com/v1/checkpoint',
authToken: Config.AEGIS_TOKEN,
});
// Category A + C, one wrap
return <AegisBipCaptureRoot><YourAppNavigator /></AegisBipCaptureRoot>;
Signal categories
Nine categories of passive signal feed the risk model. All nine apply regardless of which client SDK collects them.
| Cat | Category | What it captures |
|---|---|---|
| A | Touch | Pressure, contact size, gesture velocity and curvature |
| B | Keystroke | Inter-key timing, dwell time, correction rate |
| C | Navigation | Screen sequence, dwell per screen, back-navigation rate |
| D | Session timing | Login latency, session duration, time-of-day pattern |
| E | Motion | Accelerometer / gyroscope signature while holding the device |
| F | Cognitive | Hesitation before high-risk actions (button press latency) |
| G | Environment | Locale, timezone, network type, screen metrics |
| H | Security | Auth method used, credential entry pattern, biometric availability |
| I | Device state | Battery, orientation, accessibility services active |
Add the dependency
Distributed privately via git ref — not published to pub.dev.
dependencies:
aegis_bip:
git:
url: https://gitlab.funtech.dev/bip/mobile-sdk.git
ref: v1.1.0
flutter pub get
Pin the ref. Track a tag, not a branch — ref: v1.1.0 not
ref: main — so a SDK release can't change your app's behaviour without a deliberate bump.
Or use the prebuilt package archive — for offline use, a CI mirror, or as the tree you push to your own private git tag: ↓ aegis-bip-flutter-1.1.0.tar.gz (297 KB) · ↓ API docs (706 KB) · verify against SHA256SUMS.txt.
dependencies:
aegis_bip:
path: vendor/aegis_bip # extract the archive here
Add the dependency
Distributed privately via a Gradle source dependency — the Gradle-native equivalent of pubspec's git-ref dependency. No Maven registry involved.
sourceControl {
gitRepository(java.net.URI("https://gitlab.funtech.dev/bip/android-sdk.git")) {
producesModule("com.funkash.bip:bip-android")
}
}
dependencies {
implementation("com.funkash.bip:bip-android") {
version { branch = "v0.1.0" }
}
}
Pin the tag. Track a release tag, not a branch — v0.1.0 not
main — so an SDK release can't change your app's behaviour without a deliberate bump.
Same discipline the Flutter SDK's git-ref dependency requires.
Or use the prebuilt AAR — for offline use or a CI mirror, no Gradle source dependency required: ↓ aegis-bip-android-1.1.0.aar (841 KB) · verify against SHA256SUMS.txt.
dependencies {
implementation(files("libs/aegis-bip-android-1.1.0.aar"))
// AAR doesn't bundle transitive deps — also add:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:<version>")
implementation("com.squareup.okhttp3:okhttp:<version>")
implementation("com.google.protobuf:protobuf-kotlin:4.28.2")
}
Add the dependency
Distributed privately as a git-ref npm dependency — not on the npm registry, same no-registry model as both other SDKs. TurboModule-based (New Architecture only, matching React Native 0.76+'s default).
{
"dependencies": {
"react-native-aegis-bip": "git+https://gitlab.funtech.dev/bip/react-native-sdk.git#v0.1.0"
}
}
It also depends on two well-known community packages for native device/sensor access — the
same reuse-over-reinvention tradeoff the Flutter SDK makes with sensors_plus/
device_info_plus:
npm install @react-native-async-storage/async-storage react-native-device-info
cd ios && pod install
Pin the tag. Track a release tag, not a branch — v0.1.0 not
main — so an SDK release can't change your app's behaviour without a deliberate bump.
Same discipline both other SDKs' dependency declarations require.
Or use the prebuilt tarball — for offline use or a CI mirror, no git access required: ↓ react-native-aegis-bip-0.1.0.tgz (159 KB) · verify against SHA256SUMS.txt.
npm install ./react-native-aegis-bip-0.1.0.tgz
cd ios && pod install
Platform support
Coverage varies by OS capability — motion sensors and touch pressure aren't available everywhere.
Platform support
Android only — this SDK doesn't target other OSes, so there's no cross-platform matrix the way the Flutter SDK has one. What varies here is signal-category coverage, not OS.
Signal-category coverage
| Cat | Category | Status |
|---|---|---|
| A | Touch | Full — Window.Callback interception |
| B | Keystroke | Full — TextWatcher |
| C | Navigation | Activity-level automatic; per-destination needs a manual hook for single-Activity/Compose apps |
| D | Session timing | Full |
| E | Motion | Full — SensorManager |
| F | Cognitive | Full |
| G | Environment | Partial — carrier/roaming/brightness wired; network type, VPN, battery, location not yet |
| H | Security | Full — including auth timing and screen-capture/remote-access detection |
| I | Device state | Full |
| J | Composite | Full |
Platform support
New Architecture (TurboModules) only — matches React Native 0.76+'s default; RN 0.82 removes the legacy bridge entirely, so there's nothing to configure either way.
Signal-category coverage
| Cat | Category | Status |
|---|---|---|
| A | Touch | Full — passive bubble-phase touch events, never the responder system |
| B | Keystroke | Full — TextInput length-delta tracking; dwell time unmeasurable on a soft keyboard, same gap every SDK has |
| C | Navigation | Manual recordScreenVisit() API, no hard dependency on any nav library |
| D | Session timing | Full |
| E | Motion | Full — native TurboModule streaming (accelerometer/gyroscope), not a community package |
| F | Cognitive | Full |
| G | Environment | Partial — carrier/roaming/brightness wired; network type, VPN, battery, location not yet |
| H | Security | Full on Android; iOS has no accessibility/remote-control introspection API and hard-codes is_roaming false — same known gap as the Flutter SDK's iOS plugin |
| I | Device state | Full |
| J | Composite | Full |
A newcomer's walk-through
Every screen and code excerpt below is pulled from the demo app (example/lib/main.dart,
"BIP Demo" powered by Funkash). Follow it top to bottom: it's the order a first-time integrator would
actually wire things up in, and by the end it touches all nine signal categories.
App starts — wire the SDK once
no screen · wiringawait AegisBIP.init(AegisConfig(
clientId: TenantConfig.clientId,
backendUrl: TenantConfig.baseUrl,
checkpointUrl: TenantConfig.baseUrl + '/checkpoint',
useProtobuf: false,
));
MaterialApp(
navigatorObservers: [_aegisObserver], // = AegisBIP.instance.navigatorObserver
builder: (context, child) => AegisTouchDetector(
sessionManager: AegisBIP.instance.sessionManager,
child: child!,
),
);
Consent, then login
Before anything is collected, the app asks for consent. Only on accept does a session start.
bip.grantConsent();
bip.startSession(
subjectId: pseudonymousSubjectId(identity, salt: _kSubjectSalt),
userEmail: email,
);
// reported: AuthPromptShownEvent, CredentialEntryEvent, AuthCompletedEvent
Home — moving around the app
Landing on the dashboard tags the current task and the whole body sits under the touch detector wired in step 1 — no extra code needed on this screen.
bip.setTask(BipTask.browse);
return Scaffold(
body: AegisTouchDetector(
sessionManager: AegisBIP.instance.sessionManager,
child: CustomScrollView(/* dashboard content */),
),
);
Filling the transfer form
Every field the user types into is wrapped so keystroke cadence is captured while they fill it in.
bip.setTask(BipTask.transfer);
AegisTextField(
sessionManager: AegisBIP.instance.sessionManager,
controller: _recipientCtrl,
decoration: const InputDecoration(labelText: 'Recipient account or phone'),
);
AegisTextField(
sessionManager: AegisBIP.instance.sessionManager,
controller: _amountCtrl,
decoration: const InputDecoration(labelText: 'Amount (₦)'),
);
Confirm — the checkpoint contract
same screen as step 4Tapping send is timed (hesitation is itself a signal), then the transaction is described to the SDK and scored before anything happens.
AegisTimedAction(
onPressed: _onSendTapped,
child: IconButton(icon: const Icon(Icons.send), onPressed: _onSendTapped),
);
bip.setTransactionContext(amount: amount, recipient: recipient);
final score = await bip.checkpoint(
reason: CheckpointReason.transferInitiated,
);
The verdict — allow, or stop the transfer here
The score decides everything that happens next. On block or stepUp
the transfer is hard-stopped right here — the dialog on the left is exactly what
_showRiskAlert shows, word for word, and the function returns before the PIN
screen is ever reached. Only a clean allow continues.
final blocked = score?.shouldBlock == true;
final stepUp = score?.requiresStepUp == true;
if (blocked || stepUp) {
_showRiskAlert(
title: blocked ? 'Transfer Blocked'
: 'Transfer Halted — Verification Required',
body: blocked
? 'Irregular behaviour detected (risk ${(score!.score * 100).toStringAsFixed(0)}%). '
'This transfer has been STOPPED and flagged to your account manager.'
: 'Irregular activity detected (risk ${(score!.score * 100).toStringAsFixed(0)}%). '
'This transfer is halted and cannot proceed until your identity is verified.',
isBlock: true, // both are a hard stop
);
return; // STOP — never falls through to the PIN screen
}
_sent = true;
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (_) => PinScreen(/* clean allow continues here */),
));
void _showRiskAlert({required String title, required String body, required bool isBlock}) {
final cs = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
icon: Icon(isBlock ? Icons.block : Icons.warning_amber_rounded,
color: isBlock ? cs.error : Colors.orange, size: 36),
title: Text(title),
content: Text(body),
actions: [
FilledButton(
onPressed: () => Navigator.pop(ctx),
style: isBlock ? FilledButton.styleFrom(backgroundColor: cs.error) : null,
child: const Text('Understood'),
),
],
),
);
}
Logout — end the session
same home screen as step 3Ending the session is explicit — don't rely on app termination to close it out.
AegisBIP.instance.endSession();
await FirebaseAuth.instance.signOut();
Navigator.pushAndRemoveUntil(
context, MaterialPageRoute(builder: (_) => const LoginScreen()), (_) => false,
);
Signal coverage across this flow
| Cat | Category | Activated where |
|---|---|---|
| A | Touch | Steps 1 (detector wired), 3 (dashboard interaction) |
| B | Keystroke | Step 4 (transfer form fields) |
| C | Navigation | Steps 1 (observer wired), 3 (screen transitions) |
| D | Session timing | Steps 2 (session start), 7 (session end) |
| E | Motion | Passive from step 1 — sampled continuously once the session starts |
| F | Cognitive | Step 5 (send-button hesitation) |
| G | Environment | Passive from step 1 — sampled at session start |
| H | Security | Step 2 (consent + auth events) |
| I | Device state | Passive from step 1 — sampled continuously once the session starts |
A newcomer's walk-through
Pulled from the :demo module (com.funkash.bip.demo) in the
android-sdk repo. Follow it top to bottom: it's the order a first-time integrator would
actually wire things up in, and by the end it touches all nine signal categories.
App starts — wire the SDK once
no screen · wiringAegisBip.init(this, AegisConfig(
clientId = DemoConfig.CLIENT_ID,
backendUrl = DemoConfig.BACKEND_URL,
checkpointUrl = DemoConfig.CHECKPOINT_URL,
authToken = DemoConfig.AUTH_TOKEN,
))
AegisBip.attachToApplication(this) // Category A + C, one call
Consent, then login
no screen · code only// consent dialog "Accept" button
AegisBip.grantConsent()
// on login tap
AegisBip.report(AegisEvent.AuthPromptShownEvent(AuthMethod.PASSWORD))
val success = username.isNotBlank() && password.isNotBlank()
AegisBip.report(AegisEvent.AuthCompletedEvent(AuthMethod.PASSWORD, success, latencyMs))
AegisBip.startSession(subjectId = pseudonymousSubjectId(username, DemoConfig.SUBJECT_SALT))
AegisBip.setTask(BipTask.LOGIN)
Home — moving around the app
no screen · code onlyAegisBip.setTask(BipTask.BROWSE)
// touch (A) + navigation (C) already active — no other code on this screen
Filling the transfer form
no screen · code onlyAegisBip.setTask(BipTask.TRANSFER)
AegisBip.watchKeystrokes(binding.recipientField)
AegisBip.watchKeystrokes(binding.amountField)
Confirm — the checkpoint contract
no screen · code onlyAegisBip.timedClick(binding.sendButton) { onSendTapped() }
AegisBip.setTransactionContext(
counterpartyAccountHash = pseudonymousSubjectId(recipient, DemoConfig.SUBJECT_SALT),
channel = "bank_transfer", amountNative = amount, currencyCode = "NGN",
)
val verdict = AegisBip.checkpoint(CheckpointReason.TRANSFER_INITIATED)
The verdict — allow, or stop the transfer here
no screen · code onlywhen {
verdict == null -> proceed() // FAIL OPEN — our outage, never the user's
verdict.shouldBlock -> showRiskAlert("Transfer Blocked", ...)
verdict.requiresStepUp -> showRiskAlert("Transfer Halted — Verification Required", ...)
else -> proceed() // clean allow
}
Logout — end the session
no screen · code onlyAegisBip.endSession() // transmits the session payload
A newcomer's walk-through
Pulled from the example/ app in the react-native-sdk repo, verified end-to-end on
both Android and iOS. Follow it top to bottom: it's the order a first-time integrator would
actually wire things up in, and by the end it touches all nine signal categories.
App starts — wire the SDK once
no screen · wiringawait AegisBip.init({
clientId: DemoConfig.CLIENT_ID,
clientVersion: DemoConfig.CLIENT_VERSION,
backendUrl: DemoConfig.BACKEND_URL,
checkpointUrl: DemoConfig.CHECKPOINT_URL,
authToken: DemoConfig.AUTH_TOKEN,
debugMode: __DEV__,
});
// Category A + C, one wrap
return <AegisBipCaptureRoot><SafeAreaView>...</SafeAreaView></AegisBipCaptureRoot>;
Consent, then login
no screen · code only// consent dialog "Accept" button
AegisBip.grantConsent();
// on login tap
AegisBip.report({ type: 'AuthPromptShown', method: AuthMethod.PASSWORD });
const success = username.trim().length > 0 && password.trim().length > 0;
AegisBip.report({ type: 'AuthCompleted', method: AuthMethod.PASSWORD, success, latencyMs });
await AegisBip.startSession(pseudonymousSubjectId(username, DemoConfig.SUBJECT_SALT));
AegisBip.setTask(BipTask.LOGIN);
Home — moving around the app
no screen · code onlyAegisBip.recordScreenVisit('home');
AegisBip.setTask(BipTask.BROWSE);
// touch (A) + navigation (C) already active — no other code on this screen
Filling the transfer form
no screen · code onlyconst onRecipientChange = useAegisKeystroke();
const onAmountChange = useAegisKeystroke();
<TextInput onChangeText={(text) => { onRecipientChange(text); setRecipient(text); }} />
<TextInput onChangeText={(text) => { onAmountChange(text); setAmount(text); }} />
Confirm — the checkpoint contract
no screen · code onlyconst onSendPressed = useAegisTimedAction(async () => {
AegisBip.setTransactionContext({
counterpartyAccountHash: pseudonymousSubjectId(recipient, DemoConfig.SUBJECT_SALT),
channel: 'bank_transfer', amountNative: parsedAmount, currencyCode: 'NGN',
});
const verdict = await AegisBip.checkpoint(CheckpointReason.TRANSFER_INITIATED);
});
The verdict — allow, or stop the transfer here
no screen · code onlyif (verdict == null) {
setStatus('Sent (offline verdict — failed open).'); // FAIL OPEN — our outage, never the user's
} else if (shouldBlock(verdict)) {
showRiskAlert('Transfer Blocked', ...);
} else if (requiresStepUp(verdict)) {
showRiskAlert('Transfer Halted — Verification Required', ...);
} else {
setStatus(`Transfer confirmed — risk ${...}%.`); // clean allow
}
Logout — end the session
no screen · code onlyawait AegisBip.endSession(); // transmits the session payload
Signal coverage across this flow
| Cat | Category | Activated where |
|---|---|---|
| A | Touch | Steps 1 (root wired), 3 (dashboard interaction) |
| B | Keystroke | Step 4 (transfer form fields) |
| C | Navigation | Steps 2, 3, 4 (recordScreenVisit per screen) |
| D | Session timing | Steps 2 (session start), 7 (session end) |
| E | Motion | Passive from step 1 — native TurboModule streaming once the session starts |
| F | Cognitive | Step 5 (send-button hesitation via useAegisTimedAction) |
| G | Environment | Passive from step 1 — sampled at session start |
| H | Security | Step 2 (consent + auth events) |
| I | Device state | Passive from step 1 — sampled continuously once the session starts |
Changelog
1.1.0
- Added
pseudonymousSubjectId()helper — promoted out of the example app. - Added
AegisTimedActionwidget — promoted out of the example app. sessionManageris now optional onAegisTextField/AegisTouchDetector, falling back toAegisBIP.instance.sessionManager.
1.0.3
AegisConfignow defaultsuseProtobuftofalse(JSON) — the previous default silently dropped roughly 36 of 63 scored features.- Revoking consent now discards the session instead of transmitting it.
Changelog
0.1.0 — initial build
A ground-up native SDK, not a repackaging of the Flutter SDK. Signal categories and the wire contract match the Flutter SDK's field-for-field.
- Full A–J signal payload schema, JSON-only.
- Categories G/H/I native readers — telephony, permissions, sensor inventory, screen-capture/remote-access/accessibility-fairness detection.
- Categories A/C capture via a
Window.Callbackdecorator andApplication.ActivityLifecycleCallbacks. - Categories B/D/E/F capture — keystroke
TextWatcher, session timing, realSensorManagermotion, cognitive hesitation timing. - Full auth/credential event handling for Category H.
AegisBippublic singleton and a:demoreference app exercising the full login → home → transfer → checkpoint → logout lifecycle.
Known gaps: G_environment's network-type/
VPN/battery/location fields aren't wired yet.
Changelog
0.1.0 — initial build
Mirrors the Flutter SDK's architecture: orchestration and Categories A/B/C/D/F run in the cross-platform runtime, and native TurboModules cover only what neither TypeScript nor a community package can reach. Signal categories and the wire contract match the Flutter and native Android SDKs' field-for-field.
- Full A–J signal payload types (
SessionPayload.ts), JSON-only. TransportLayer—fetch/AbortController-based checkpoint/training transport with a real 5s hard timeout, fail-open on any failure, offline retry queue, remote kill-switch parsing.- Native TurboModule (Kotlin + Objective-C++) for Categories G (telephony/brightness)/H
(screen-capture/remote-access)/I (permissions/sensor inventory), ported from
AegisBipPlugin.kt/.swift. - Native motion-sensor streaming (
startMotionUpdates/getLatestMotionSample) built directly into the same TurboModule afterreact-native-sensors— the obvious community-package choice — turned out to be unmaintained (years-out-of-date Android Gradle/AGP toolchain, noarm64-v8ain its ABI filter). - Pure-TypeScript capture for Category A (touch, via RN's passive bubble-phase touch events —
never the responder system), B (keystroke, via
TextInput's length-delta comparisons), C (navigation, manualrecordScreenVisitAPI), D (session timing), F (cognitive/hesitation). AegisBippublic singleton and anexample/reference app exercising the full login → home → transfer → checkpoint → logout lifecycle, emulator/simulator-verified end-to-end on both Android and iOS.
Known gaps: G_environment's network-type/
VPN/battery/location fields aren't wired yet; iOS accessibility/remote-control detection and
is_roaming carry the same known gaps as the Flutter SDK's iOS plugin; touch contact
area is always null on both platforms; not yet verified against a real backend
end-to-end.