# User Graph Traversal

> Traverse a Warp user's terminal ID / user graph to find all accounts that have shared the same computer (anonymous terminal ID). Use when investigating whether a user has multiple accounts, finding connected fraud rings via shared devices, or asked to "traverse the user graph", "find related accounts", "find accounts sharing the same computer", or "find connected users" for a given email or user.

- Skill: `warpdotdev/user-graph-traversal` (Agent Skill)
- Install (CLI): `npx skillmds@latest add warpdotdev/user-graph-traversal`
- Raw SKILL.md: https://api.skillmd.com/api/skills/warpdotdev/user-graph-traversal/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: warpdotdev (https://skillmd.com/u/warpdotdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/warpdotdev/user-graph-traversal

---


# User Graph Traversal

When a user creates a Warp account, their computer's anonymous terminal ID is linked
to their user ID. This creates a graph: multiple accounts that have logged into the
same machine share terminal IDs and can be connected via 2-hop traversal.

## Schema config
The `bq` query below uses the **default** logical mapping: project `YOUR_GCP_PROJECT`
and tables `core_user_facts`, `int_user_id_to_terminal_anonymous_id_mapping`,
`core_ai_cycles_clean` in the `prod` dataset. Replace them with your physical names
from `config/schema.json` (see `SCHEMA.md`).

## Query (seed by email)

```bash
bq query --use_legacy_sql=false --format=prettyjson '
with
seed as (
    select user_id
    from `YOUR_GCP_PROJECT.prod.core_user_facts`
    where user_email = "USER_EMAIL_HERE"
)
, seed_anon_ids as (
    select distinct terminal_anonymous_id
    from `YOUR_GCP_PROJECT.prod.int_user_id_to_terminal_anonymous_id_mapping`
    where user_id in (select user_id from seed)
      and terminal_anonymous_id != ""
)
, users_hop1 as (
    select distinct user_id
    from `YOUR_GCP_PROJECT.prod.int_user_id_to_terminal_anonymous_id_mapping`
    where terminal_anonymous_id in (select terminal_anonymous_id from seed_anon_ids)
      and terminal_anonymous_id != ""
)
, anon_ids_hop2 as (
    select distinct terminal_anonymous_id
    from `YOUR_GCP_PROJECT.prod.int_user_id_to_terminal_anonymous_id_mapping`
    where user_id in (select user_id from users_hop1)
      and terminal_anonymous_id not in (select terminal_anonymous_id from seed_anon_ids)
      and terminal_anonymous_id != ""
)
, users_hop2 as (
    select distinct user_id
    from `YOUR_GCP_PROJECT.prod.int_user_id_to_terminal_anonymous_id_mapping`
    where terminal_anonymous_id in (select terminal_anonymous_id from anon_ids_hop2)
      and terminal_anonymous_id != ""
      and user_id not in (select user_id from users_hop1)
)
, connected_users as (
    select user_id from users_hop1
    union distinct
    select user_id from users_hop2
)
, current_cycles as (
    select
        user_id
        , total_credits_used as total_requests_used
        , plan_type_at_cycle_end
    from `YOUR_GCP_PROJECT.prod.core_ai_cycles_clean`
    where is_current_cycle
)
select
    cu.user_id                                          as firebase_uid
    , cuf.user_email                                    as email
    , cuf.signup_timestamp                              as created_at
    , coalesce(cc.total_requests_used, 0)               as total_requests_used
    , case
        when coalesce(cc.plan_type_at_cycle_end, "free") = "free" then "free"
        else "active"
    end                                                 as paid_status
    , cuf.reputation_status
from connected_users cu
join `YOUR_GCP_PROJECT.prod.core_user_facts` cuf
    using (user_id)
left join current_cycles cc
    using (user_id)
order by total_requests_used desc
'
```

## Output

| Column | Description |
|---|---|
| `firebase_uid` | User ID |
| `email` | Account email |
| `created_at` | Signup timestamp |
| `total_requests_used` | AI requests used in current cycle |
| `paid_status` | `free` or `active` (paid) |
| `reputation_status` | e.g. `unreputable`, `reputable`, `reputable_from_override` |

## Output Guidance

**Filter by reputation status first.** Skip any user with `reputation_status` of `reputable` or `reputable_from_override` — these have already been cleared and are not worth investigating. Focus only on users with `reputation_status = unreputable` or null.

After running the query, report:

**Summary stats**
- Total connected users, broken down as free vs. active (e.g., "Found 47 connected users: 44 free, 3 active")
- Total AI requests consumed by free accounts in the cluster

**Active (paid) users** — list each with email and requests used. These are the most suspicious:
- Many free accounts surrounding a paid account may mean the paid account used a stolen credit card to fund credit farming
- Flag: "Users X, Y, Z are 'active' — worth checking for credit card fraud"

**Signup timeline** — scan the `created_at` values for bursts:
- Multiple accounts created on the same day = strong fraud signal
- Flag any days with 3+ signups (e.g., "10 accounts signed up on 2025-11-03")

**Fraud signal strength** — summarize your read:
- 🔴 High: many free accounts, signup bursts, active accounts present
- 🟡 Medium: moderate cluster size, no clear bursts
- 🟢 Low: small cluster, organic signup spread, matches a household/team

