<!-- topic: overview -->
# Authoring a Reclaim provider (Builder backend)

For claimant-facing Builder-client integration, result verification, or optional
TEE binding, read topic `integration`. Keep URLs without `api=2` on the
existing legacy path.

<!-- common: terminology -->

## The loop, at a glance

Copy this checklist and check off each step as you finish it. Read the linked
topic before a step you aren't already fluent in.

- [ ] 1. `attach_browser` — pick a browser source with the developer.
      → topic `browser`
- [ ] 2. `list_tabs` → `start_capture(tabId)` ⇒ **`captureId`**, your handle for
      everything below. → topic `capture`
- [ ] 3. Drive the page: `navigate`, `wait_for_page`, `eval_in_page`,
      `get_cookies`. Detect login, land on the page showing the value, trigger
      the request. → topic `capture`
- [ ] 4. `find_requests_containing` → `analyze_request_constraints` → pick an
      **authenticated** request. → topic `capture`
- [ ] 5. `propose_provider(captureId, requestId, …)` ⇒ **`draftId`** (recipe +
      captured secrets, held server-side — secrets never enter the transcript).
      → topic `draft`
- [ ] 6. Parameterize user-specific URL segments; mark secrets. → topic `params`
- [ ] 7. `replay_request(captureId, draftId)`, then again with
      `withoutSecrets: true`. → topic `prove`
- [ ] 8. `run_proof(captureId, draftId, ownerAddress)`.
      → topics `prove`, `credentials`
- [ ] 9. Resolve a `providerId` (**reuse before create**) →
      `create_provider_version_from_capture` → `publish_provider_version` or
      `submit_provider_version_for_review`. → topic `publish`

Multi-request provider? Run steps 5–8 once per request, then publish every
proven draft in one call. See topic `publish`.

<!-- topic: contributions -->
## Public provider contributions

For an asynchronous volunteer contribution, read
[community-contributions.md](community-contributions.md) before using Builder.
It defines the history-first workflow, approved-field allowlist, fixed-US
egress, privacy boundaries, generalized recipe checks, and candidate review.
Never modify a published version in place; create a candidate and submit it
for review.

<!-- topic: auth -->
## Getting authenticated

**Check first:** call `get_me`. A profile back means you're authenticated —
proceed.

**On 401:** `authenticate({})` starts a device-pairing session. It returns
immediately with a `code` and `attachUrl` — it does NOT poll.

1. Tell the user: _"Open `<attachUrl>`, sign in, and click 'Authorize this
   device'."_
2. After they confirm, call `authenticate({ code: "<code>" })` —
   `LINKED` → proceed; `PENDING` → wait and call again; `EXPIRED` → start over.

The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs
(`RECLAIM_API_TOKEN` in the server env overrides it).

<!-- topic: devtools-migration -->
## Import providers from devtools

Use these operations only when the developer is migrating an existing legacy
integration. Prefer Builder for new integrations.

### Import from devtools

1. Get the target `orgId` with `list_orgs`.
2. Ask the developer for the legacy provider UUID.
3. Call `import_devtools_provider(orgId, providerId)`.
4. Use the returned provider UUID for all Builder operations. Import preserves
   the UUID and latest legacy semantic version. It claims an anonymous catalog
   seed idempotently, but never moves a provider from another Builder org. The
   server reads the public devtools API; no legacy token is required.

<!-- topic: browser -->
<!-- common: browser -->

<!-- topic: capture -->
## Capture

1. **Attach** — topic `browser`.

2. **Pick a tab and start the capture.** `list_tabs`, then
   `start_capture(tabId)` ⇒ a **`captureId`**: the session handle every browser
   and inspect tool takes as its first argument (`navigate`, `eval_in_page`,
   `wait_for_page`, `get_cookies`, `list_requests`, `find_requests_containing`,
   `propose_provider`). If a tab is already on the right origin use it,
   otherwise the first is usually fine. Capturing from the start is harmless —
   it also records the login navigation. Then `navigate(captureId, url)` to
   the site you inferred from the developer's request ("GitHub username" →
   `https://github.com`). Don't ask the developer to navigate.

3. <!-- common: login-detect -->

