# auth.md - agent identity for ActionLayer

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

- Pay-as-you-go per-call payments: not required. Wallet rails settle the priced 402 as usual (see https://agents.actionlayer.io/zcj/7dnirfw0b4ot/llms.txt), and calls of $0.50 or more can also pay by card with no registration and no bearer - follow the 402's card block.

### Worked example: pay a pay-as-you-go 402 by card

```bash
# 1. Call the API; a priced 402 arrives with a card block when the amount is $0.50+
R="$(curl -sS -X POST https://agents.actionlayer.io/zcj/7dnirfw0b4ot/<path> -H 'Content-Type: application/json' -d '<body>')"
PAY_URL="$(printf '%s' "$R" | jq -r .card.payUrl)"
PAYMENT_ID="$(printf '%s' "$R" | jq -r .payment.id)"

# 2. Pay it - ONE of the accepted lanes:
#    hosted page for your human (needs nothing):
curl -sS -X POST "$PAY_URL" -H 'Content-Type: application/json' -d '{"checkout": true}'
#    ...or skip the payUrl: answer the 402's method="stripe" www-authenticate challenge
#    with a shared-payment-token credential on the original request (pays and delivers in one round trip):
# curl -sS -X POST https://agents.actionlayer.io/zcj/7dnirfw0b4ot/<path> -H 'Content-Type: application/json' -H "Authorization: Payment <credential>" -d '<body>'

# 3. Poll until complete, then retry the ORIGINAL request unchanged, plus one header:
STATUS_URL="$(printf '%s' "$R" | jq -r .card.statusUrl)"
curl -sS "$STATUS_URL"
curl -sS -X POST https://agents.actionlayer.io/zcj/7dnirfw0b4ot/<path> -H 'Content-Type: application/json' -H "zc-payment-id: $PAYMENT_ID" -d '<body>'
```

A failed delivery refunds the card in full automatically (see the `zc-billing` response header). A retry answering `payment_refunded` or `payment_consumed` is terminal: that payment is spent and will never work again - if you still need the call, start over with a fresh request and pay its new 402.

## Identity endpoints

All identity endpoints are served on this host. Standards-shaped discovery: `https://agents.actionlayer.io/zcj/7dnirfw0b4ot/.well-known/oauth-protected-resource` (RFC 9728) and `https://agents.actionlayer.io/zcj/7dnirfw0b4ot/.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.actionlayer.io/zcj/7dnirfw0b4ot"
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.actionlayer.io/zcj/7dnirfw0b4ot/` 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.actionlayer.io/zcj/7dnirfw0b4ot/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 ActionLayer'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 ActionLayer

- What do you already own? `GET https://agents.actionlayer.io/zcj/7dnirfw0b4ot/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`.
- Pay as you go: no bearer needed - settle priced 402s per https://agents.actionlayer.io/zcj/7dnirfw0b4ot/llms.txt.

Services and prices: https://agents.actionlayer.io/zcj/7dnirfw0b4ot/services (plan ids and live JSON: https://agents.actionlayer.io/zcj/7dnirfw0b4ot/manifest.json). Payment mechanics: https://agents.actionlayer.io/zcj/7dnirfw0b4ot/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.
