Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ Python client for the AgentScore APIs.

## Identity Model

Two identity paths: `X-Wallet-Address` (wallet-based) and `X-Operator-Token` (credential-based). Wallet addresses accept both EVM (`0x...` 40-hex) and Solana (base58, 32–44 chars) formats — network is auto-detected from the address shape. `assess` responses include `resolved_operator` and `linked_wallets[]` (same-operator sibling wallets, normalized per network — EVM lowercased, Solana base58 verbatim; may mix chains for cross-chain operators). `create_session` and `create_credential` responses include an `agent_memory` cross-merchant pattern hint. `create_session` also returns `next_steps.action="deliver_verify_url_and_poll"` + polling instructions. `poll_session` returns `next_steps.action` values: `continue_polling`, `retry_merchant_request_with_operator_token`, `use_stored_operator_token`, `create_new_session`, `verification_failed`, `contact_support`.
Two identity paths: `X-Wallet-Address` (wallet-based) and `X-Operator-Token` (credential-based). Wallet addresses accept both EVM (`0x...` 40-hex) and Solana (base58, 32–44 chars) formats; network is auto-detected from the address shape. `assess` responses include `resolved_operator` and `linked_wallets[]` (same-operator sibling wallets, normalized per network, EVM lowercased, Solana base58 verbatim; may mix chains for cross-chain operators). `create_session` and `create_credential` responses include an `agent_memory` cross-merchant pattern hint. `create_session` also returns `next_steps.action="deliver_verify_url_and_poll"` + polling instructions. `poll_session` returns `next_steps.action` values: `continue_polling`, `retry_merchant_request_with_operator_token`, `use_stored_operator_token`, `create_new_session`, `verification_failed`, `contact_support`.

## Methods (sync + async)

- `assess` / `aassess` — identity gate with policy (paid). Accepts `operator_token` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`. Optional `signer: { address, network }` opts into server-side wallet-signer-match AND OFAC SDN wallet-address screening — the response then carries both a `signer_match` block (wallet-binding verdict: `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing`) and a `signer_sanctions` block (discriminated union: `{status: "clear"}` | `{sanctioned: True, ofac_label, sdn_uid, listed_at}` | `{status: "unavailable"}`). Wallet-OFAC SDN enforcement on the `signer` block is unconditional whenever a signer is supplied — no `policy.require_sanctions_clear` opt-in required. A `sanctioned: True` OR `status: "unavailable"` verdict flips the response `decision` to `deny` with `decision_reasons` including `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). `policy.require_sanctions_clear` is the separate NAME-based screen on the resolved operator's KYC identity.
- `create_session` / `acreate_session` — create verification session. Returns `agent_memory` + `next_steps`.
- `poll_session` / `apoll_session` — poll session status, returns credential when verified, plus `next_steps.action`.
- `create_credential` / `acreate_credential` — create operator credential (24h TTL default). Response includes `agent_memory`.
- `list_credentials` / `alist_credentials` — list active credentials
- `revoke_credential` / `arevoke_credential` — revoke a credential
- `associate_wallet` / `aassociate_wallet` — report a signer wallet seen paying under a credential. Accepts optional `idempotency_key` (payment intent id / tx hash) so retries don't inflate transaction_count.
- `telemetry_signer_match` / `atelemetry_signer_match` — fire-and-forget POST to `/v1/telemetry/signer-match`; commerce gate uses this to report `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing` verdicts.
- `assess` / `aassess`: identity gate with policy (paid). Accepts `operator_token` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`. Optional `signer: { address, network }` opts into server-side wallet-signer-match AND OFAC SDN wallet-address screening; the response then carries both a `signer_match` block (wallet-binding verdict: `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing`) and a `signer_sanctions` block (discriminated union: `{status: "clear"}` | `{sanctioned: True, ofac_label, sdn_uid, listed_at}` | `{status: "unavailable"}`). Wallet-OFAC SDN enforcement on the `signer` block is unconditional whenever a signer is supplied, no `policy.require_sanctions_clear` opt-in required. A `sanctioned: True` OR `status: "unavailable"` verdict flips the response `decision` to `deny` with `decision_reasons` including `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). `policy.require_sanctions_clear` is the separate NAME-based screen on the resolved operator's KYC identity.
- `create_session` / `acreate_session`: create verification session. Returns `agent_memory` + `next_steps`.
- `poll_session` / `apoll_session`: poll session status, returns credential when verified, plus `next_steps.action`.
- `create_credential` / `acreate_credential`: create operator credential (24h TTL default). Response includes `agent_memory`.
- `list_credentials` / `alist_credentials`: list active credentials
- `revoke_credential` / `arevoke_credential`: revoke a credential
- `associate_wallet` / `aassociate_wallet`: report a signer wallet seen paying under a credential. Accepts optional `idempotency_key` (payment intent id / tx hash) so retries don't inflate transaction_count.
- `telemetry_signer_match` / `atelemetry_signer_match`: fire-and-forget POST to `/v1/telemetry/signer-match`; commerce gate uses this to report `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing` verdicts.

