Mobile device fraud detection
Detect jailbroken / rooted / emulated / debugger-attached / sideloaded devices on iOS + Android. Use the signals to gate sensitive onboarding and transactions.
The shape
App startup
│
â–¼
1. Device SDK collects integrity signals
│ @quantum-elixir/device-sdk-rn (or native)
â–¼
2. SDK POSTs to /api/device-session
│ returns sessionToken (30-min TTL)
â–¼
3. User initiates sensitive action
│
â–¼
4. Backend: POST /api/evaluate
{ customerId, lane, sessionToken, ... }
│
â–¼
5. Anti-fraud rules + ML score using device signals
│
├─ jailbroken/rooted → block (hard rule, bypassMl)
├─ emulator → review or flag (configurable)
├─ vpn/proxy/tor → flag (don't block — many legit users)
├─ debugger attached → block
├─ attestation failed → review or block (per org)
└─ all clean → score normallySetup (React Native)
npm install @quantum-elixir/device-sdk-rn
npx pod-installimport { ElixirDeviceSDK } from '@quantum-elixir/device-sdk-rn';
const sdk = new ElixirDeviceSDK({
apiKey: process.env.QE_PUBLIC_KEY!,
endpoint: 'https://your-backend.example.com/api/proxy/device-session',
challengeEndpoint: 'https://your-backend.example.com/api/proxy/attestation/challenge',
onReady: (sessionToken) => store.set('sessionToken', sessionToken),
});
// Call on app boot AND before every sensitive submit
await sdk.collect();Backend proxy
Your backend forwards the device-session POST to anti-fraud with your secret API key. Never embed your secret key in the mobile bundle.
app.post('/api/proxy/device-session', async (req, res) => {
// Verify request came from your app (e.g. JWT, public-key issued to client)
if (!isAuthorized(req)) return res.status(401).send();
const sandboxRes = await fetch(
'https://api.quantumelixir.tech/anti-fraud/api/device-session',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QE_ANTI_FRAUD_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
},
);
res.status(sandboxRes.status).json(await sandboxRes.json());
});What signals get collected
| Signal | iOS | Android |
|---|---|---|
jailbroken | Cydia / Sileo / RootHide / ElleKit / sandbox-write probe | n/a |
rooted | n/a | Magisk / SuperSU / KernelSU / APatch detection |
emulator | Simulator detection (TARGET_OS_SIMULATOR) | Build.HARDWARE / Build.FINGERPRINT heuristics |
debuggerAttached | sysctl PTRACE check | Debug.isDebuggerConnected() + native ptrace check |
sideloaded | Bundle ID prefix + provisioning profile | INSTALLER_PACKAGE_NAME check |
vpn | Network reachability + interface name | NetworkCapabilities + VpnService check |
proxy | NSURLSessionConfiguration proxy | System proxy properties |
tor | Known Tor exit node IP | Same |
mitm | Cert chain mismatch when MITM probe URL configured | Same |
attestation (iOS) | AppAttest token | n/a |
attestation (Android) | n/a | Play Integrity token |
Default rule policy
Out-of-the-box rules (you can edit any of them):
| Rule code | Trigger | Action | bypassMl |
|---|---|---|---|
DEV-001 | jailbroken == true | block | true |
DEV-002 | rooted == true | block | true |
DEV-003 | emulator == true | review | false |
DEV-004 | debuggerAttached == true | block | true |
DEV-005 | sideloaded == true | review | false |
DEV-006 | vpn == true | flag | false |
DEV-007 | mitm == true | block | true |
DEV-008 | attestationVerdict == 'fail' | review | false |
Adjust from the Anti-Fraud dashboard's Rules page. bypassMl: true means the action is never downgraded by ML suppression — use for hard-stop signals.
Common false positives
- VPN users. Banking-app users on corporate VPNs are common. Don't block on
vpn:truealone. - Modified-iOS users. RootHide and similar jailbreaks try to evade detection. The SDK occasionally has false positives on heavily-customized devices that aren't actually jailbroken. Surface a "device security concern" message and offer a support path; don't silently hard-block.
- Genuine devs on debug builds. Internal testers might trip
debuggerAttached. Whitelist your internal team's customer IDs.
Combining with biometric
Device intelligence + biometric step-up is the gold-standard pattern:
// 1. Device signals + anti-fraud
const fraud = await qe.antiFraud.evaluate({
customerId, lane: 'transaction', sessionToken, externalId: txn.id, ...
});
if (fraud.decision === 'block') return reject('fraud_block');
// 2. Biometric step-up if action is high-value
if (txn.amount > 10_000_000) { // > IDR 10M
const stepUp = await qe.identity.authStepUp({ ... });
if (!stepUp.allow) return reject('biometric_failed');
}
// 3. Commit
await commit(txn);Rotating SDK config
The SDK polls GET /api/sdk-config on app boot and caches for 5–15 min. Change cert pins or toggle MITM probes via the dashboard — no app-store release needed.