Peabody Compliance SDK
Integrate sub-second location integrity and hardware-backed device verification into your mobile and web applications.
Installation
The Peabody SDK for iOS is distributed as a Swift Package. You can add it to your project in Xcode by pointing to our release repository.
https://github.com/PeabodySecure/PeabodySDK-Release
Configuration
The Peabody SDK supports multiple authentication methods depending on your security architecture. Choose one of the following methods to initialize the SDK early in your app lifecycle (e.g., in AppDelegate or your main App struct).
Method 1: Hardcoded API Key
Use this method if you wish to bundle your API key directly within the application.
import PeabodySDK Peabody.configure(apiKey: "your_api_key_here")
Method 2: Client ID Handshake (Recommended)
Use this method to exchange a public Client ID for a temporary session token. This avoids embedding your sensitive API key in the binary. You can configure this via code or Info.plist.
// Option A: Configure via code Peabody.configure(clientId: "your_client_id_here") // Option B: Add 'PeabodyClientID' to your Info.plist // The SDK will automatically perform the handshake on the first verification.
Debug Logging
During development, you can enable verbose console logging to inspect hardware signals and network handshakes.
Peabody.enableDebugLogging()
Requesting Permissions
Peabody requires location permissions to perform jurisdictional checks. You can request "When In Use" or "Always" access using our helper methods.
Peabody.requestWhenInUsePermission { status in
print("Location status: \(status)")
}
Customer Tracking
To accurately track and log end-user (retail player) activity in your Peabody Dashboard, it is required to provide a unique User ID and Email during every verification call. This allows you to audit specific users and detect multi-accounting or high-risk repeat offenders.
- External ID: Your internal database ID for the user (e.g., Player UUID or Username).
- External Email: The end-user's registered email address.
// Swift (iOS) Example
Peabody.verifyLocation(externalId: "user_123", externalEmail: "player@example.com") { result in
// Handle result
}
Running Verification
To perform a check, call verifyLocation. This method automatically gathers GPS coordinates, IP intelligence, and device hardware signals. The response provides deep programmatic access to specific risk vectors.
Note: Always pass the current player's ID and Email to ensure your database logs are correctly associated with the correct individual.
// Swift (iOS)
Peabody.verifyLocation(
externalId: "player_uuid_99",
externalEmail: "player@domain.com"
) { result in
switch result {
case .success(let verdict):
// 1. Direct Verdict
if verdict.isCompliant {
print("Access Granted. Risk Score: \(verdict.score)")
}
// 2. Programmatic Integrity Flags
if verdict.isVPNOrProxyActive {
print("Threat: VPN or Proxy Detected")
}
if verdict.isScreenCaptured {
print("Threat: Screen Mirroring Active")
}
// 3. Location Metadata
print("Device City: \(verdict.city)")
print("IP Address: \(verdict.ipAddress)")
case .failure(let error):
print("Verification error: \(error.localizedDescription)")
}
}
Continuous Jurisdictional Monitoring
For applications requiring constant compliance (such as live betting or regulated gaming), Peabody supports a "Heartbeat" mode. This automatically re-verifies the user's location at a set interval while the app is active, handling app lifecycle events (foreground/background) efficiently.
// Start monitoring every 15 minutes (900 seconds)
Peabody.startMonitoring(
interval: 900,
externalId: "user_123",
externalEmail: "player@example.com"
) { result in
switch result {
case .success(let verdict):
if !verdict.isCompliant {
// Take action: e.g. pause the game session
print("Jurisdictional change detected: \(verdict.reason)")
}
case .failure(let error):
print("Monitoring error: \(error.localizedDescription)")
}
}
// Stop monitoring when no longer required
Peabody.stopMonitoring()
Hardware-Backed Integrity
The Peabody SDK utilizes Apple's App Attest service to prove that requests originate from a genuine, unmodified device. This is handled automatically during the verifyLocation flow.
- Cryptographic proof via Secure Enclave
- Automatic key rotation and registration
- Replay protection via stateless HMAC challenges
Advanced Network Security (SSL Pinning)
To prevent Man-in-the-Middle (MITM) attacks and credential intercept, the Peabody iOS SDK employs Public Key Pinning. Every connection to our compliance servers is validated against known, trusted keys. If a mismatch is detected, the SDK immediately terminates the connection and logs a security event to your dashboard.
Security Note: This is a zero-configuration feature. The SDK is pre-configured with current and backup pins to ensure high availability without manual intervention.
Android SDK
Integrate sub-second location integrity and Google Play Integrity verification into your Android applications.
Installation
The Peabody SDK for Android is distributed via Maven. To integrate it, add the dependency to your app-level build.gradle file and ensure your repository settings are configured.
Step 1: Add Dependency
// build.gradle
dependencies {
implementation 'com.peabodycompliance:sdk-android:1.1.0'
}
Step 2: Repository Configuration
Ensure mavenCentral() is included in your settings.gradle or project-level build.gradle. If you are using a private repository, add the following:
// settings.gradle
dependencyResolutionManagement {
repositories {
mavenCentral()
// Add custom repository if provided
}
}
Configuration
Initialize the SDK early in your application lifecycle, typically in your MainActivity or Application class. Choose the method that matches your security architecture.
Method 1: Client ID Handshake (Recommended) — SDK 1.0.5+
Use this method to keep your API key off the device entirely. The SDK automatically calls your backend handshake endpoint to exchange the clientId for a short-lived sessionToken. Your backend proxies the request to Peabody's handshake.php. The SDK then calls verify.php directly with that token — your API key never touches the device.
Both URLs default to Peabody's own endpoints. Supply handshakeUrl and/or verifyUrl to route traffic through your own backend instead.
// Kotlin (SDK 1.0.6+) — route both calls through your backend
Peabody.configureWithClientId(
context = this,
clientId = "your_client_id_here",
cloudProjectNumber = 350256575822L,
handshakeUrl = "https://your-backend.com/peabody-handshake", // optional
verifyUrl = "https://your-backend.com/peabody-verify" // optional
)
// Omit both URLs to use Peabody's default endpoints directly
Peabody.configureWithClientId(
context = this,
clientId = "your_client_id_here",
cloudProjectNumber = 350256575822L
)
// Legacy alias — serverURL is still accepted (same as handshakeUrl)
// Peabody.configureWithClientId(..., serverURL = "https://your-backend.com/peabody-handshake")
Integration Flow
Security properties
clientIdis a public identifier — safe to embed in the APK.appKey(API key) never leaves your backend.- The
sessionTokenis short-lived (2 h) and bound to a specificexternalId/externalEmail— Peabody rejects reuse across different users. - The SDK caches the token and reuses it until 5 minutes before expiry, minimising handshake calls.
- Your backend endpoint must accept
POSTwith JSON body{"clientId","externalId","externalEmail"}and return{"sessionToken","expiresIn"}.
Minimal backend proxy (Node / Express)
// POST /peabody-handshake
app.post('/peabody-handshake', async (req, res) => {
const { clientId, externalId, externalEmail } = req.body;
const response = await fetch('https://www.peabodycompliance.com/handshake.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientId, externalId, externalEmail })
});
const data = await response.json(); // { sessionToken, expiresIn }
res.json(data);
});
Method 2: Direct API Key
Use only for server-to-server integrations or internal tooling where the API key never leaves your infrastructure. Do not embed your API key in a distributed APK.
// Kotlin
Peabody.configureWithApiKey(
context = this,
apiKey = "your_api_key_here",
cloudProjectNumber = 350256575822L
)
Proxy Integrity Guarantee (SDK v1.1.0)
Operators using the Client ID Handshake method may configure a custom verifyUrl to route verification requests through their own backend before they reach Peabody. This is a legitimate and common pattern — it gives operators control over rate limiting, logging, and additional business logic.
However, it raises an important question: what prevents an operator from altering the GPS coordinates in transit to make a user in a restricted location appear compliant?
SDK v1.1.0 closes this gap with nonce binding — a cryptographic chain that ties the device-measured location data to the Google Play Integrity token in a way that cannot be forged or altered without detection.
How Nonce Binding Works
On the Device — The Truth Is Sealed
Before requesting a Play Integrity token, the SDK computes a SHA-256 hash of the exact location data it collected: latitude, longitude, BSSID, timestamp, and package name. This hash is passed to Google as the nonce. Google cryptographically signs the nonce into the integrity token — it cannot be modified without invalidating the token entirely.
At the Proxy — The Lie
A malicious proxy operator receives the payload, changes the GPS coordinates to a permitted location, and re-signs the request with their HMAC key. The altered data now looks compliant — but the sealed nonce inside the Play Integrity token still reflects the original restricted coordinates.
At Peabody's Server — The Detection
The server decodes the Play Integrity token and extracts the Google-signed nonce. It independently recomputes the SHA-256 hash from the received coordinates. The two hashes will not match. The request is immediately rejected with isCompliant: false and the reason "Data Integrity Mismatch — Tampering Detected", and the incident is logged against the operator's account with cryptographic proof.
What This Means for Operators and Auditors
- Legitimate proxy operators see no difference — routing through your backend works exactly as before.
- Tampered requests are cryptographically identified. The verification log records the operator's account ID, masked API key, IP address, the expected hash, and the received hash — providing unambiguous, court-admissible evidence of the attempt.
- Regulators and auditors can be assured that even when an operator controls the transport layer, they cannot alter device-signed compliance data. The cryptographic chain of custody runs from the device's GPS hardware through Google's servers to Peabody's logs.
- Requires no integration change — nonce binding is automatic in SDK v1.1.0. No additional configuration is needed on the operator side.
Tampered Response Example
When a nonce mismatch is detected, the response and audit log look like this:
// verify.php response when tampering is detected:
{
"status": "failure",
"compliant": false,
"risk_score": "100",
"risk_level": "Critical",
"reason": "Data Integrity Mismatch - Tampering Detected"
}
// Server-side audit log entry (error_log):
// Peabody Security CRITICAL: Android Nonce Mismatch - Tampering Detected.
// UserID=4821 Key=a3f8c2d1... IP=203.0.113.47 BundleID=com.operator.app
// Expected=e3b0c44298fc... Got=a665a45920422...
Running Verification
Verify a user's location and device integrity by calling verifyLocation. The SDK automatically acquires a fresh high-accuracy GPS fix, performs reverse geocoding, and submits a Play Integrity token.
Note: For proper database logging, you MUST pass the unique Player ID and Email.
// Kotlin
Peabody.verifyLocation(
externalId = "user_abc_123",
externalEmail = "player@email.com"
) { result ->
result.onSuccess { verdict ->
if (verdict.isCompliant) {
// Access Granted
Log.d("Peabody", "Verified: ${verdict.score}/100")
}
}.onFailure { error ->
// Handle network or permission errors
}
}
Verification Response — Location Metadata
Every verify.php response includes a metadata object with the reverse-geocoded location. For users inside the US, the jurisdiction.code field (e.g. US-NV) is the primary signal. For international users, use metadata.country and metadata.state to apply regional policy on your backend.
{
"compliant": true,
"jurisdiction": {
"code": "UNKNOWN",
"resolved": false,
"operator_note": "Location outside loaded jurisdiction boundaries. Operator should apply jurisdiction policy."
},
"metadata": {
"lat": 31.2304,
"lon": 121.4737,
"city": "Shanghai",
"state": "Shanghai",
"country": "China"
}
}
International Geofencing: Use metadata.country + metadata.state to whitelist specific regions (e.g. allow Shanghai and Beijing, block all other Chinese provinces). Province names are returned in English. GPS coordinates are anti-spoof verified before geocoding, so these values are reliable when isMockLocation is false.
Peabody Verify on Google Play
For Android users accessing your platform through a mobile web browser (rather than your native app), Peabody Verify is available on the Google Play Store. The JS SDK automatically fires a Chrome Intent URL when it detects an Android browser — if the app is installed it opens directly; if not, Chrome redirects to the Play Store automatically.
Google Play Store
https://play.google.com/store/apps/details?id=com.peabodycompliance.verify
No configuration is needed — the JS SDK handles the Intent URL and Play Store fallback automatically. This flow mirrors the iOS Peabody Verify deep link and requires no changes to your integration code.
Play Console Linking
For hardware-backed integrity to function, your app must be linked to a Google Cloud project with the Play Integrity API enabled, and those credentials must be configured in your Peabody dashboard. Each operator uses their own Google Cloud project — this keeps your app fully isolated and requires no access to Peabody's infrastructure.
One-time setup — takes about 5 minutes
- Create a Google Cloud project at
console.cloud.google.com(free). Note the project number shown on the dashboard. - Enable the Play Integrity API — search for "Play Integrity API" in the API Library and click Enable.
- Link your app in Play Console — navigate to Release › Setup › App integrity, click "Link a Google Cloud project", and enter your own project number. You own both, so this completes immediately.
- Create a service account — in Cloud Console go to IAM & Admin › Service Accounts, create a new account, and grant it the
roles/playintegrity.tokenVerifierrole (or the primitiveViewerrole if not available). - Download the JSON key — click your service account, go to the Keys tab, click Add Key › Create new key, choose JSON, and save the file.
- Paste it into your Peabody dashboard — open the Plan & Billing modal, find the Android SDK — Google Cloud Credentials section, and paste the contents of the JSON file. Click Save.
Once saved, Peabody uses your credentials to verify Play Integrity tokens for your app. Your service account JSON is stored encrypted and is never returned to the browser or exposed in API responses.
JavaScript SDK (Web)
Secure your web applications and sweepstakes sites using our browser-based trust layer.
Supported Platforms
| Platform | Status | Notes |
|---|---|---|
| iOS (iPhone / iPad) | Supported | Native SDK or Peabody Verify deep link |
| Android | Supported | Native SDK with Play Integrity, or Peabody Verify app (Google Play) for mobile web users |
| macOS (Safari, Chrome, Firefox) | Supported | JS SDK + Mac Compliance Agent |
| Windows (Chrome, Edge, Firefox) | Supported | JS SDK + Windows Compliance Agent |
| Linux & Steam Deck (Chrome, Firefox) | Beta | JS SDK + Linux Compliance Agent; requires agent to be installed |
Linux note: Linux desktop requests without a hardware agent payload are rejected with HTTP 403. Virtual machine environments (VirtualBox, VMware, KVM, HyperV) and Wine are blocked regardless of agent presence.
Installation
You can integrate the Peabody Web SDK using our hosted CDN for immediate updates, or download the source for self-hosting.
Option 1: CDN (Recommended)
Include the Peabody script directly in your HTML <head>. This ensures you always have the latest security definitions.
<script src="https://peabodycompliance.com/resources/js/peabody.min.js"></script>
Option 2: Self-Hosting
For organizations requiring full control over assets, you can download the script and host it on your own servers. Contact support for access to the self-hosted distribution package.
Configuration & Authentication
Security Warning: Never set Peabody.config.apiKey in browser-side code. Your master API key is visible to anyone who opens DevTools and can be used to drain your credit balance from any server in the world. Always use a short-lived session token instead.
The JS SDK authenticates via a session token — a short-lived credential issued by Peabody's handshake endpoint. The flow is always the same: your backend calls Peabody's /handshake.php using your Client ID (found in your dashboard), receives a session token, and passes it to the browser. The browser then calls /verify.php with that token — which is how Peabody identifies your account and bills one credit per call.
🛡️ Enhanced Security: Session Binding
To prevent "Credential Re-use" (where one user's valid session token is used to perform a verification for a different user), you should bind the session token to a specific User ID and Email during the handshake. If bound, Peabody will reject any verification call where the payload identifiers do not match the session identifiers.
Your two credentials:
• API Key — master secret. Only for direct server-to-server calls. Never put this in a browser or mobile app.
• Client ID — used exclusively to call /handshake.php from your backend. Keep this server-side too.
The Handshake Call (with Binding)
When calling /handshake.php, provide the externalId and externalEmail of the user you are about to verify. This locks the token to that specific user.
POST https://peabodycompliance.com/handshake.php
Content-Type: application/json
{
"clientId": "your_client_id",
"bundleId": "your-domain.com",
"platform": "web",
"externalId": "user_12345",
"externalEmail": "player@email.com"
}
// Peabody responds:
{
"sessionToken": "a3f8c2d1e4f6...",
"expiresIn": 7200
}
There are two integration patterns depending on your stack:
Pattern 1 — Server-Side Rendering (SSR)
Your backend calls the Peabody handshake on every protected page load and injects the returned token directly into the HTML. The token is baked in before the browser sees it — no extra client-side round trip needed.
PHP
<?php
// Your backend calls Peabody's handshake to get a session token
// We pass the user's ID and Email to BIND the token to this specific user.
$response = file_get_contents('https://www.peabodycompliance.com/handshake.php', false,
stream_context_create(['http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode([
'clientId' => 'your_client_id',
'bundleId' => 'your-domain.com',
'platform' => 'web',
'externalId' => $user->id,
'externalEmail' => $user->email
])
]])
);
$peabody = json_decode($response, true);
$sessionToken = $peabody['sessionToken'];
?>
<script src="https://www.peabodycompliance.com/resources/js/peabody.min.js"></script>
<script>
Peabody.config.sessionToken = '<?= htmlspecialchars($sessionToken, ENT_QUOTES) ?>';
Peabody.config.tokenRefreshEndpoint = '/your-token-refresh-endpoint';
</script>
Node / Express
const fetch = require('node-fetch');
app.get('/protected-page', requireAuth, async (req, res) => {
// Call Peabody handshake server-side
const peabody = await fetch('https://peabodycompliance.com/handshake.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: 'your_client_id',
bundleId: 'your-domain.com',
platform: 'web'
})
}).then(r => r.json());
// Inject the token into your rendered template
res.render('protected-page', { sessionToken: peabody.sessionToken });
});
// In your template (EJS, Handlebars, etc.):
// <script>
// Peabody.config.sessionToken = '<%= sessionToken %>';
// Peabody.config.tokenRefreshEndpoint = '/your-token-refresh-endpoint';
// </script>
Python / Flask
import requests
from flask import render_template
from functools import wraps
@app.route('/protected-page')
@login_required
def protected_page():
# Call Peabody handshake server-side
peabody = requests.post('https://peabodycompliance.com/handshake.php', json={
'clientId': 'your_client_id',
'bundleId': 'your-domain.com',
'platform': 'web'
}).json()
return render_template('protected_page.html',
session_token=peabody['sessionToken'])
# In your Jinja2 template:
# <script>
# Peabody.config.sessionToken = '{{ session_token }}';
# Peabody.config.tokenRefreshEndpoint = '/your-token-refresh-endpoint';
# </script>
Pattern 2 — Single-Page Apps (SPA / React / Vue / Angular)
For client-side SPAs there is no server-rendered page to inject into. Instead, create a small protected endpoint on your own backend that calls the Peabody handshake and returns the token to your frontend. Point tokenRefreshEndpoint at that same route so the SDK can renew automatically.
Your backend token endpoint (any language)
// Example: Express route — requires the user to be authenticated first
app.post('/api/peabody-token', requireAuth, async (req, res) => {
// Call Peabody handshake with your Client ID (server-side only)
const peabody = await fetch('https://peabodycompliance.com/handshake.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: 'your_client_id', // ← kept on your server, never sent to browser
bundleId: 'your-domain.com',
platform: 'web'
})
}).then(r => r.json());
res.json({ sessionToken: peabody.sessionToken });
});
Frontend (JavaScript / React / Vue)
// On app load, fetch a token from your own backend (user must be logged in)
async function initPeabody() {
const res = await fetch('/api/peabody-token', { credentials: 'include' });
const data = await res.json();
Peabody.config.sessionToken = data.sessionToken;
Peabody.config.tokenRefreshEndpoint = '/api/peabody-token'; // SDK auto-refreshes here on expiry
}
await initPeabody();
Automatic Token Refresh
When a token expires, the SDK automatically calls your tokenRefreshEndpoint, which in turn calls the Peabody handshake to issue a fresh token. The failed verification is then retried transparently — no action needed from your application code.
// Your refresh endpoint calls Peabody's handshake (same as above) // The SDK calls it automatically when it gets a 401 — then retries the verification. Peabody.config.tokenRefreshEndpoint = '/api/peabody-token'; // That's it. The SDK handles the rest silently.
Web Verification
Trigger a verification session when a user performs a high-value action. On desktop this gathers browser telemetry and hardware agent data. On iPhone/iPad it automatically fires the Peabody Verify deep link, and on Android it fires the Peabody Verify Intent URL — no extra configuration required for either mobile platform.
Safari macOS Restriction
Desktop Safari blocks the secure local connection required by the Mac Compliance Agent. If requireDesktopAgent is enabled, users on Safari macOS will receive a browser_unsupported status. They must be advised to use Chrome, Firefox, or Edge.
Note: Always pass the current user's ID and Email so your dashboard logs are correctly attributed.
async function checkUser() {
const result = await Peabody.verifySession('user_123', 'player@email.com');
// Case 1: Browser Restriction (Safari macOS)
if (result.status === 'browser_unsupported') {
showBrowserSwitchPrompt(); // Advise user to use Chrome or Firefox
return;
}
// Case 2: iOS/iPadOS — app not installed
if (result.status === 'app_required') {
showAppStorePrompt();
return;
}
// Case 3: Desktop — agent required but not running
if (result.status === 'download_required') {
showAgentInstallModal(result.downloadUrl);
return;
}
// Handle errors or success...
}
Heartbeat & 15-Minute Re-Verification
For regulated operators (sports betting, iGaming, live content) requiring continuous compliance, Peabody provides a server-driven heartbeat. verify.php records a last_verified_at timestamp in the session on every successful iOS verification. The JS SDK polls check_session.php every 60 seconds and automatically re-triggers the iOS app when the 15-minute window expires.
Why Server-Side?
Client-side caching (sessionStorage) can be cleared or tampered with by the user. The server is the authoritative source of truth — check_session.php checks the age of last_verified_at in your database, making compliance state unforgeable from the browser.
Heartbeat API
// Call after the iOS app completes initial verification and the user returns to the browser.
// Default interval is 60,000 ms (60 seconds).
Peabody.startHeartbeat();
// Stop the heartbeat on logout or page unload.
Peabody.stopHeartbeat();
// Single poll — check the server-side session status once.
// Useful for confirming the iOS app has posted back before granting access.
const status = await Peabody.checkSession();
// { status: "valid", age_seconds: 247, expires_in_seconds: 653 }
// { status: "expired", reason: "re-verification required" }
Configuration
// Default — points to Peabody's hosted endpoint. No change needed for most integrations. Peabody.config.heartbeatEndpoint = 'https://www.peabodycompliance.com/check_session.php'; // Override the poll interval (milliseconds): Peabody.startHeartbeat(30000); // poll every 30 seconds
What Happens on Expiry
When check_session.php returns status: "expired", the SDK automatically:
- Clears the client-side
sessionStorageverification cache. - Shows a full-screen "Verifying Location..." overlay — blocking the UI while re-verification is in progress.
- Re-fires the
peabodyverify://deep link so the iOS app performs a fresh location check. - Hides the overlay automatically once the next heartbeat poll returns
status: "valid".
check_session.php Reference
POST https://peabodycompliance.com/check_session.php
Content-Type: application/json
{ "sessionToken": "your_session_token" }
// Valid — last verified less than 15 minutes ago:
{ "status": "valid", "age_seconds": 247, "expires_in_seconds": 653 }
// Expired — 15+ minutes since last verification:
{ "status": "expired", "reason": "re-verification required", "age_seconds": 1043 }
// Invalid or expired session token:
HTTP 401 → { "status": "error", "message": "Invalid or expired session" }
Desktop Compliance Agents (Mac & Windows)
Hardware-backed location verification for macOS and Windows desktop users — bridging the gap between browser GPS and physical truth.
Why Desktop Agents Exist
Most desktop computers have no GPS chip. When a browser calls navigator.geolocation, the OS falls back to IP-based geolocation — meaning your user in Florida may appear to be in New Jersey because their ISP routes traffic through a distant Point of Presence. The Peabody Compliance Agents solve this by reading real WiFi access point data (BSSIDs + signal strengths) directly from the hardware and signing it with a cryptographic key (ECDSA P-256), giving the server proof of the device's physical location.
How It Works
The agent is a lightweight system service (PeabodyService) that runs a local HTTP server on localhost:12180. When the Peabody JS SDK detects a desktop environment, it silently probes this port. If the agent is running, its hardware payload is included in the verification POST to your server. Your server then verifies the signature and uses the WiFi triangulation data — via the Google Geolocation API — to resolve the user's true physical coordinates.
WiFi Triangulation
Scans all nearby access points (BSSID + RSSI). Top 5 by signal strength are sent to Google Geolocation API for 3–10 meter accuracy.
Hardware Fingerprint
Generates a persistent hardware device ID (IOPlatformUUID on Mac, MachineGUID on Windows), enabling anti-multi-accounting detection across sessions.
OS-Level VPN Detection
Inspects the system routing table for utun, ppp, tap, and wintun interfaces — 100% certainty vs browser-based WebRTC heuristics.
SDK Configuration
Options are available in Peabody.config to control agent behavior for Mac, Windows, and Linux:
Peabody.config.sessionToken = 'your_session_token';
// Set true to REQUIRE the agent on desktop browsers (Mac, Windows, Linux).
// If the agent is not running, verifySession() returns { status: 'download_required' }
// instead of falling back to browser GPS.
Peabody.config.requireDesktopAgent = true;
// Download page URLs — users are redirected here when the agent is not installed.
Peabody.config.macAgentDownloadUrl = 'https://www.peabodycompliance.com/downloads/PeabodyService.dmg';
Peabody.config.windowsAgentDownloadUrl = 'https://www.peabodycompliance.com/downloads/windows.html';
Peabody.config.linuxAgentDownloadUrl = 'https://www.peabodycompliance.com/downloads/linux.html';
When requireDesktopAgent is false (the default), the agent is used opportunistically — if it's running, its data enhances the verification; if not, standard browser geolocation is used as a fallback. Set it to true only when you need the highest-assurance mode on desktop.
Linux & Steam Deck Agent Beta
Hardware-backed location and device verification for Linux desktops and Steam Deck — the first geolocation compliance agent for the Linux platform.
How It Works
The Linux agent is a small Go binary that runs as a background systemd service on localhost:12180 — the same port as the Mac and Windows agents. The JS SDK detects Linux via the User-Agent, polls the agent, and forwards the payload to verify.php under the key linux_hardware. Without the agent, Linux requests are rejected with HTTP 403.
Distributing the Agent
The agent binary is hosted at:
Linux Binary (amd64)
https://www.peabodycompliance.com/downloads/PeabodyAgent-linux-amd64
The SDK's default linuxAgentDownloadUrl points to the download page at /downloads/linux.html, which walks users through installation. No additional configuration is required for most integrations.
User Installation Steps
The agent is a one-time install. Once running, all future verifications happen silently in the background.
- Download
PeabodyAgent-linux-amd64from the link above. - Open a terminal and make the file executable:
chmod +x PeabodyAgent-linux-amd64 - Run the installer:
./PeabodyAgent-linux-amd64 --install - Return to the site and refresh — verification completes automatically.
Auto-Start
The --install flag copies the binary to ~/.local/bin/peabody-agent, writes a user-level systemd service, and enables it. loginctl enable-linger is called so the service starts at boot — no sudo required. Steam Deck is detected automatically; the agent installs to the home partition and survives SteamOS updates.
To uninstall: peabody-agent --uninstall
Server-Side Verification
When linux_hardware is present in the payload, verify.php applies the following logic:
- VPN detection from
/proc/net/dev(tun, tap, WireGuard, ProtonVPN, NordLynx, Mullvad, PIA) overrides browser WebRTC signals. - Wi-Fi BSSIDs from nmcli are sent to the Google Geolocation API for triangulation. If nmcli is unavailable, browser GPS is used as fallback.
- Virtual machine environments (VirtualBox, VMware, KVM, HyperV, Xen) are detected via DMI and CPU flags — VM sessions receive a risk score of 100 and are rejected.
- Wine compatibility layer is detected and scored +70 risk.
- Steam Deck hardware is identified natively (Jupiter/Galileo DMI product names) — Steam Deck is treated as real hardware, not a VM.
device_idis derived from/etc/machine-idfor cross-session device tracking.- Low-precision and IP distance penalties are suppressed — same as Mac and Windows agents.
Agent Payload Schema
The raw JSON returned by http://localhost:12180 and forwarded under linux_hardware:
{
"agentVersion": "1.0.0",
"platform": "linux",
"device": {
"machineId": "a1b2c3d4e5f6...", // /etc/machine-id
"hostname": "my-ubuntu-pc",
"osRelease": "Ubuntu 24.04.2 LTS",
"isVM": false,
"vmType": "",
"isSteamDeck": false,
"isWine": false
},
"vpn": {
"vpnDetected": false,
"vpnInterfaces": null // e.g. ["tun0", "wg0"] when VPN active
},
"wifi": {
"accessPoints": [
{ "macAddress": "aa:bb:cc:dd:ee:ff", "signalStrength": -58, "channel": 6 },
{ "macAddress": "11:22:33:44:55:66", "signalStrength": -71, "channel": 11 }
],
"scanError": "" // set if nmcli unavailable
}
}
Note: No ECDSA signature is included in the Linux payload — the trust model is equivalent to the Windows agent (OS-level signals without cryptographic attestation). A signed agent build is planned for a future release.
Peabody Verify (iOS)
High-integrity location and device verification for users on mobile browsers and tablets — where the Peabody desktop agents cannot run and the native iOS SDK is not embedded in the operator's app.
The Mobile Browser Challenge
Mobile browsers (Safari on iPhone/iPad) are heavily sandboxed. They cannot access hardware-level integrity signals like App Attest, cannot detect VPNs at the OS level, and allow users to spoof browser geolocation. The Peabody Verify app solves this by accepting a deep link from the browser, performing full native hardware checks in under a second, posting the signed result directly to verify.php, and redirecting the user back to their browser.
How It Works
When Peabody.verifySession() is called on an iPhone or iPad, the JS SDK automatically fires the Peabody Verify deep link — no additional configuration required. No browser-side GPS or WebRTC telemetry is attempted; the iOS app is the sole source of truth.
Step 1 — Deep Link
Browser calls verifySession(). SDK fires peabodyverify://verify?session=...&returnUrl=.... Peabody Verify app opens.
Step 2 — Native Check
App collects GPS, BSSID, VPN status, and jailbreak state. Signs the payload with HMAC and POSTs directly to verify.php. Shield turns green or red.
Step 3 — Return
App calls UIApplication.shared.open(returnUrl) to send the user back to their browser. Site calls startHeartbeat() to begin the 15-minute compliance loop.
Deep Link URL Format
The SDK constructs the deep link automatically from Peabody.config.sessionToken and the current page URL. The iOS app must parse both parameters from the incoming URL.
peabodyverify://verify?session=SESSION_TOKEN&returnUrl=https%3A%2F%2Fyoursite.com%2Fgame
| Parameter | Description |
|---|---|
| session | The active sessionToken. Included in the iOS app's POST body to verify.php so the verification is logged against the correct account and updates last_verified_at. |
| returnUrl | URL-encoded address of the originating page. The app opens this URL after a successful verification. Validate this against an allowlist in the iOS app to prevent open-redirect abuse. |
iOS App Requirements (Swift)
The Peabody Verify iOS app must handle two things to complete the integration:
// 1. Parse the session token and returnUrl from the incoming deep link
// in your AppDelegate / Scene delegate:
func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
guard let url = contexts.first?.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let sessionParam = components.queryItems?.first(where: { $0.name == "session" })?.value,
let returnParam = components.queryItems?.first(where: { $0.name == "returnUrl" })?.value,
let returnUrl = URL(string: returnParam) else { return }
VerificationService.shared.verify(sessionToken: sessionParam) { success in
guard success else { return }
// 2. Redirect back to the browser after successful verification.
// Validate the domain against your allowlist first.
let allowedHosts = ["yoursite.com", "app.yoursite.com"]
if allowedHosts.contains(returnUrl.host ?? "") {
UIApplication.shared.open(returnUrl)
}
}
}
Continuous Compliance — 15-Minute Heartbeat
After the iOS app redirects the user back to the browser, poll Peabody.checkSession() to confirm verification completed, then call Peabody.startHeartbeat() to begin the 60-second compliance loop. When the 15-minute window expires, the SDK automatically shows an overlay and re-triggers the deep link.
// On page load (after the user returns from the Peabody Verify app):
async function waitForVerificationThenStart() {
// Poll up to 10 seconds to confirm the iOS app posted to verify.php
for (let i = 0; i < 10; i++) {
const status = await Peabody.checkSession();
if (status.status === 'valid') {
Peabody.startHeartbeat(); // begin 60-second server-side polling
enableGameplay(); // grant access to regulated content
return;
}
await new Promise(r => setTimeout(r, 1000));
}
// App did not complete in time — re-trigger
Peabody.verifySession('user_123', 'player@email.com');
}
waitForVerificationThenStart();
// On logout or page unload:
window.addEventListener('beforeunload', () => Peabody.stopHeartbeat());
Handling the download_required and app_required Responses
When the required component is not detected, verifySession() returns a special status without calling your server. Handle each case in your UI:
const result = await Peabody.verifySession('user_123', 'player@email.com');
// iOS/iPadOS — Peabody Verify app not installed
if (result.status === 'app_required' && /iPhone|iPad|iPod/.test(navigator.userAgent)) {
showAppStoreLink('https://apps.apple.com/us/app/peabody-verify/id6762066589');
return;
}
// Android — Peabody Verify app not installed (non-Chrome browser fallback)
if (result.status === 'app_required' && /Android/.test(navigator.userAgent)) {
showPlayStoreLink('https://play.google.com/store/apps/details?id=com.peabodycompliance.verify');
return;
}
// Desktop (Mac, Windows, or Linux) — compliance agent not running
if (result.status === 'download_required') {
showAgentInstallModal(result.downloadUrl); // result.downloadUrl points to DMG, EXE, or linux.html
return;
}
// All other statuses proceed normally
if (result.compliant) { /* access granted */ }
Distributing the Agent
No build step required. Peabody hosts a signed and Apple-notarized DMG on our servers. The SDK's default macAgentDownloadUrl already points to it — so if you use the CDN-hosted SDK, your users will automatically receive our verified installer with no configuration needed.
Hosted DMG
https://www.peabodycompliance.com/downloads/PeabodyService.dmg
This is the default value of Peabody.config.macAgentDownloadUrl. You can override it if you need to self-host the DMG on your own infrastructure, but for most integrations no change is needed:
// Default — uses Peabody's hosted, signed DMG. No action required. Peabody.config.macAgentDownloadUrl = 'https://www.peabodycompliance.com/downloads/PeabodyService.dmg'; // Override only if self-hosting on your own servers: // Peabody.config.macAgentDownloadUrl = 'https://your-domain.com/downloads/PeabodyService.dmg';
The Peabody-hosted DMG is signed with an Apple Developer ID certificate and notarized by Apple. Users can open it immediately without any macOS security warnings.
User Installation Steps
The agent is a one-time install. Once it is running, all future verifications happen silently in the background — the user will never see the install prompt again.
- The DMG downloads automatically. Open it from the Downloads folder.
- Drag PeabodyServiceMac into the Applications folder shown in the installer window.
- Open PeabodyServiceMac from Applications. Grant Location Services and Network access when prompted by macOS.
- A small icon appears in the macOS menu bar — the agent is now running. Return to the page to continue.
Auto-Start
The agent registers itself as a macOS Login Item after the first launch. It will start automatically whenever the user logs in — no manual action required on subsequent visits.
Server-Side Verification (verify.php)
When a Mac hardware payload is present, your server must verify the ECDSA P-256 signature before trusting any of the hardware signals. The Peabody verify.php reference implementation handles this automatically. Key behaviors when mac_hardware is present in the payload:
- ECDSA signature is verified against the included public key (DER/SPKI format). Request is rejected with HTTP 403 if verification fails.
- Top 5 nearby networks by RSSI strength are sent to the Google Geolocation API. The returned coordinates replace browser GPS as the authoritative location.
vpn_activefrom the OS routing table overrides browser WebRTC VPN detection.device_id(hardware UUID) is stored in the log, enabling cross-session device tracking and multi-accounting detection.- The connected access point BSSID is stored in
verification_logs.bssid. - Low-precision and IP distance discrepancy risk penalties are suppressed — desktop Macs have no GPS chip, so browser accuracy is inherently coarse, and ISP routing makes IP geolocation unreliable.
Agent Payload Schema
The raw JSON returned by http://localhost:12180 and forwarded to your server under mac_hardware:
{
"timestamp": 1711046400,
"device_id": "550e8400-e29b-41d4-a716-446655440000",
"connected_network": {
"ssid": "MyWiFiNetwork",
"bssid": "aa:bb:cc:dd:ee:ff",
"rssi": -42
},
"nearby_networks": [
{ "ssid": "MyWiFiNetwork", "bssid": "aa:bb:cc:dd:ee:ff", "rssi": -42 },
{ "ssid": "NeighborNet", "bssid": "11:22:33:44:55:66", "rssi": -71 }
],
"vpn_active": false,
"signature": "MEUCIQDa...", // ECDSA P-256 signature over sorted JSON
"public_key": "MFkwEwYH..." // DER/SPKI-encoded EC public key (base64)
}
Security Properties
- Replay protection: The
timestampfield is verified server-side; stale payloads are rejected. - Tamper detection: JSON keys are sorted before signing. Any modification to the payload (e.g., changing WiFi BSSIDs or VPN status) invalidates the signature.
- CORS enforcement: The local bridge only responds to requests originating from your registered domain.
- No network egress: The agent never contacts external servers. All data flows through the browser to your server, keeping the architecture simple and auditable.
SDK Response Structure
The verification call returns a comprehensive object containing all trust signals. Whether using the iOS or JavaScript SDK, the underlying data structure is consistent.
Response Object Reference
{
"status": "failure",
"compliant": false,
"risk_score": "100",
"risk_level": "Critical",
"reason": "Located in exclusion zone: Atlantic City Casino District; Proxy/VPN detected",
"external_id": "user_12345",
"device_integrity": {
"hardware_integrity": true,
"is_jailbroken": false,
"is_mock_location": false,
"is_screen_captured": false,
"is_vpn_active": true
},
"jurisdiction": {
"name": "New Jersey",
"code": "US-NJ",
"status": "active",
"resolved": true,
"in_us": true
},
"exclusion_zone": {
"active": true,
"name": "Atlantic City Casino District",
"type": "casino_floor"
},
// Tribal zone example — same structure:
// "exclusion_zone": { "active": true, "name": "Ho-Chunk Nation", "type": "exclusion" }
"metadata": {
"lat": 39.3585,
"lon": -74.4358,
"ip": "149.102.244.98",
"city": "Atlantic City",
"state": "NJ",
"country": "United States"
},
"timestamp": "2026-03-08 15:03:50"
}