## Errors + observability

Typed error subclasses of `AgentScoreError` so callers can `except` on the specific class without parsing `err.code`: `PaymentRequiredError` (402), `TokenExpiredError` (401 token_expired — exposes parsed `verify_url` / `session_id` / `poll_secret` / `poll_url` / `next_steps` / `agent_memory` instance attributes), `InvalidCredentialError` (401 invalid_credential), `QuotaExceededError` (429 quota_exceeded — don't retry), `RateLimitedError` (429 rate_limited — retry after Retry-After), `TimeoutError` (httpx.TimeoutException — note: subclasses `AgentScoreError`, not the builtin; import explicitly from `agentscore.errors` to disambiguate). All non-timeout `httpx.HTTPError` (ConnectError, ProtocolError, NetworkError, etc.) wrap to `AgentScoreError(code="network_error", status_code=0)` for parity with node-sdk.
Typed error subclasses of `AgentScoreError` so callers can `except` on the specific class without parsing `err.code`: `PaymentRequiredError` (402), `TokenExpiredError` (401 token_expired, exposes parsed `verify_url` / `session_id` / `poll_secret` / `poll_url` / `next_steps` / `agent_memory` instance attributes), `InvalidCredentialError` (401 invalid_credential), `QuotaExceededError` (429 quota_exceeded, don't retry), `RateLimitedError` (429 rate_limited, retry after Retry-After), `TimeoutError` (httpx.TimeoutException, note: subclasses `AgentScoreError`, not the builtin; import explicitly from `agentscore.errors` to disambiguate). All non-timeout `httpx.HTTPError` (ConnectError, ProtocolError, NetworkError, etc.) wrap to `AgentScoreError(code="network_error", status_code=0)` for parity with node-sdk.

`assess()` / `aassess()` responses include an optional `quota` field captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` response headers, so callers can monitor approach-to-cap proactively before hitting 429.

Expand All @@ -34,18 +34,18 @@ Single-package Python library published to PyPI.

## Tooling

- **uv** — package manager. Use `uv sync`, `uv run`.
- **ruff** — linting + formatting. `uv run ruff check .` and `uv run ruff format --check .`.
- **ty** — type checker (Astral). `uv run ty check agentscore/`.
- **vulture** — dead code detection.
- **pytest** — tests. `uv run pytest tests/`.
- **Lefthook** — git hooks. Pre-commit: ruff. Pre-push: ty + vulture (parallel).
- **uv**: package manager. Use `uv sync`, `uv run`.
- **ruff**: linting + formatting. `uv run ruff check .` and `uv run ruff format --check .`.
- **ty**: type checker (Astral). `uv run ty check agentscore/`.
- **vulture**: dead code detection.
- **pytest**: tests. `uv run pytest tests/`.
- **Lefthook**: git hooks. Pre-commit: ruff. Pre-push: ty + vulture (parallel).

## Key Commands

```bash
uv sync --all-extras
uv run lefthook install # one-time per clone — wires pre-commit + pre-push
uv run lefthook install # one-time per clone, wires pre-commit + pre-push
uv run ruff check .
uv run ruff format .
uv run ty check agentscore/
Expand All @@ -57,14 +57,14 @@ uv run pytest tests/
1. Create a branch
2. Make changes
3. Lefthook runs ruff on commit, ty + vulture on push
4. Open a PR — CI runs automatically
4. Open a PR, CI runs automatically
5. Merge (squash)

## Rules

- **No silent refactors**
- **Never commit .env files or secrets**
- **Use PRs** — never push directly to main
- **Use PRs**: never push directly to main

## Releasing

Expand Down
18 changes: 9 additions & 9 deletions agentscore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def _build_error_from_response(response: httpx.Response) -> AgentScoreError:
# verify_url, linked_wallets, reasons, etc. for granular denial recovery.
details = {k: v for k, v in body.items() if k != "error"}
except ValueError:
# Body wasn't JSON or didn't have the expected shape — keep defaults.
# Body wasn't JSON or didn't have the expected shape: keep defaults.
pass

if response.status_code == 402:
Expand Down Expand Up @@ -304,7 +304,7 @@ def create_session(
"""Create a verification or sign-in session.

``address`` pre-associates the session with a known wallet (EVM ``0x...`` or
Solana base58). ``operator_token`` pre-associates with an existing ``opc_...`` —
Solana base58). ``operator_token`` pre-associates with an existing ``opc_...``:
e.g. refresh KYC for a credential. ``kind`` selects the session kind: ``"kyc"``
(the API default) runs identity verification; ``"sign_in"`` is registration-only
(the buyer signs in with an AgentScore account, no identity documents) and mints a
Expand Down Expand Up @@ -368,20 +368,20 @@ def associate_wallet(
) -> AssociateWalletResponse:
"""Report that a wallet paid under an operator credential.

``network`` is the key-derivation family (``"evm"`` or ``"solana"``) — EVM EOAs share
``network`` is the key-derivation family (``"evm"`` or ``"solana"``): EVM EOAs share
identity across every EVM chain (Base, Tempo, Ethereum, …) so one value covers them all.

``idempotency_key`` is optional — pass a stable per-payment key (e.g., payment intent id,
``idempotency_key`` is optional: pass a stable per-payment key (e.g., payment intent id,
x402 tx hash) so agent retries of the same logical payment don't inflate transaction_count.

Fire-and-forget friendly — the returned ``first_seen`` boolean is informational only.
Fire-and-forget friendly: the returned ``first_seen`` boolean is informational only.
"""
body: dict[str, Any] = {
"operator_token": operator_token,
"wallet_address": wallet_address,
"network": network,
}
# Truthy check (not `is not None`) so empty strings don't ship a useless key —
# Truthy check (not `is not None`) so empty strings don't ship a useless key:
# only forward when the key actually has content.
if idempotency_key:
if len(idempotency_key) > _IDEMPOTENCY_KEY_MAX:
Expand Down Expand Up @@ -446,7 +446,7 @@ async def acreate_session(
"""Create a verification or sign-in session.

``address`` pre-associates the session with a known wallet (EVM ``0x...`` or
Solana base58). ``operator_token`` pre-associates with an existing ``opc_...`` —
Solana base58). ``operator_token`` pre-associates with an existing ``opc_...``:
e.g. refresh KYC for a credential. ``kind`` selects the session kind: ``"kyc"``
(the API default) runs identity verification; ``"sign_in"`` is registration-only
(the buyer signs in with an AgentScore account, no identity documents) and mints a
Expand Down Expand Up @@ -514,7 +514,7 @@ async def aassociate_wallet(
"wallet_address": wallet_address,
"network": network,
}
# Truthy check (not `is not None`) so empty strings don't ship a useless key —
# Truthy check (not `is not None`) so empty strings don't ship a useless key:
# only forward when the key actually has content.
if idempotency_key:
if len(idempotency_key) > _IDEMPOTENCY_KEY_MAX:
Expand All @@ -527,7 +527,7 @@ async def aassociate_wallet(
return await self._send_async(lambda: client.post("/v1/credentials/wallets", json=body))

def telemetry_signer_match(self, payload: dict[str, Any]) -> None:
"""Fire-and-forget telemetry — report a wallet-signer-match verdict.
"""Fire-and-forget telemetry: report a wallet-signer-match verdict.

Tracks aggregate signer-binding behavior across merchants. Does not raise;
failures are logged at warning level so persistent telemetry outages are visible
Expand Down
14 changes: 7 additions & 7 deletions agentscore/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def __init__(
super().__init__(message)
self.code = code
self.status_code = status_code
# Response-body fields beyond `error.{code,message}` — e.g. verify_url,
# Response-body fields beyond `error.{code,message}`: e.g. verify_url,
# linked_wallets, claimed_operator, actual_signer, reasons. Consumers
# branch on these for granular recovery. Defaults to {} so callers
# constructing this error by hand without a body can omit it.
Expand All @@ -25,17 +25,17 @@ def status(self) -> int:


class PaymentRequiredError(AgentScoreError):
"""HTTP 402 — the endpoint is not enabled for this account."""
"""HTTP 402: the endpoint is not enabled for this account."""

def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
super().__init__("payment_required", message, 402, details)


class TokenExpiredError(AgentScoreError):
"""HTTP 401 with ``error.code = 'token_expired'`` — credential is no longer valid.
"""HTTP 401 with ``error.code = 'token_expired'``: credential is no longer valid.

Covers both revoked and TTL-expired credentials; the API does not distinguish
which. Body carries an auto-minted verification session — exposed here so callers recover
which. Body carries an auto-minted verification session: exposed here so callers recover
without re-parsing ``details``.
"""

Expand All @@ -51,7 +51,7 @@ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:


class InvalidCredentialError(AgentScoreError):
"""HTTP 401 with ``error.code = 'invalid_credential'`` — operator_token doesn't exist.
"""HTTP 401 with ``error.code = 'invalid_credential'``: operator_token doesn't exist.

Permanent: no auto-session is issued. Caller should switch tokens or restart.
"""
Expand All @@ -61,7 +61,7 @@ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:


class QuotaExceededError(AgentScoreError):
"""HTTP 429 with ``error.code = 'quota_exceeded'`` — account-level cap reached.
"""HTTP 429 with ``error.code = 'quota_exceeded'``: account-level cap reached.

Don't retry; the cap won't lift through retry alone. Distinct from per-second
:class:`RateLimitedError`.
Expand All @@ -72,7 +72,7 @@ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:


class RateLimitedError(AgentScoreError):
"""HTTP 429 with ``error.code = 'rate_limited'`` — per-second sliding-window cap hit.
"""HTTP 429 with ``error.code = 'rate_limited'``: per-second sliding-window cap hit.

Retry after the interval indicated by the ``Retry-After`` header (typically <= 1s).
"""
Expand Down
2 changes: 1 addition & 1 deletion agentscore/test_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

AgentScore's ``/v1/assess`` endpoint recognizes seven EVM addresses
(``0x0000…0001`` through ``0x0000…0007``) as test fixtures with deterministic
policy outcomes — KYC verified, sanctions clear, age gates passing — so dev/test
policy outcomes (KYC verified, sanctions clear, age gates passing) so dev/test
interactions don't burn real KYC credits and produce predictable results.

Use this in test suites and dev/staging tooling to label test-mode interactions
Expand Down
Loading
Loading