# auth.md - agent identity for MyRadar

> You are an agent. MyRadar's storefront supports agent registration: register once, exchange for an access token, and present it as `Authorization: Bearer`. Agent identity here is operated by ZeroClick, MyRadar's trusted agent identity platform - if you already hold an agent credential from any ZeroClick-powered storefront, reuse it (step 1): one registration works at every ZeroClick storefront.

When you need a credential:
Link identity preview is available here. Reuse an existing ZeroClick credential first. Otherwise, when acting for a human signed in to Link, follow [Link identity](#link-identity-preview) before anonymous registration. An empty local Link credential list means request a credential. Link-created credentials work at every ZeroClick storefront but not at the platform-wide entitlements endpoint.


- Buying a plan (`POST https://agents.myradar.dev/zcj/hnp9jvh1efbw/plans/{planId}/purchase`) or topping one up (`POST https://agents.myradar.dev/zcj/hnp9jvh1efbw/extend`): required, on every payment rail - wallet and card alike. Send `Authorization: Bearer <access token>` together with your payment. x402 and card payments carry no `Authorization` of their own, so nothing collides. An MPP credential does - it occupies `Authorization` as `Payment <credential>` - so on MPP send the token in the dedicated header instead: `ZC-Agent-Authorization: Bearer <access token>` alongside `Authorization: Payment <credential>`. With an automatic MPP client, pass `ZC-Agent-Authorization` on the initial request: the client keeps it while it installs the `Payment` credential. `Authorization` carries exactly one credential - never comma-combine the two. Card lanes: `checkout: true` (needs no funds and no card - returns an unpaid hosted checkout for a browser agent where supported, or your human; see /payment/card.md for LPT and WebMCP discovery), a shared payment token, sent as the answer to the 402's `method="stripe"` `www-authenticate` challenge (`Authorization: Payment <credential>` with your token in `ZC-Agent-Authorization: Bearer <token>` - never a body field) - the purchase 402's `card` block has the details.

## Identity endpoints

All identity endpoints are served on this host. Standards-shaped discovery: `https://agents.myradar.dev/zcj/hnp9jvh1efbw/.well-known/oauth-protected-resource` (RFC 9728) and `https://agents.myradar.dev/zcj/hnp9jvh1efbw/.well-known/oauth-authorization-server` (RFC 8414, with an `agent_auth` block listing the registration, claim, and entitlements endpoints). Registration types: `anonymous` (start instantly, no email, no human step), `service_auth` (anchored to your user's email via a claim ceremony), and `identity_assertion` (an ID-JAG from a trusted agent identity provider - no ceremony, born claimed).

## 1. Check for an existing credential - register once, reuse everywhere

Credentials use the shared `zeroclick-agent-auth` store. Reuse an existing credential wherever its identity method is supported. A duplicate registration fragments your purchase history (duplicates converge later when both are claimed with the same email, but reuse is always better).

In sandboxed environments, run keychain commands with host access before concluding that no credential exists.

```bash
AUTH_BASE="https://agents.myradar.dev/zcj/hnp9jvh1efbw"
unset ACCT
SVC="zeroclick-agent-auth"    # keychain service label - shared across all ZeroClick-powered storefronts
EMAIL="<user-email>"           # keychain account for email-anchored credentials

if   security find-generic-password -s "$SVC" -a "$EMAIL"  -w >/dev/null 2>&1; then ACCT="$EMAIL"
elif security find-generic-password -s "$SVC" -a anonymous -w >/dev/null 2>&1; then ACCT="anonymous"
fi
# ACCT set -> skip to step 4 (Exchange). ACCT unset -> continue to step 2.
```

Commands are macOS (`curl`, `security`, `jq`); adapt for Linux (`secret-tool`, reads stdin) or Windows (PowerShell `Set-Secret`/`Get-Secret`). Don't print secret values and don't write your own scripts - these commands persist secrets and surface only non-secret fields. Only `security` takes the secret on argv; `secret-tool`/`Set-Secret` don't.

Link registrations use `zeroclick-agent-auth` with the verified email as the account. For a Link-backed credential, set `AUTH_BASE="https://agents.myradar.dev/zcj/hnp9jvh1efbw"` for exchange and refresh. Its saved `type:link_identity` records its origin; the keychain label does not determine where it works.

## 2. Register

When acting for a human and no reusable ZeroClick credential exists, use Link identity if that user's Link account is signed in and email disclosure is authorized. An empty or expired local credential list means request a credential, not that Link identity is unavailable. Complete the Link flow before considering anonymous registration. Do not infer autonomous operation from omitted human availability or a request to proceed without interruptions. If the user requested Link identity and issuance fails, report the error instead of silently switching identities. Preserve existing credentials and purchases.

For an autonomous agent acting on its own behalf with no human in the loop, prefer anonymous ZeroClick agent auth when a new identity is needed. Executing a human's request without further interruptions still means acting for that human. Reuse existing credentials and access first. Anonymous auth cannot satisfy a verified-email requirement; choose a compatible plan or stop if no authorized verified identity is available.

Use existing authorization to share the user's email with this seller when it covers the current task. Ask the user only if that authorization is missing or unclear. Permission to pay alone does not authorize email disclosure. Checking local credential availability does not disclose email to the seller.


### Link identity preview

An agent with Link Agent Wallet can use a signed email presentation to register for credit plans, purchases, and entitlements. Use registration type `link_identity`, not the separate ID-JAG `identity_assertion` route. Anonymous Agent Attestation Tokens alone cannot identify a buyer. Use existing email-disclosure authorization for this seller and task; ask only when it is missing or unclear. Checking local credential availability requires no disclosure to the seller.

Run LINK_IDENTITY_COMMANDS=1 link-cli identity credentials list --format json locally. If it contains a usable credential for the intended user, reuse it. If the list is empty or the credential has expired, run LINK_IDENTITY_COMMANDS=1 link-cli identity credentials request --format json using that user's authenticated Link session, then continue to the seller challenge. Listing reads local files; it does not check whether Link can issue a credential. A signed-in wallet does not itself supply a credential, but its session can request one. Inspect list errors instead of treating them as empty; preserve credentials belonging to another user. On issuance failure, report the error and resolve it before choosing another identity. Use CLI 0.23.1 or newer. Keep claims and keys local.

Set `LINK_IDENTITY_COMMANDS=1` on every identity command; these commands are unavailable through Link's MCP tools. Credentials are local to the home directory; logging into a different Link account does not switch the saved identity.

Once a usable Link credential is available, POST this body to the seller's registration endpoint. The 401 response is the expected next step, not an authentication failure. It carries `WWW-Authenticate: Identity-Presentation`, `aud`, `nonce`, `claims`, `expires_at`, and `interaction_token`:

```http
POST https://agents.myradar.dev/zcj/hnp9jvh1efbw/agent/identity
Content-Type: application/json

{"type":"link_identity"}
```

Create a presentation with the exact audience and nonce from the challenge, after checking the audience matches this seller:

```sh
LINK_IDENTITY_COMMANDS=1 link-cli identity credentials present \
  --aud "<aud from challenge>" --nonce "<nonce from challenge>" \
  --claim email --format json
```

Capture and parse stdout programmatically. Retry the same registration endpoint with `Identity-Presentation: <presentation>` and JSON `{"type":"link_identity","interaction_token":"<interaction_token from challenge>"}`. Keep proofs, claim values and credentials out of logs and model transcripts. Never manually transcribe a proof or forward it across redirects.

Success returns the normal `identity.assertion` and refresh token. Exchange the assertion at this seller's `/oauth2/token`, then use `Authorization: Bearer` (or `ZC-Agent-Authorization` when Authorization is reserved for payment) for plan purchases, `/agent/entitlements`, and API calls. Refresh at this seller's `/agent/identity` using the existing refresh flow.

Save new registrations under zeroclick-agent-auth with the verified email as the account, retaining the complete response including type:link_identity, identity.assertion and identity.refresh_token.value. Reuse an existing credential instead of overwriting it with a different registration. Preserve the registration type when rotating tokens. The server enforces the enabled-seller restriction regardless of the storage label.
For a new Link registration, use `SVC=zeroclick-agent-auth` and the verified email as `ACCT` for secure storage, exchange and refresh. Keep the registration type alongside rotated tokens; no separate Link-specific keychain entry is needed.

Registration and token exchange complete this flow; continue to entitlements or the authorized purchase without running another registration method below. If the email already belongs to an account, `interaction_required` returns the existing mailbox confirmation ceremony; complete it using step 3 below. A changed holder key may require confirmation again. Re-registering does not reset free-trial eligibility.

Challenges expire after five minutes and are single-use. `invalid_interaction` or `replay_detected`: obtain a new challenge and presentation. `invalid_claims_presentation`: check the credential, audience, nonce and disclosed email. `issuer_unavailable` or `temporarily_unavailable`: retry with backoff and obtain a new challenge if necessary. `link_identity_not_enabled`: this seller does not accept Link identities; use another registration type. Link-created credentials cannot use the platform-wide entitlements endpoint during the preview.

### Other registration methods

Checking the keychain (step 1) always comes first - an existing credential beats registering any type. For the remaining registration choices, pick the type: if your user is signed in at one of the trusted agent identity providers below and you can mint an audience-bound ID-JAG there, use `identity_assertion` - no ceremony, no human step, the credential is born claimed. Otherwise, if they indicate an existing profile or past purchases on this network, use `service_auth` with their email as `login_hint` - the credential is born claimed in one ceremony and immediately draws their existing plans. Use `anonymous` only for a brand-new identity with no history to attach: one API call, no human step, claimable later. For `service_auth` and `identity_assertion`, use existing authorization to assert the user’s identity to this platform; ask only if that authorization is missing or unclear.

Trusted agent identity providers for `identity_assertion`:

- `https://auth.zero.xyz`
- `https://real-time-76-staging.authkit.app`

Mint the ID-JAG at your provider with: `aud` = `https://agentauth.zeroclick.io` (every storefront on this network shares this authorization server, so a credential here is network-wide and consent granted for this audience covers the whole network; this storefront's own resource `https://agents.myradar.dev/zcj/hnp9jvh1efbw/` from the PRM is also accepted), your user's `email` with `email_verified: true`, a fresh `jti` (single-use), a short `exp` (10 minutes or less), and `auth_time` no older than 30 days. Then register with it - on this host, in both modes:

```bash
# identity_assertion - your user has a session at a trusted provider (no ceremony):
ID_JAG="<assertion minted at your provider>"
REG="$(curl -sS "https://agents.myradar.dev/zcj/hnp9jvh1efbw/agent/identity" -H 'Content-Type: application/json' \
  -d "{\"type\":\"identity_assertion\",\"assertion_type\":\"urn:ietf:params:oauth:token-type:id-jag\",\"assertion\":\"$ID_JAG\"}")"
ACCT="$EMAIL"
printf '%s' "$REG" | jq -e .identity.assertion >/dev/null && security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$REG"
```

Success returns the verified identity immediately - skip step 3 and exchange it (step 4). Errors: `issuer_not_enabled` -> your provider is not on the trust list, fall back to `service_auth` or `anonymous`; `replay_detected` -> the assertion was already used, mint a fresh one; `login_required` (401) -> `auth_time` is missing or stale - re-authenticate your user at your provider and mint a fresh ID-JAG (nothing at this service helps); `interaction_required` (401) -> the asserted email already has an account here - the response carries a `claim` block, surface `claim.attempt.verification_uri` to your user and complete exactly as in step 3, using `claim.token` as `CLAIM_TOKEN`; `missing_verified_email` / `invalid_audience` / `expired` / `invalid_signature` -> fix the mint and retry.

```bash
# anonymous - no email, no human step (claim later, optional):
REG="$(curl -sS "$AUTH_BASE/agent/identity" -H 'Content-Type: application/json' \
  -d '{"type":"anonymous"}')"
ACCT="anonymous"
security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$REG"

# service_auth - you have the user's email (claim required before any credential exists):
REG="$(curl -sS "$AUTH_BASE/agent/identity" -H 'Content-Type: application/json' \
  -d "{\"type\":\"service_auth\",\"login_hint\":\"$EMAIL\"}")"
ACCT="$EMAIL"   # persist after the claim verifies (step 3), not now
```

Errors: `invalid_request` -> fix the body; `invalid_login_hint` -> fix `EMAIL`; `anonymous_registration_disabled` / `service_auth_registration_disabled` -> that method is off, use the other.

## 3. Claim - hand the credential to your human (service_auth: required; anonymous: optional)

Claiming anchors the credential to your user's verified email: it gains a refresh token (so it outlives the initial assertion), purchases become recoverable, and free included units unlock - the free allowance is reserved for claimed credentials with a verified email and is shared across every credential the same person claims. Use your user's real mailbox: plus-aliased addresses (name+tag@domain) are refused at claim time. An unclaimed anonymous credential expires after a few days and any plans bought with it die with it - claim before or soon after buying anything.

```bash
CLAIM_TOKEN="$(printf '%s' "$REG" | jq -r .claim.token)"
ATT="$(curl -sS "$AUTH_BASE/agent/identity/claim" -H 'Content-Type: application/json' \
  -d "{\"type\":\"service_auth\",\"claim_token\":\"$CLAIM_TOKEN\",\"login_hint\":\"$EMAIL\"}")"

# give the user this link (non-secret); it opens MyRadar's claim page on this host, signs them in, and shows them a code:
printf '%s' "$ATT" | jq '{verification_uri:.attempt.verification_uri}'

# the user reads the code off that page back to you; complete the claim:
USER_CODE="<code the user read off the claim page>"
VER="$(curl -sS "$AUTH_BASE/agent/identity/claim/complete" -H 'Content-Type: application/json' \
  -d "{\"claim_token\":\"$CLAIM_TOKEN\",\"user_code\":\"$USER_CODE\"}")"

# success returns the verified identity once - persist it:
printf '%s' "$VER" | jq -e .identity.assertion >/dev/null && security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$VER"
```

After a successful claim, re-run the exchange (step 4) immediately: the post-claim access token carries your user link; a pre-claim token does not upgrade itself.

Claim links expire after a few minutes - re-run this step for a fresh one, attempts are free. Errors minting an attempt: `invalid_claim_token` -> restart at step 2; `too_many_attempts` -> wait for a pending attempt to expire. Errors completing: `claim_not_confirmed` -> the user hasn't finished on the page, wait and retry; `invalid_user_code` -> wrong code, ask the user again; `user_code_expired` -> re-run this step for a fresh link; `claim_denied` -> the user denied the claim; `claim_expired` / `already_claimed` -> restart at step 2.

## 4. Exchange for an access token

```bash
ASSERTION="$(security find-generic-password -s "$SVC" -a "$ACCT" -w | jq -r .identity.assertion)"
CRED="$(curl -sS "$AUTH_BASE/oauth2/token" -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
  --data-urlencode "assertion=$ASSERTION")"
ACCESS_TOKEN="$(printf '%s' "$CRED" | jq -r .access_token)"
```

Access tokens are short-lived (minutes): never persist them, re-run this step from the stored assertion when one expires. Errors: `invalid_request` -> the assertion could not be decoded; `invalid_grant` -> the assertion expired or was revoked - refresh (step 5) if you hold a claimed credential, otherwise restart at step 2; `unsupported_grant_type` -> use the grant above.

## 5. Refresh - when the stored assertion nears expiry

Claimed credentials carry a rotating refresh token; unclaimed anonymous credentials have none (when their assertion dies, register again - or better, claim before that).

```bash
REG_TYPE="$(security find-generic-password -s "$SVC" -a "$ACCT" -w | jq -r '.type // empty')"
RT="$(security find-generic-password -s "$SVC" -a "$ACCT" -w | jq -r .identity.refresh_token.value)"
REG="$(curl -sS "$AUTH_BASE/agent/identity" -H 'Content-Type: application/json' \
  -d "{\"type\":\"refresh\",\"refresh_token\":\"$RT\"}")"
REG="$(printf '%s' "$REG" | jq --arg type "$REG_TYPE" 'if $type == "link_identity" then .type = $type else . end')"
printf '%s' "$REG" | jq -e .identity.assertion >/dev/null && security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$REG"   # replace only after a successful refresh
```

Then re-run step 4 with the fresh assertion. `invalid_refresh_token` -> restart at step 2.

## 6. Use it at MyRadar

- Buy a plan: `POST https://agents.myradar.dev/zcj/hnp9jvh1efbw/plans/{planId}/purchase` with `Authorization: Bearer $ACCESS_TOKEN` and your payment (any rail - the 402 lists them, wallet and card). The plan binds to your registered identity, not to the payment instrument. Paying over MPP? `Authorization` carries the `Payment` credential, so send the token as `ZC-Agent-Authorization: Bearer $ACCESS_TOKEN` instead, per [payment/mpp.md](https://agents.myradar.dev/zcj/hnp9jvh1efbw/payment/mpp.md).
- Top up: `POST https://agents.myradar.dev/zcj/hnp9jvh1efbw/extend`, same header.
- What do you already own? `GET https://agents.myradar.dev/zcj/hnp9jvh1efbw/agent/entitlements`, same header: your active plans here with remaining credit - yours plus, once claimed, every credential your human claimed. Lost local state, or unsure whether a purchase went through? Check this before buying again. The network-wide view (all sellers at once) is `GET https://api.zeroclick.io/agent/entitlements`.

- Network failures and 5xx: retry confirmed reads with bounded backoff. Reconcile the previous result using its documented status/receipt route. If no outcome check exists, stop with unknown outcome. Do not repay or repeat a mutation merely because its response was lost. Rate limits: `rate_limit_exceeded` means wait `retry_after` or Retry-After before a safe retry. Services and prices: [services](https://agents.myradar.dev/zcj/hnp9jvh1efbw/services). Plan identifiers: [manifest.json](https://agents.myradar.dev/zcj/hnp9jvh1efbw/manifest.json). Reference index: [llms.txt](https://agents.myradar.dev/zcj/hnp9jvh1efbw/llms.txt). Payment mechanics: [llms-full.txt](https://agents.myradar.dev/zcj/hnp9jvh1efbw/llms-full.txt).
