
TS9: Automated Account Risk Scoring
How we built an internal fraud detection system that computes daily risk scores from audio fingerprints, IP signals, streaming patterns, and AI conversation analysis.
ONCE Engineering
Music distribution platforms have a fraud problem. Copyright infringement, artificial streaming, alt-account abuse, and referral gaming are constant threats. Most platforms rely on manual review or third-party reports that arrive weeks after the damage is done. We built TS9, an automated risk scoring system that evaluates every account on the platform daily and surfaces the ones that need attention.
Architecture
TS9 runs as a daily cron job on a 5-minute execution budget. It loads the entire account universe, computes a set of boolean flags per account from multiple data sources, and produces a weighted risk score. Results are written to two database tables: a latest-snapshot table for the admin UI and a history table for trend charts.
┌─────────────────┐
│ Cron Job │ 07:30 UTC daily
│ /api/cron/ts9 │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Load Profiles │────▶│ Compute │────▶│ Score & │
│ + Data Sources │ │ Flags │ │ Persist │
└─────────────────┘ └──────────────┘ └──────────────┘
│ │ │
┌────┴─────┐ ┌─────┴─────┐ ┌────┴────┐
│ Profiles │ │ 18 flag │ │ Latest │
│ Releases │ │ checks │ │ History │
│ Tracks │ │ per acct │ │ Tables │
│ IPs │ └───────────┘ └─────────┘
│ Referrals│
│ Playlists│
│ Streams │
│ Note logs│
└──────────┘
The key design constraint: TS9 must be self-contained. It doesn't call external APIs during scoring. Everything else is pre-computed and stored in our database. This keeps the cron job fast and deterministic.
The Flag System
TS9 defines 18 boolean flags, each representing a specific risk indicator. A flag is true (signal present), false (signal absent), or null (data unavailable or flag disabled). The distinction between false and null matters: false means we checked and the account is clean; null means we couldn't check.
Flags are grouped into six categories:
Audio Fingerprint Signals
When a release is distributed through ONCE, tracks are checked against our ACR-provider's audio fingerprint database. We persist the maximum similarity score per track in a dedicated column. TS9 checks these scores at a 0.9 threshold:
acr_high_match_one(MED): Exactly one track with high-confidence similarityacr_high_match_multiple(HIGH): Multiple tracks flagged
A legacy fallback reads NOTIFIED_POSSIBLE_INFRINGEMENT: markers from internal release notes for coverage on older releases that predate the stored similarity column.
Rights Rejection Tracking
TS9 tracks releases that were rejected for rights/copyright issues by cross-referencing distribution status with a conservative keyword list across distribution errors, store-status snapshots, and Note metadata:
const RIGHTS_REJECTION_KEYWORDS = [
'copyright', 'rights', 'infring', 'dmca', 'unauthoriz',
'licensed', 'ownership', 'non-exclusive', 'third-party',
'content id', 'not eligible for youtube content id',
] as const;
The system separates total rejections from rights-specific rejections. One rights-issue rejection triggers a MED flag. Three or more triggers a HIGH flag at 100 points, which alone can push an account into critical severity.
IP and Account Relationship Signals
This is the most complex flag category. TS9 builds a per-account IP candidate set from three sources with different trust levels:
- Signup IP (highest trust): Captured at registration
- Verification IP (high trust): Captured at email verification
- Observed IPs (conditional trust): Recorded during sessions, only included if seen within the last 120 days
┌──────────────────────────────────────────────┐
│ IP Candidate Set (per account) │
├──────────────────────────────────────────────┤
│ signup_ip ← always included │
│ verification_ip ← always included │
│ observed IPs ← only if recent (120d) │
└──────────────────────────────────────────────┘
The flagging rule is source-aware: any overlap on a signup or verification IP triggers immediately. Observation-only overlaps require at least two distinct matching IPs to reduce false positives from shared networks.
Three flags use this IP infrastructure:
shared_ip_with_terminated(100 pts): Account shares an IP with a suspended accountpossible_alt_account_detected(MED): Alt-account signal from profile fields or IP overlappossible_alt_account_strong_signal(HIGH): Strong alt-account signal (same OAuth, phone, or IP)
Streaming Pattern Detection
TS9 detects potential artificial streaming by comparing a recently distributed release's Spotify streams against the account's baseline. The check targets releases distributed in the last 14 days and looks at consumption data from the last 60 days:
- Minimum threshold: 500 Spotify streams on the candidate release
- Ratio to account average: >= 5x
- Ratio to account max: >= 3x
Both ratio thresholds must be met simultaneously. This avoids false positives on accounts where one release simply outperforms the rest organically.
Playlist Context Signals
TS9 fetches Spotify playlist movement data and scans playlist titles for concerning keywords. If an account's tracks appear on playlists titled "unreleased" or "leaks," that's a strong indicator of unauthorized distribution. Both flags carry HIGH severity.
The scan processes playlist movements in chunks, covering Added, Active, and Removed states across a 28-day window. Evidence includes up to 5 sample entries per flag with playlist names, track names, and Spotify URLs for admin review.
Content Pattern and Referral Signals
Two lower-severity flags round out the system:
sped_up_nightcore_slowed_over_half_releases(LOW): More than half of an account's releases contain "sped up," "nightcore," or "slowed down" in the title. A heuristic for low-effort derivative content.self_linked_referrals_over_two(LOW): More than two referrals where the referrer and referred accounts share an IP. Uses the same source-aware matching as the IP flags.
AI Conversation Safety Signal
This is where TS9 intersects with Note. When Note summarizes a conversation, the summary includes a nefarious activity assessment. If Note observes strong indications that a user may be seeking help with illegal or abusive activity (copyright infringement distribution, fraud streaming), the signal is persisted and picked up by TS9.
The signal uses time-aware thresholds to balance recency with pattern detection:
- Any strong signal in the last 30 days triggers the flag
- OR 2+ strong signals in the last 90 days
- OR 3+ strong signals in the last 180 days
This means a single recent signal is actionable, but older signals need repetition to stay active.
Scoring
Each flag has a base weight determined by severity:
| Severity | Default Weight |
|---|---|
| LOW | 10 points |
| MED | 25 points |
| HIGH | 50 points |
Three flags carry policy overrides at 100 points: shared_ip_with_terminated, rights_rejected_multiple, and spotify_recent_release_disproportionate_streams. These represent behaviors serious enough to warrant near-immediate review.
The risk score is the sum of weighted contributions from all enabled flags that are true. Severity is bucketed:
| Score | Severity |
|---|---|
| 0 | none |
| 1–24 | low |
| 25–49 | medium |
| 50–99 | high |
| >= 100 | critical |
Data Quality Multipliers
Not all signals have the same confidence. An ACR similarity flag backed by fresh scans is more reliable than one relying on stale data. TS9 applies per-flag quality multipliers in the range [0, 1] that reduce a flag's contribution when data freshness or coverage is weaker.
For ACR flags, the quality score blends checked coverage ratio with freshness ratio. For IP flags, it drops to 0.45 when the observation table is unavailable and 0.7 when no candidate IPs exist for an account. The final points for any flag are weight * qualityMultiplier, truncated to an integer.
Outcome-Calibrated Weights
TS9 doesn't use static weights blindly. Before scoring, it runs a calibration pass that compares each flag's exposure rate against the actual suspension rate in the current account population. The idea: if accounts with a particular flag are suspended at a higher rate than baseline, that flag should carry more weight.
The calibration computes a lift ratio (exposed suspension rate / baseline suspension rate), clamps it to [0.5, 2.5], and smooths it based on sample confidence. Flags with fewer than 15 exposures get no calibration adjustment. Flags with 120+ exposures get the full multiplier. Everything in between is linearly interpolated.
This means TS9's scoring model self-adjusts as the platform's fraud landscape evolves, without manual weight tuning.
Admin UI
The TS9 admin page surfaces results through a searchable, filterable table sorted by risk severity. Admins can:
- Search by email or name
- Filter by flag presence or specific flag
- Click into any account for a detail modal showing:
- Current risk score and severity
- Full flag breakdown (true/false/disabled/unknown per flag)
- Risk history chart (7d / 30d / 90d / all time)
- Evidence details for every flagged signal
- Suspend accounts directly
- Mark accounts as "No Risk" to exclude them from future scoring
The No Risk override persists across cron runs. Overridden accounts are filtered out at the start of each daily computation, so they never appear in results until the override is cleared.
Resilience
TS9 is designed to degrade gracefully. Every data source fetch is wrapped in try/catch logic that checks for missing tables or columns. If a migration hasn't been applied yet, the affected flags are set to null (unknown) rather than false (clean). The cron job continues running with reduced coverage rather than failing entirely.
This pattern was critical during rollout. We shipped the flag definitions and scoring logic first, then added data source integrations incrementally. Each new integration expanded TS9's coverage without requiring coordinated deploys.
The same resilience applies to the Note nefarious signal. If the note_activity_logs table is unavailable, TS9 falls back to previously persisted signals from the ts9_account_risk table. The quality multiplier drops to 0.7 to reflect the reduced confidence, but the signal isn't lost.
What's Next
We're working on a few expansions:
- Real-time flag updates: Currently TS9 runs daily. We're exploring event-driven updates for high-severity flags (like shared IP with terminated) so they surface within minutes instead of waiting for the next cron cycle.
- Additional external integrations: Several flags are defined but disabled, waiting for data source integrations: known fraud lists, DMCA takedown notices, YouTube/Meta copyright claims, and artificial streams reports. Each one adds a new dimension to the scoring model.
- Threshold auto-tuning: The outcome calibration currently adjusts weights. We're looking at extending this to automatically tune flag thresholds (like the ACR similarity cutoff or streaming ratio minimums) based on historical true-positive rates.
TS9 runs automatically every day. No user action required — it protects the platform behind the scenes.