# Identity

Part of the Oya Browser docs. All of it: https://oyabrowser.com/docs.md

## Personas

A **persona** is one identity: a fingerprint, a cookie jar and a proxy, bound together and stable for its life. One persona is one device.

There are two ways to get caught, and they are mirror images of each other:

| Shape | Signal |
| --- | --- |
| One account seen from many device fingerprints | Textbook bot farm |
| One device fingerprint across many accounts, or 1,000 concurrent sessions | Device farm |

Binding the fingerprint to your API key avoids the first and walks straight into the second. Binding it to each browser avoids the second and walks into the first. So the binding sits at the level that actually corresponds to a device:

```
persona = fingerprint + cookie jar + proxy       # one identity, one device
API key = a group of personas                    # your fleet
```

A persona's fingerprint is derived from a stored seed, so it is byte-identical across restarts, a returning session looks like a returning device, not a new one.

```
const p = await oya.personas.create({ name: "acme-ops" });
const browser = await oya.browser.start({ persona: p.id });

await oya.personas.list();     // includes activeBrowsers and maxConcurrent
await oya.personas.remove(p.id);
```

Every API key has a **default** persona whose seed reproduces the fingerprint that key had before personas existed. If you run a single account, nothing changed for you.

### Rotation and concurrency

Rotation means picking a *different* persona, never giving one persona a new fingerprint. `persona: 'auto'` selects the least recently used persona that is still under its concurrency cap.

Concurrency is capped per persona, because one laptop cannot be in a thousand places at once. Named personas default to 2 (a phone and a laptop is plausible); the default persona is uncapped so an existing fleet does not break on upgrade. Past the cap you get a clear 429 rather than a silent breach, and `activeBrowsers` is visible in the dashboard and as a Prometheus metric.

## CAPTCHA

```
await browser.solveCaptcha();                    // explicit
oya.browser.start({ captcha: 'auto' });          // solve as they appear
```

Detects reCAPTCHA v2/v3, hCaptcha and Turnstile. Providers that solve natively, Anchor, Browserbase, Steel, Browser Use, are left to do it rather than paying twice and racing their attempt. Everything else goes to your configured solver (CapSolver or 2Captcha).

Returns `{ solved, method: 'provider' | 'solver' | 'none' }`. A failure returns `solved: false`: a silent no-op that leaves an agent stuck is worse than a clear answer.

Automated solving conflicts with some sites' terms of service. Sessions that used it are recorded in the audit trail so you can see which.

## MFA

```
await oya.personas.setMfa(id, { type: 'totp', secret: 'JBSWY3DPEHPK3PXP' });
await oya.personas.setMfa(id, { type: 'email', url: 'https://mail.example/api/latest' });

const r = await browser.completeMfa();
if (!r.completed) open(r.liveViewUrl);   // finish it by hand
```

TOTP is generated locally (RFC 6238). Email and SMS one-time codes are polled from a relay endpoint you supply, within a bounded window, because the code does not exist yet when the prompt appears. When nothing automated can answer, `liveViewUrl` is where a person finishes; that is also the only workable answer for push-approval MFA.

TOTP seeds are credential material of the same weight as a password: sealed at rest with AES-256-GCM, audited on use, and never returned by the API. The relay URL is checked against private and link-local ranges when you store it *and* on every poll, because a public name says nothing about where it resolves later.
