# 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/f5hnbq96ev0l/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/f5hnbq96ev0l/<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 - hosted page for your human (needs nothing):
curl -sS -X POST "$PAY_URL" -H 'Content-Type: application/json' -d '{"checkout": true}'

# 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/f5hnbq96ev0l/<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/f5hnbq96ev0l/.well-known/oauth-protected-resource` (RFC 9728) and `https://agents.actionlayer.io/zcj/f5hnbq96ev0l/.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).

```bash
AUTH_BASE="https://agents.actionlayer.io/zcj/f5hnbq96ev0l"
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/f5hnbq96ev0l/` 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/f5hnbq96ev0l/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/f5hnbq96ev0l/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/f5hnbq96ev0l/llms.txt.

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