4. **Navigate to the page that displays the value.** Construct the URL from what
   you know about the site; eval for anything you need (for example, GitHub's
   `meta[name="user-login"]`'s `content`), then `navigate(captureId, url)`.

5. **Trigger the request.** Capture has been live since step 2, so make the
   call fire: `eval_in_page(captureId, "location.reload()")`, or click an
   element / hit a SPA route programmatically. Don't ask the developer to click.

6. <!-- common: identify-value -->

7. <!-- common: find-request -->

<!-- topic: draft -->
## Draft the provider

<!-- common: oprf -->

**Call `propose_provider`.** It returns a **`draftId`** — the handle for
`replay_request` and `run_proof` — plus the secret-free `provider` and
`secretRefs`. The recipe **and the captured secret values** live server-side
under that `draftId`; the secrets never enter the transcript, so you carry the
`draftId`, not the provider JSON, into replay/prove.

**Extracting more than one value?** Prefer a **single request whose response
contains all of them** and draft them together — one `responseMatches` +
`responseRedactions` pair per value. Find that request by running
`find_requests_containing` for each value and picking a `requestId` common to
all. Then pass a `targets` array, each entry with its `value` (exactly as it
appears) and a distinct `name`:

```
propose_provider(captureId, requestId, name, targets: [
  { value: "Scratch-net", name: "username" },
  { value: "Alex",        name: "name" },
])
```

The shorthand `propose_provider(captureId, requestId, target, name, hash?)`
still works for one target. Per-target `hash` works inside `targets` too.

**Inspect what comes back:**

- Is the right header marked secret? (cookies, `Authorization`, `X-*` tokens)
- Is `responseMatches[0].value` a `contains` with a `{{name}}` template matching
  the captured value's structural context?
- Is `paramValues[name]` the original captured value?
- Does each `responseRedactions` entry carry an `xPath`/`jsonPath` anchor
  **plus** a named-capture `regex` isolating the value? The synthesizer adds the
  regex in both cases — an HTML reveal is the element markup and a jsonPath
  reveal is the surrounding `"key":"value"`, so neither is the bare value
  without it. (With `hash`, the regex also carries `hash: oprf-…`.)

**Provenance — match the structure, never the bare value.** The `contains`
template and the redaction regex are always anchored on the value's surrounding
context — the JSON key (`"login":"{{username}}"`) or the enclosing element
(`<div class="handle">{{username}}</div>`) — never `{{username}}` alone. That
ties the proof to *where the value legitimately appears*; a bare-value match
would let a user surface the same string from any field they control. The
synthesizer does this for you.

**Canonical drafted shape** (no hash):

```json
{
    "responseMatches": [
        { "type": "contains", "value": "\"login\":\"{{username}}\"" }
    ],
    "responseRedactions": [
        {
            "xPath": "//script[@id=\"client-env\"]",
            "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\""
        }
    ],
    "paramValues": { "username": "Scratch-net" }
}
```

With `hash: 'oprf-raw'`, the redaction gains `"hash": "oprf-raw"`. Nothing else
changes.

<!-- common: rewrite-shape-block -->

<!-- topic: params -->
## Parameters: extracted, consumer-supplied, and secret

**Parameterize user-specific URL segments** so the provider isn't pinned to one
user. The matcher already puts the *captured* value in `paramValues`. If the
consumer knows a URL value before verification and the proof must be pinned to
it, make it a **consumer-supplied context param**. At verification time, its
value comes from the consumer's session `context`, not from extraction.

Replace them with `{{context.<name>}}`, for example
`https://github.com/Scratch-net/repos` →
`https://github.com/{{context.username}}/repos`, with `paramValues: { …,
"context.username": "Scratch-net" }` for replay/proof (the key must match the
placeholder exactly, dot included).

- Query params with user-specific values → same treatment.
- Generic segments (`/api/v3`, `/users`, `/repos`) stay literal.
- No user-identifying segments (for example, `/settings/profile`) → leave it
  alone.

Use a bare `{{name}}` only when the value can be observed in the same live
request. A multi-request recipe does not bind a value extracted from one
response to a later request. If proving that relationship is required, use a
request that exposes both values, require the value as consumer context, or
stop and explain that the current recipe format cannot prove the dependency.

Declare them in the version's `requiredContext` at publish time (topic
`publish`).

<!-- common: secrets -->

The `context.` prefix is a **Builder-only namespace** (consumer-supplied vs
extracted-from-traffic). The attestor itself only looks up `{{whatever}}` in
`paramValues`/`secretParams.paramValues` and doesn't care about the prefix.

<!-- common: no-rename-param -->

### How the verification client resolves these at runtime

- Requests match by **exact origin+path** (query ignored). A
  `{{context.<name>}}` segment matches **only the value the consumer sent**
  (validated against `requiredContext`); a bare `{{name}}` segment matches any
  one path segment and its value is **auto-extracted from live traffic**.
- Interpolation and `contains` matching are **plain text** — JSON, XML, HTML,
  form-encoded, anything.
