Start from the linked accounts starter
Thelinked_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
kindisaccount, 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
countis a feature:devices.count,phones.count,emails.countandcards.counton every answer and evaluation, which a query or subscription can filter on asanswers.linked_risk.features.devices.count. What the counts are says what they count. - Thresholds.
holdat 0.85 andreviewat 0.5: hold an account abovehold, and send the band between them to a person.busy_deviceis true whendevices.countis 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: trueto 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.
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.countis how many other accounts the judgment read under the device key: at most 10, the relation’slast_n, and only those created within itswindow. 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.
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 infreshness.fanout, sent at create or changed later with a PATCH, apart from window and last_n, which are in the definition:
windowandlast_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 withinvalid_requestwhen a value already has more, naming it indetails.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 readstale(limit_reached), and the events feed recordsjudgment.limit_reachedwithlimitblock_capand the value inkey. They run again once the value is back within the cap, or when aPATCHraises 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, readingstale(limit_reached), until the window has room or aPATCHraises 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 readsstalewithstale_reasonreferenced_changeduntil its own next write. If an older account should be judged again when a new account links to it, send"scope": {"created_within": null}.
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 everyfanout.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.
The sign-ups’ own judgments are billed on top, one each. All three are well inside the default rolling limit.
What you confirm
Withoutconfirm: 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.