Every mainstream social app follows the same playbook: vacuum up your interests, your behavior, your connections, ship it all to a server farm, and run compatibility algorithms in the cloud. The result is a recommendation that feels eerily accurate — because the company knows everything about you. Serendipity takes a fundamentally different approach. We compute compatibility scores entirely on your phone, and the server never sees a single interest, preference, or trait. Here is exactly how that works.
Why Matching Must Happen On-Device
The argument is straightforward: if a server computes compatibility between two people, it necessarily knows both of their interest profiles. It knows that User A is into mycology, stoic philosophy, and watercolor painting. It knows that User B is seeking a co-founder for a climate tech startup. Even if you encrypt the data at rest and in transit, the server must decrypt it to run the math. That single moment of plaintext comparison is the vulnerability — to breaches, to subpoenas, to internal misuse, and to the slow creep of ad-targeting logic into what was supposed to be a social tool.
On-device matching eliminates this class of risk entirely. Your phone already knows your interests because you entered them. When another person walks into BLE range, their phone broadcasts a compact fingerprint — not their full profile, but a lossy, privacy-preserving summary. Your phone receives that fingerprint, scores it locally, and decides whether to surface a nudge. The server relays encrypted messages after a mutual match, but it never learns why two people matched. It is a mail carrier that cannot read the letters.
The Two-Phase Approach
Serendipity splits resonance matching into two distinct phases, each optimized for a different stage of the encounter.
Phase 1: Quick Resonance (Pre-Filter)
Quick Resonance runs the instant your phone picks up a BLE advertisement from a nearby device. The goal is triage: with potentially dozens of beacons in range at a busy coffee shop, you need to cheaply filter out low-compatibility signals before committing to a full handshake. Quick Resonance operates on the 34-byte TransmissionFingerprint embedded directly in the BLE advertisement payload — no connection required, no data exchange beyond what Bluetooth already broadcasts.
Phase 2: Full Resonance (Post-Handshake)
When Quick Resonance scores above a configurable threshold (default: 0.40), the two devices establish a short-lived encrypted channel and exchange richer profile fragments. Full Resonance then evaluates six weighted dimensions to produce a final compatibility score. This is the score that determines whether to show a notification like "Someone interesting is nearby." We will break down both phases in detail.
The TransmissionFingerprint: 34 Bytes That Represent You
BLE advertisements are limited to 31 bytes of usable payload in legacy mode, or up to 254 bytes with extended advertising. We target 34 bytes to fit comfortably within a single extended advertisement PDU. Every field is chosen for maximum signal density per byte.
struct TransmissionFingerprint {
beacon_id: [u8; 8], // Rotating pseudonymous identifier
tag_hash: [u8; 4], // Hash of current tag (e.g. "builder")
energy_mode: u8, // 0=ghost, 1=low, 2=open, 3=beacon
interest_bloom: [u8; 16], // 128-bit bloom filter of interests
seek_offer_bits: u32, // 32-bit bitmap: 16 seeking + 16 offering
version: u8, // Protocol version for forward compat
}
// Total: 8 + 4 + 1 + 16 + 4 + 1 = 34 bytes
| Field | Size | Purpose |
|---|---|---|
beacon_id |
8 bytes | Rotating ID, changes every 15 minutes to prevent tracking |
tag_hash |
4 bytes | CRC32 of the user's active social tag (e.g. "mentor," "builder," "explorer") |
energy_mode |
1 byte | Current social energy: ghost (invisible), low, open, or beacon (actively seeking) |
interest_bloom |
16 bytes | 128-bit bloom filter encoding the user's top interests |
seek_offer_bits |
4 bytes | Bitmap: upper 16 bits = categories the user is seeking; lower 16 bits = categories the user is offering |
version |
1 byte | Protocol version for forward compatibility |
The beacon_id rotates on a schedule and is unlinkable across rotation periods. This means even someone passively scanning BLE traffic cannot build a movement profile from Serendipity beacons — a property we covered in depth in our article on rotating beacons and zero-knowledge design.
Bloom Filters: Encoding Interests in 128 Bits
A bloom filter is a probabilistic data structure that can tell you "definitely not in the set" or "probably in the set." It uses multiple hash functions to set bits in a fixed-size bit array. For Serendipity, we hash each of a user's interests (normalized, lowercased strings like "rock climbing," "rust programming," "fermenting") using three independent hash functions (MurmurHash3 with three different seeds), each mapping to a position in our 128-bit array.
With 128 bits and 3 hash functions, a user with 10 interests sets approximately 30 bits. The false positive rate — the probability that a random interest appears to match when it does not — is roughly 3.5%. For 15 interests, it climbs to about 8%. This is deliberately acceptable: Quick Resonance is a pre-filter, not a final judgment. A few false positives mean occasionally proceeding to Phase 2 with someone who turns out to be less compatible than the bloom filter suggested. That costs a brief encrypted handshake, not a bad user experience.
Why bloom filters are perfect for BLE: they are fixed-size (exactly 16 bytes regardless of whether you have 3 interests or 30), they require no coordination between devices (each phone builds its own filter independently), and comparison is a single bitwise AND operation followed by a popcount — a few CPU cycles at most.
Quick Resonance: The Scoring Formula
When your phone receives a TransmissionFingerprint from a nearby device, it computes the Quick Resonance score as a weighted sum of four signals:
quick_resonance =
bloom_match * 0.30
+ cross_match * 0.35
+ tag_equality * 0.15
+ energy_compat * 0.20
bloom_match (weight: 0.30) — The interest overlap estimate. We AND the two 128-bit bloom filters together and count the set bits, then divide by the number of set bits in the smaller filter. This gives a Jaccard-like similarity ratio that approximates how much two people's interests overlap, without either phone knowing the other's actual interests.
cross_match (weight: 0.35) — The seeking/offering complementarity. This is the most heavily weighted signal because mutual need fulfillment is the strongest predictor of a meaningful encounter. We AND one user's seeking bits with the other's offering bits, and vice versa, then average the two popcount ratios. If you are seeking "design feedback" and the nearby person is offering "design mentorship," the relevant bits light up.
tag_equality (weight: 0.15) — A binary signal: do both users share the same active tag? If both are broadcasting "builder," there is an immediate tribal affinity. The CRC32 hash comparison is a single 32-bit equality check.
energy_compat (weight: 0.20) — Social energy compatibility. Two people in "beacon" mode are both actively looking — high compatibility. Someone in "low" energy and someone in "beacon" mode get a moderate score. A "ghost" mode user produces a zero for this term and should never be surfaced regardless of other signals. This ensures that social boundaries are respected at the protocol level, not just the UI level.
Full Resonance: Six Dimensions of Compatibility
When Quick Resonance exceeds the threshold, both devices exchange encrypted profile fragments over a short-lived BLE GATT connection. Full Resonance then evaluates six dimensions, each normalized to [0, 1] and combined with carefully chosen weights:
| Dimension | Method | Weight |
|---|---|---|
| Interest Overlap | Jaccard similarity on full interest sets | 0.20 |
| Skill Complementarity | Can-teach / want-to-learn cross-match | 0.25 |
| Problem Alignment | Keyword overlap on current problems/projects | 0.15 |
| Comm Compatibility | Depth preference and communication style scoring | 0.10 |
| Transmission Match | Full seeking/offering category cross-match | 0.25 |
| Synastry | Optional astrological compatibility (user opt-in) | 0.05 |
Interest Overlap (0.20) uses proper Jaccard similarity on the full, unhashed interest lists exchanged during the handshake. Unlike the bloom filter approximation, this is exact: |A ∩ B| / |A ∪ B|. It gets a moderate weight because shared interests alone do not predict meaningful connection — two people who both like hiking may have nothing to talk about beyond trail recommendations.
Skill Complementarity (0.25) is the highest-weighted dimension alongside Transmission Match. It cross-references what one person can teach against what the other wants to learn, and vice versa. A machine learning engineer who wants to learn woodworking, paired with a furniture maker who wants to understand AI — that is the kind of encounter Serendipity is designed to create. The score is the average of the two directional match ratios.
Problem Alignment (0.15) performs keyword overlap on free-text descriptions of current projects or problems. We tokenize, stem, and remove stop words on-device, then compute a simple overlap coefficient. Two people both struggling with "supply chain logistics for small farms" will score highly here even if their interests and skills diverge. Shared problems create immediate conversational fuel.
Comm Compatibility (0.10) receives the lowest non-optional weight because it is a soft preference rather than a hard filter. It compares communication depth preference (small talk vs. deep conversation) and style (direct vs. exploratory). A mismatch here does not prevent a great encounter, but alignment makes the first conversation smoother.
Transmission Match (0.25) is the full-resolution version of the Quick Resonance cross-match. Instead of 16-bit category bitmaps, it operates on the complete seeking/offering lists with semantic similarity. Someone seeking "startup co-founder, technical" will partially match someone offering "engineering partnership" even though the strings differ.
Synastry (0.05) is entirely optional and only included when both users have opted into astrological features. It computes a simplified natal chart compatibility score. The tiny weight means it functions as a tiebreaker and conversation starter, never as a primary matching signal. If either user has not opted in, its weight is redistributed proportionally across the other five dimensions.
Why These Weights?
The weight distribution reflects a specific thesis about human connection: the most meaningful encounters happen when people can help each other, not merely when they share the same hobbies. This is why Skill Complementarity and Transmission Match together account for half the total score (0.25 + 0.25 = 0.50). Interest Overlap matters but is secondary (0.20). Problem Alignment adds contextual relevance (0.15). Communication style and synastry are light touches (0.10 + 0.05).
We arrived at these weights through iterative testing with our early user cohort. Initial versions weighted Interest Overlap at 0.35, which produced matches that felt "too similar" — people found each other agreeable but unstimulating. Shifting weight toward complementarity produced encounters that users described as "surprisingly useful" and "I would never have found this person on a normal app."
Performance: Under 10ms on a Mid-Range Phone
The entire resonance pipeline is designed to be computationally trivial. Quick Resonance involves bitwise operations on small fixed-size arrays — a few hundred nanoseconds on any modern ARM processor. Full Resonance is more involved but still lightweight: Jaccard similarity on sets of 10-30 items, keyword overlap on short text fragments, and simple arithmetic. No matrix multiplication, no neural network inference, no embedding lookups.
On a Snapdragon 695 (a mid-range chipset from 2022), the complete Full Resonance computation for a single pair takes approximately 2-4 milliseconds. Even if ten people simultaneously pass through Quick Resonance and trigger Full Resonance handshakes, the total compute time stays well under 50ms — imperceptible to the user and negligible for battery consumption. We allocate the matching work to a dedicated background thread using a coroutine dispatcher, ensuring the UI thread is never blocked.
Compare this with what happens on a server: Netflix's recommendation engine processes hundreds of millions of user-item interactions through collaborative filtering models that require GPU clusters. Spotify's Discover Weekly runs matrix factorization across 500 million+ users. These systems produce excellent recommendations, but they require centralized infrastructure and centralized data. Serendipity achieves comparable conceptual goals — surface relevant people instead of relevant songs — using math that runs on the device in your pocket.
The Privacy Tradeoff That Is Not a Tradeoff
The conventional wisdom in recommendation systems is that you need more data and more centralized compute to make better predictions. This is true when you are trying to predict what a user wants from a catalog of millions of items. But social matching is a different problem. You are not picking one movie from 50,000. You are deciding whether the one person standing three meters away might be worth talking to. The search space is small (whoever is physically nearby), the feature set is compact (interests, skills, needs), and the judgment is binary (nudge or do not nudge).
This means the privacy-preserving approach is not a compromise — it is genuinely sufficient for the task. A bloom filter with a 3.5% false positive rate is fine when the consequence of a false positive is a single unnecessary handshake, not a bad product recommendation that costs a company revenue. Weighted vector scoring across six dimensions is adequate when you are scoring tens of candidates, not millions. The math is simpler because the problem is simpler, and simpler math can run on-device.
The server never learns your interests. It never learns who you matched with or why. It handles relay encryption and nothing else. That is not a limitation of our architecture — it is the entire point.
Serendipity proves that you do not need to sacrifice privacy for relevance. When the problem is "who nearby is compatible," 34 bytes and a few milliseconds of on-device math are all you need. The server stays out of it, your data stays on your phone, and the best encounters of your week happen because two devices did some quiet arithmetic over Bluetooth.