- Extract selectors apply **client-side in spec order xPath → jsonPath →
  regex**, each to the prior selection. Only the FIRST selector constrains the
  full response (an `xPath`→`jsonPath` chain reads JSON embedded in HTML; a
  jsonPath-first extract needs the whole response to be JSON). Only the selected
  portions reach the attestor.
- A matched response that can't satisfy the recipe is **skipped, not failed**
  (the first selector can't operate on it, or a required `contains` is missing).
  The client keeps polling until timeout.
- So: `{{context.<name>}}` when the consumer knows the value up front and the
  proof should be **pinned** to it; bare when the value should come from
  whatever the logged-in user's traffic shows.

Builder-mode Verification Clients include one migration fallback for old
DevTools providers: when a recipe uses bare `{{name}}` instead of
`{{context.name}}`, the client seeds a missing `parameters.name` from scalar
session `context.name`. An explicit parameter wins. Do not rely on this
fallback for new providers; keep using `{{context.<name>}}` so
`requiredContext` is derived and validated before the session starts.
Builder-owned `reclaimSessionId` and `attestationNonce` remain context-only.

`propose_provider` returns `verificationWarnings` for calls only an author can
make (currently: a captured value baked literally into the URL, needing the
`{{context.<name>}}`-vs-bare-vs-recapture decision). Advisory, never blocking —
**surface each one verbatim** and resolve it before publishing.

<!-- topic: prove -->
## Replay, then prove

**1. Replay.** `replay_request(captureId, draftId)`:

- `matchesOriginal: true` → proceed.
- `false` with `hints` mentioning auth/csrf → re-trigger the action yourself
  (navigate or reload with the tools) and redraft. Don't ask the developer to
  redo it.
- `false` with a stable diff (timestamps, and so on) → tighten the matcher
  and retry.

**2. The authentication-bound check — mandatory.**
`replay_request(captureId, draftId, withoutSecrets: true)` re-issues the request
with no secrets/cookies, as an anonymous visitor.

- FAILS to extract (401/403/redirect, or the value is gone) → the endpoint is
  auth-bound. Good: the proof genuinely means "the holder of these secrets saw
  this value."
- STILL extracts the value → the endpoint is **PUBLIC**; the proof binds to
  nobody. **Do NOT prove it.** Find a request whose response depends on the
  session (an API keyed to the logged-in user — `/settings/*`, a
  `viewer`/`currentUser` API) and re-check. If the value only ever appears on
  public responses, tell the developer it can't be proven as an identity claim.

**3. Prove.** `run_proof(captureId, draftId, ownerAddress)`. On
`verified: true` the full claim is written to
`~/.reclaim/claims/<timestamp>.json` and you get
`{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`.
`claim` is display-ready (huge `request` payload stripped, byte arrays base64'd)
— **show it to the developer as a fenced JSON block at the end**, with the
provider JSON. That's the final result.

**`errorKind: "missing-owner-key"`** → no eth proof-owner key yet. Tell the
developer what's happening and drive the bootstrap yourself — see topic
`credentials`. No restart needed, and the existing `draftId` stays valid.

<!-- common: prove-tail -->

<!-- topic: publish -->
## Publish

`provider` is the JSON from the draft step with the `params` URL
parameterization applied. `initialUrl` is the page where the user logs in / the
value is displayed (for example, `https://github.com/Syed`) — **not** the API
endpoint URL inside the provider.

**1. Reuse before create — this is how duplicates happen.**
`create_provider_version_from_capture` requires a `providerId`. Before minting a
new one, search your existing providers with `providers` (by domain / data
point) — a site you've already built is there. Match → pass its
`providerId` and the capture lands as a NEW VERSION on it. Only call
`create_provider(orgId, domain, slug, title, description)` when none exists
(`orgId` from `list_orgs`; `domain` such as `github.com`, `slug` such as
`github-username`, `title` such as "GitHub Username"). It starts PRIVATE with an
empty seed version. Routing every run through `create_provider` is what stacks
duplicates in "My providers".

**2. Persist the version.**
`create_provider_version_from_capture(providerId, provider, initialUrl, notes)`
— `notes` such as `"Initial draft from browser capture"`. It lands on the
`draft` branch. This always creates a new immutable semantic version. Omit
`version` and `bump` for the default patch bump; set `bump` to `major`, `minor`,
or `patch`, or pass the exact higher `version` requested by the user. Never edit
or archive an existing version to apply provider changes.

- **Multi-request provider** (landing call + details call, a
  CSRF-preflight/refresh-token chain, …): run capture→draft→replay→prove ONCE
  PER REQUEST, then pass `providers: [draftA, draftB, …]` instead of the single
  `provider`. Each becomes its own request on the version and **all** are
  required at verification time. This proves that every request matched; it
  does not automatically bind a value extracted from one response to another
  request.
- **Login/navigation-gated data:** supply `jsUserScripts` (plain page JS that
  drives login/waits/navigation so the target request fires), or
  `allowedJsRequests` (extra requests the script may fire, for example,
  pagination). → topic `user-script`. Before populating `allowedJsRequests` for
  a `verifyProof({ providerId })` use case, read topic `hash-validation` — a
  session-unique URL there makes verifyProof reject every real proof.

**3. `requiredContext` is derived for you.** The publish tool scans the recipe's
url/body for `{{context.<name>}}` placeholders and emits a JSON Schema requiring
each — the Builder then validates every session-create's `context` against it (a
clear 400 instead of a late attestor failure). Extra context fields are always
allowed; it's optional when the recipe has no `{{context.*}}` placeholders.
Derived properties are plain strings. To refine types or add descriptions,
pass the complete `requiredContext` in the same
`create_provider_version_from_capture` call:

```json
{ "requiredContext": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string", "description": "GitHub username to verify" } } } }
```

**4. Go live.** PRIVATE → `publish_provider_version`. PUBLIC →
`submit_provider_version_for_review(providerId, version)`, then tell the
developer it's awaiting admin review.

**5. Tell the developer how consumers call it.** Sessions must include the
context params — for example, `context: { "username": "…" }` in
`create_verification_session`. Quote the `requiredContext` from the created
version verbatim — a session missing those fields gets a 400, and verification
matches the supplied values exactly.

**6. If the response includes `_notes` for an attached browser, follow them for
an optional authenticated-state retest.** Only when its profile is still signed
in, navigate to `initialUrl`; if a user script is present, run
`test_user_script` with that script and `initialUrl`. When the existing capture
has enough data, replay and prove every required request. Do not clear
cookies or force another sign-in. Treat an unavailable or inconclusive retest as
advisory and do not block publication. Ask whether the developer wants to keep
the session for further edits; call `dispose_browser` only after they confirm.
A `builder` session is quota-accounted, so don't leave it idle.

<!-- topic: discover -->
## Discover and resolve providers for the developer's app

When a developer wants to add verification but doesn't know which providers to
use, run the same flow the Discover UI does:

1. Ask which website their users prove data from.
2. `discover_data_points({ domain })` → what's verifiable there. Confirm the
   data points with the developer.
3. `discover_resolve({ domain, dataPoints })` → the smallest provider set
   (`providers`, greedy combine), the `requiredContext` inputs to collect, and
   any `uncovered` points (author those with the flow above, then re-resolve).
4. `create_verification_session({ providers: [{ providerId }, …], context,
   verificationClientUrl })` with EVERY resolved provider id and the collected
   context.

`merge.mergeable` only signals the set *could* collapse into one provider — you
still pass them all; the Builder runs them in one session.

For client integration, result verification, or TEE binding, read topic
`integration`.

<!-- topic: integration -->
## Integrate a Builder Verification Client

**Verification sessions.** Consumers authenticate with a per-org token
(`rorg_…`, issued in the dashboard via `POST /orgs/{orgId}/token`) and create
sessions via `POST /verifications/sessions`. Results are ECIES-encrypted
(secp256k1 + AES-256-GCM) to the org's eth public key when `canEncryptResult` is
enabled (set via `PUT /orgs/{orgId}/keypair`) and delivered to the org's
callback subscriptions; plaintext otherwise. Troubleshoot with
`get_verification_session`, `list_verification_events`,
`list_org_verification_sessions`.

For a claimant-facing Builder-mode integration, share the returned
`verificationUrl` (it contains the session id and `api=2`). Append client-owned
redirect parameters only after session creation. Append `diag=1` only for a
deliberate troubleshooting launch; it can enable logs containing personal data.
The registered client resolves its own `x-reclaim-vc-id`; do not invent a
client token or put callback/redirect fields in the session request. Consumers
should verify terminal deliveries with `verifyResultFull` and the session's
trusted issuer.

An optional `themeId` on session creation selects an active theme owned by the
organization. Omit it to snapshot the organization's most recently updated
active theme. A theme's localized `consent` block makes supported clients show
consent before the first provider; no block means no consent screen. Theme and
branding data never affect proof trust.

**Browser extension.** For the exact `api=2` URL, use
`reclaimExtensionSDK.fromVerificationUrl` or `initBuilder` with the session's
`verificationClientId`, then call `startVerification()`. The extension
generates and persists `claimantClientId` when omitted, resolves recipes from
bootstrap, runs them sequentially, and sends `x-reclaim-vc-id` to every bridge
route, including session-bound `attestor-auth`. It forwards raw legacy proofs
inside the Builder result and does not perform client-side `allowedJsRequests`
validation. Builder recipes support dotted `{{context.key}}` templates; the
extension forwards matching session context to claim creation. Builder mode
skips legacy offscreen session-status calls and uses the Builder bridge for
session lifecycle updates. It emits `verification_result_submitting` and
`verification_result_submission_failed` when applicable. It does not send
terminal success or error as a separate best-effort event; the results request
makes the bridge record the matching terminal event strongly before signing
and storing result deliveries. Missing or unknown `api` values stay on the
existing `init` / `fromJsonString` / direct-callback path.

**Builder client bridge.** In `api=2` mode, the registered client UUID is sent
in `x-reclaim-vc-id` to these routes:

| Route | Purpose |
| --- | --- |
| `GET /api/sdk/builder/v2/sessions/{sessionId}/bootstrap` | Load the session and resolved recipe. |
| `PATCH /api/sdk/builder/v2/sessions/{sessionId}/claimant` | Update claimant diagnostics. |
| `POST /api/sdk/builder/v2/sessions/{sessionId}/events` | Report canonical lower-snake-case events. |
| `POST /api/sdk/builder/v2/sessions/{sessionId}/attestor-auth` | Request session-bound attestor authentication. Do not send a legacy app secret or provider signature. |
| `POST /api/sdk/builder/v2/sessions/{sessionId}/results` | Submit exact legacy proof objects; the bridge records the matching terminal event strongly before signing and storing result deliveries. |

The bridge preserves each legacy proof and signs only the outer Verification
Client JWS. `verifyResultFull` validates the issuer, JWS, session, audience,
and expiry, then passes each exact proof to `verifyProof`. Keep TEE settings in
the legacy verifier configuration; do not add verification hooks. Treat outer
`extracted_parameters` and `content_hash` as diagnostics, not trust inputs.

**Optional TEE binding.** A consumer can pass
`teeAttestation: { appSecret }` to `sessions.create`. The SDK derives the
hash nonce locally with the pinned `@reclaimprotocol/js-sdk` formula, signs
the binding payload with EIP-191, and registers it with
`PUT /verifications/sessions/{sessionId}/tee-nonce` using the organization
token. The private key stays in the consumer process. Bind before proof
generation. The binding is idempotent for the same values and immutable
afterward; this helper registers only `RECLAIM_TEE_NONCE_V1` (`v3`). Legacy
`verifyProof` accepts the older signature nonce, but this helper does not
generate or register it.

<!-- topic: never-do -->
## Things to never do

<!-- common: never-do-common -->
- Never request `hash` for a target whose value also appears in the request URL
  (topic `draft`), and never `hash` a free-form target — `propose_provider`
  throws either way; retrying won't help.

<!-- topic: credentials -->
<!-- common: credentials -->

<!-- topic: troubleshooting -->
## Troubleshooting

**`unknown draftId` / `no capture`.** The draft is bound to its capture, held in
the MCP server process. Anything that restarts the server (Claude Code restart,
`reset_session`, reboot) drops every capture and its drafts. The fix is always
`start_capture` → interact → `propose_provider` for a fresh `draftId`. Don't ask
the developer to "re-enter the value" — walk them back through capture.

**Replaying as an anonymous visitor.** Pass `withoutSecrets: true` to
`replay_request`; the draft still holds the captured secrets, so you never
re-supply them.

**Site won't load, won't let the user sign in, or breaks right after
sign-in.** Check WHICH client the session targets first: on `builder` (the
default) and `portals` interception happens over CDP, so it is never the
cause — look for a captcha instead. On `inapp-sdk`, `verifier-app`, or the
browser extension, suspect interception before the recipe, and bisect: FIRST
turn
document replay off and keep the interceptor (`disableRequestReplay: true`, or
`isDocumentRequestReplayEnabled: false` on builder) — often that alone is what
breaks the page, and it's the smallest change. Only if that doesn't help, turn
interception off entirely (`injectionType: NONE`, or omit
`interceptorOptions`). Full checklist and remedies: topic `interception`.

**Proof failures / attestor errors** → topic `prove`.
**Browser won't launch or attach** → topic `browser`.
**Missing signing key** → topic `credentials`.
