Biometric Oracle
Network Protocol
A standard for wrapping biometric device data as cryptographically attested settlement oracles in prediction markets and commitment contracts.
Why BONP?
Prediction markets that settle on biometric data all solve the same problem independently: fetch from a device API, prove the data is real, handle disputes. BONP extracts that into a standard. Every adapter speaks the same envelope format. Every verifier runs the same checks.
Standard envelope
One typed format across all providers. Whoop, Oura, and Apple
Health all produce the same BiometricEnvelope.
Replay protection
Every envelope carries a 32-byte nonce and a signed fetch timestamp. Stale or replayed data is rejected at verification.
Dispute pathway
Counter-evidence envelopes, heuristic adjudication, and optional on-chain arbitration — baked in from day one.
Quick Start
pnpm add @bonp/sdk
Settle a market with Whoop recovery
import { WhoopAdapter, AdapterRegistry, verifyEnvelope, generateStalenessProof } from "@bonp/sdk";
const registry = new AdapterRegistry();
registry.register(new WhoopAdapter());
const adapter = registry.get("whoop")!;
const envelope = await adapter.fetchMetric(
userId,
"recovery_score",
{ start: new Date("2026-04-19"), end: new Date("2026-04-20") },
{ access_token: whoopToken }
);
const { valid, errors } = await verifyEnvelope(envelope);
if (!valid) throw new Error(errors.map(e => e.code).join(", "));
const staleness = generateStalenessProof(envelope);
if (!staleness.is_fresh) throw new Error(`Stale: ${staleness.lag_ms}ms`);
console.log(envelope.claim.value); // e.g. 74 (recovery score)
BiometricEnvelope
The core BONP data structure. Every adapter produces one; every verifier consumes one.
{
"version": "BONP-1.0",
"claim": {
"metric": "recovery_score",
"value": 74,
"unit": "percent",
"timestamp": "2026-04-19T08:32:00.000Z",
"window": {
"start": "2026-04-19T00:00:00.000Z",
"end": "2026-04-20T00:00:00.000Z"
},
"device": {
"provider": "whoop",
"device_id": "a3f9c2...", // SHA-256 hashed
"model": "WHOOP 4.0"
},
"subject": {
"id": "7b2e1d...", // SHA-256 of user ID
"data_hash": "c8f0a4..." // SHA-256 of raw API response
}
},
"attestation": {
"provider_signature": "3a7c...",
"provider_public_key": "d9e1...",
"adapter_id": "bonp-whoop-v1",
"adapter_version": "1.0.0",
"fetch_timestamp": "2026-04-19T09:01:14.322Z",
"nonce": "8f4a1b2c..."
},
"settlement": {
"staleness_window_ms": 43200000,
"confidence_score": 0.95,
"dispute_window_ms": 86400000
}
}
Settlement Lifecycle
sequenceDiagram
participant M as Market Contract
participant O as Oracle (Adapter)
participant P as Device Provider API
participant V as Verifier
M->>O: fetchMetric(userId, metric, range)
O->>P: GET /recovery?start=&end=
P-->>O: { records: [...] }
O->>O: sign(canonicalize(claim), adapter_key)
O-->>M: BiometricEnvelope
M->>V: verifyEnvelope(envelope)
V->>V: check version + fields
V->>V: check nonce registry
V->>V: check staleness_window_ms
V->>V: check confidence_score >= 0.5
V-->>M: { valid: true, errors: [] }
M->>M: generateStalenessProof(envelope)
Note over M: dispute_window_ms begins
alt No dispute
M->>M: Settlement final
else Dispute opened
M->>V: openDispute + submitEvidence
V->>V: adjudicate(dispute, original)
V-->>M: DisputeRecord resolved
end
-
1Fetch
Oracle calls
adapter.fetchMetric(). The adapter hits the provider API, wraps the response in a signedBiometricEnvelope. -
2Verify
Settlement engine calls
verifyEnvelope(). Checks version, nonce, staleness, confidence floor, and signature. -
3Challenge period
dispute_window_mselapses. During this window, any party can open a dispute with counter-evidence. -
4Resolve
No dispute → settlement final. Disputed → heuristic adjudication or human arbiter resolves.
Error Codes
| Code | Meaning | Recovery |
|---|---|---|
| BONP-001 | Invalid envelope format | Fix malformed fields |
| BONP-002 | Signature verification failed | Re-fetch with valid adapter keypair |
| BONP-003 | Stale data outside window | Re-fetch with current timestamp |
| BONP-004 | Replay detected — nonce reuse | Generate new nonce per envelope |
| BONP-005 | Provider authentication failed | Refresh OAuth token |
| BONP-006 | Metric not supported by adapter | Check getCapabilities().metrics |
| BONP-007 | Dispute window expired | No remedy; settlement is final |
| BONP-008 | Confidence score too low | Use higher-confidence adapter |
Whoop Adapter
Hits the Whoop Developer API v1. Requires a Bearer access token from Whoop OAuth.
| Metric | Endpoint | Field |
|---|---|---|
recovery_score |
GET /v1/recovery |
score.recovery_score |
hrv_rmssd |
GET /v1/recovery |
score.hrv_rmssd_milli |
resting_heart_rate |
GET /v1/recovery |
score.resting_heart_rate |
spo2 |
GET /v1/recovery |
score.spo2_percentage |
sleep_performance |
GET /v1/sleep |
score.sleep_performance_percentage |
sleep_hours |
GET /v1/sleep |
score.total_in_bed_time_milli ÷ 3.6M |
strain |
GET /v1/cycle |
score.strain |
active_calories |
GET /v1/cycle |
score.kilojoule × 0.239 |
import { WhoopAdapter } from "@bonp/sdk";
const adapter = new WhoopAdapter();
const envelope = await adapter.fetchMetric(
userId, "recovery_score",
{ start: new Date(), end: new Date(Date.now() + 86400000) },
{ access_token: "whoop_oauth_token" }
);
Oura Adapter
Hits the Oura Ring API v2. Requires a Personal Access Token or OAuth Bearer token.
| Metric | Endpoint | Field |
|---|---|---|
readiness_score |
GET /v2/usercollection/daily_readiness |
data[0].score |
sleep_hours |
GET /v2/usercollection/sleep |
total_sleep_duration ÷ 3600 |
sleep_performance |
GET /v2/usercollection/sleep |
efficiency |
step_count |
GET /v2/usercollection/daily_activity |
data[0].steps |
active_calories |
GET /v2/usercollection/daily_activity |
data[0].active_calories |
Apple Health Adapter
Bridge adapter for HealthKit export XML or iOS push data. No live API — works offline.
import { AppleHealthAdapter } from "@bonp/sdk";
const adapter = new AppleHealthAdapter();
adapter.loadExport("/path/to/export.xml");
const envelope = await adapter.fetchMetric(
userId, "step_count",
{ start: new Date("2026-04-19"), end: new Date("2026-04-20") }
);
To use with a live iOS push: pass the parsed
HealthKitReading[] array directly to
loadExport().
Dispute Flow
flowchart TD
A[Settlement submitted] --> B{Within dispute window?}
B -- No --> C[Settlement final]
B -- Yes --> D[openDispute]
D --> E[submitEvidence with counter-envelopes]
E --> F{adjudicate}
F -- No evidence --> G[resolved_for_claimant]
F -- Different provider --> H[open · needs manual review]
F -- Same provider, higher confidence --> I[resolved_for_challenger]
F -- Same provider, lower confidence --> G
H --> J[resolveDispute manual]
import { openDispute, submitEvidence, adjudicate, resolveDispute } from "@bonp/sdk";
const dispute = openDispute(originalEnvelope, "challenger-user-id");
const withEvidence = submitEvidence(dispute, [counterEnvelope]);
const result = adjudicate(withEvidence, originalEnvelope);
// "resolved_for_claimant" | "resolved_for_challenger" | "open"
const resolved = resolveDispute(withEvidence, result, "reason text");
Staleness Proofs
A StalenessProof documents why a specific envelope is
fresh or stale. It is serializable — store it alongside the
settlement for audit purposes.
import { generateStalenessProof, checkFreshness, freshestValidEnvelope } from "@bonp/sdk";
const proof = generateStalenessProof(envelope);
// {
// envelope_id: "c8f0...",
// claimed_timestamp: "2026-04-19T08:32:00Z",
// fetch_timestamp: "2026-04-19T09:01:14Z",
// max_age_ms: 43200000,
// is_fresh: true,
// lag_ms: 1754000
// }
// When you have multiple envelopes, pick the freshest valid one:
const best = freshestValidEnvelope([env1, env2, env3]);
Security Considerations
| Concern | Mitigation |
|---|---|
| Adapter key compromise | Store adapter keys in HSM/KMS. Rotate quarterly. Per-adapter key isolation means one compromise doesn't affect other providers. |
| OAuth token exposure | Tokens must not be logged or persisted. Adapters hold tokens only for the duration of the fetch call. |
| Replay attacks | 32-byte nonce per envelope, tracked in a registry with TTL ≥ dispute_window_ms. Use Redis in production. |
| Oracle collusion |
Use multiple independent oracle operators for high-value
markets. data_hash enables anyone with the raw
API response to verify the claim.
|
| Subject re-identification |
Add a per-market salt:
SHA-256(userId + marketId + staticSalt) for
subject.id.
|