Skip to main content
One person or group running many accounts leaves traces they share: a device, a phone number, an email address, a card. Write each of the four on every account as an attribute, and the judgment reads, for each, the other accounts with the same value. Each value is one block of a blocking relation, one of the relations, so this is four blocking relations on four keys, each with a count.

Start from the linked accounts starter

The linked_accounts.risk starter is this judgment, ready-made. Fill it with your own paths:
POST /v1/namespaces/acme%2Fsignups/judgments
  • Write each key as one short string of letters, digits, -, _ and ., up to 128 bytes. An email address or a phone number is not such a string: write a hash of it, normalised first (lower-cased, without spaces), as hex. For the card, write the fingerprint your payment processor gives, never the card number. Two accounts are linked through a key only when they hold exactly the same value. Each account holds one value per key, so an account seen on several devices is linked through the one you write.
  • Leave a key out when you don’t have it. An account with no value, or a value that is not such a string, is in no block for that key: it reads no accounts there and no account reads it. Never write a placeholder such as unknown: every account without the key would share it.
  • It judges every account, the documents whose kind is account, and reads each one’s status and the first 50 characters of its name. For each key it reads up to 10 other accounts with the same value, created within 365 days of the newest of them: when each was created, its status, and the first 50 characters of its name. It never reads the account itself.
  • The counts are on the answer. Each key’s count is a feature: devices.count, phones.count, emails.count and cards.count on every answer and evaluation, which a query or subscription can filter on as answers.linked_risk.features.devices.count. What the counts are says what they count.
  • Thresholds. hold at 0.85 and review at 0.5: hold an account above hold, and send the band between them to a person. busy_device is true when devices.count is 5 or more, with no engine involved. These are starting points; post outcomes, such as confirmed fraud or a chargeback, to measure and tune them.
  • A new account waits for quiet. A judgment with features waits until an account has had no write for 10 minutes before judging it. To judge a new account within seconds of its sign-up, send "freshness": {"policy": "on_change", "debounce_ms": 0}.
  • Your own wording. Send dry_run: true to get the filled definition, change it, and create from that. With fewer than four keys, remove the relation and its feature from it: a starter needs all four paths.
In the dashboard, the relation editor’s “Add: Linked accounts” fills the same four relations. Set the judgment’s Applies to to your accounts, and add the four counts to the definition’s features to keep them on the answer. Unless the namespace already has them, the first version also builds a reference index on each of the four attributes, listed as reference_index jobs in the create response’s job_ids; the judgment’s answers are unavailable until they are done. That is four of the namespace’s 8 reference indexes. The context is capped at 1,800 tokens with max_tokens, which keeps each judgment standard in size. Over the cap, linked accounts are left out before any of the account’s own fields are cut, the oldest of one key’s first, and the evaluation records context_truncated: true. The counts are not cut: they still count every account selected.

What the counts are

  • Per key, of the accounts shown. devices.count is how many other accounts the judgment read under the device key: at most 10, the relation’s last_n, and only those created within its window. A count of 10 means 10 or more.
  • Not the number of linked accounts. An account that shares both a device and a card with this one is shown and counted under both keys, so adding the four counts can count one account twice.
  • Not every account ever linked. An account created more than 365 days before the newest one sharing its key is not shown or counted. The 365 days count back from the newest account with that value, not from now and not from this account.
Raise last_n (up to 254) or window in the definition to count further. Every account counted is also shown, so each one is in every judgment’s context and its size class grows with it.

What it is not

  • Not a ring. It reads only the accounts that share a key with this one directly. If account A shares a device with B, and B shares a card with C, A’s judgment never sees C. Finding every account connected through a chain of shared keys is not something a relation does.
  • Not a measured fraud detector. The question, criteria and thresholds are a reasonable start, not a figure for how much fraud it catches on your accounts. Post outcomes to measure it.
  • Not their answers. It reads the linked accounts’ fields, never their answers to this or any other judgment. When you act on an account, write the result to the field the judgment shows, such as its status set to suspended, and the accounts linked to it read that.

What bounds it

Each is a setting in freshness.fanout, sent at create or changed later with a PATCH, apart from window and last_n, which are in the definition:
  • window and last_n: at most 10 accounts per key, created within 365 days of the newest with that value.
  • block_cap, 1,000 by default: the most accounts one key value may hold. A create is refused with invalid_request when a value already has more, naming it in details.key: a value many accounts share, such as a placeholder or a device fingerprint every emulator reports. Clear it from those accounts, or leave that key out. A value that grows past the cap later waits, whatever the rolling limit: the accounts sharing it read stale (limit_reached), and the events feed records judgment.limit_reached with limit block_cap and the value in key. They run again once the value is back within the cap, or when a PATCH raises it.
  • rolling_limit, 300,000 by default: the most accounts that changes to their keys may re-judge in any 30 days. Past it, the accounts that fit are re-judged and the rest wait, reading stale (limit_reached), until the window has room or a PATCH raises the limit. The values that have spent the most of the limit wait first, so one busy device cannot leave every other account stale.
  • scope.created_within, 30 days by default: an account created longer before a change to its key is not re-judged. It keeps its answer and reads stale with stale_reason referenced_changed until its own next write. If an older account should be judged again when a new account links to it, send "scope": {"created_within": null}.
See the rolling limit for how deferral catches up.

What it costs

An account’s own writes re-judge it, as for any judgment. The cost to plan for is the other direction: a change to what a key value shows re-judges every account sharing that value.
re-judgments a month = key value changes a month × accounts sharing the value inside the re-judge scope
  • A key value changes when an account gains or loses it, such as a new sign-up on a device, or when a field the judgment shows changes on an account that holds it, such as a status set to suspended. An edit to a field the judgment does not show changes nothing.
  • After a quiet period, each value’s accounts are read once and compared with what they showed before: once the value has had no change for freshness.fanout.debounce_ms, 10 minutes by default, or at most once every fanout.max_wait_ms, 1 hour by default, while changes keep coming. Nothing shown changed: no engine call and nothing counted. Something did: every account sharing the value inside the scope is re-judged, and one whose context did not change is not billed but counts toward the rolling limit.
Each account’s context and question come to under 2,000 tokens: a standard judgment, at $0.25 per 1,000. The sign-ups’ own judgments are billed on top, one each. All three are well inside the default rolling limit.

What you confirm

Without confirm: true, the create returns its cost estimate and creates nothing, with fanout.cost_usd_at_limit, the most changes to key values can cost in any 30 days. replay runs the last 30 days of writes through the judgment: each account’s own writes, and every account created in that time re-judging the accounts that share its values. It cannot see a change to an existing account, such as a suspension, so it says "excludes": ["candidate_edits", "candidate_deletes"] with lower_bound: true, and its figures are “at least”. The rolling limit bounds what it cannot count. Sending confirm: true agrees to both, and no change to a key waits for a person after that.