# auth.md - agent identity for Dappier

> You are an agent. Dappier'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, Dappier'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:

- Buying a plan (`POST https://agents.dappier.com/zcj/grahqhuwea6i/plans/{planId}/purchase`) or topping one up (`POST https://agents.dappier.com/zcj/grahqhuwea6i/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 a hosted payment URL to deliver to your human; the fallback that always works), 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.dappier.com/zcj/grahqhuwea6i/.well-known/oauth-protected-resource` (RFC 9728) and `https://agents.dappier.com/zcj/grahqhuwea6i/.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

Your credential is network-wide, not per-storefront: if you registered at any ZeroClick-powered storefront, reuse that credential here. 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.dappier.com/zcj/grahqhuwea6i"
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.

## 2. Register

Checking the keychain (step 1) always comes first - an existing credential beats registering any type. Then 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`, confirm with your user before proceeding; registering asserts their identity to the identity platform.

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.dappier.com/zcj/grahqhuwea6i/` 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.dappier.com/zcj/grahqhuwea6i/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 Dappier'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
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\"}")"
security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$REG"   # rotates the refresh token; overwrite
```

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

## 6. Use it at Dappier

- Buy a plan: `POST https://agents.dappier.com/zcj/grahqhuwea6i/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 (see https://agents.dappier.com/zcj/grahqhuwea6i/payment/mpp.md).
- Top up: `POST https://agents.dappier.com/zcj/grahqhuwea6i/extend`, same header.
- What do you already own? `GET https://agents.dappier.com/zcj/grahqhuwea6i/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`.

Services and prices: https://agents.dappier.com/zcj/grahqhuwea6i/services (plan ids and live JSON: https://agents.dappier.com/zcj/grahqhuwea6i/manifest.json). Payment mechanics: https://agents.dappier.com/zcj/grahqhuwea6i/llms-full.txt. On any request: `5xx` -> back off and retry the same request; `rate_limit_exceeded` -> wait `retry_after` seconds; a `4xx` not listed above -> fix per the response body, don't replay. A 401 here on a previously-working access token -> re-run step 4 once; if the exchange fails too, refresh or re-register.
