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
9 changes: 8 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ Two identity paths: `X-Wallet-Address` (wallet-based) and `X-Operator-Token` (cr
## Methods

- `getReputation(address, options?)` — cached reputation lookup (free)
- `assess(address, options?)` — identity gate with policy (paid). Accepts `operatorToken` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`.
- `assess(address, options?)` — identity gate with policy (paid). Accepts `operatorToken` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`. Optional `resolveSigner: { address, network }` opts into server-side wallet-signer-match — the response then carries a `signer_match` block describing whether the supplied signer wallet resolves to the same operator as the claimed `address`.
- `createSession(options?)` — create verification session for identity bootstrapping. Returns `agent_memory` + `next_steps`.
- `pollSession(sessionId, pollSecret)` — poll session status, returns credential when verified, plus `next_steps.action`.
- `createCredential(options?)` — create operator credential (24h TTL default). Response includes `agent_memory`.
- `listCredentials()` — list active credentials
- `revokeCredential(id)` — revoke a credential
- `associateWallet({ operatorToken, walletAddress, network, idempotencyKey? })` — report a signer wallet seen paying under a credential. Fire-and-forget; use the payment intent id / tx hash as `idempotencyKey` so retries don't inflate transaction_count.
- `telemetrySignerMatch(payload)` — 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 branch on `instanceof` without parsing `err.code`: `PaymentRequiredError` (402), `TokenExpiredError` (401 token_expired — exposes parsed `verifyUrl` / `sessionId` / `pollSecret` / `pollUrl` / `nextSteps` / `agentMemory` instance fields), `InvalidCredentialError` (401 invalid_credential), `QuotaExceededError` (429 quota_exceeded — don't retry), `RateLimitedError` (429 rate_limited — retry after Retry-After), `TimeoutError` (request abort/timeout). All preserve the existing `AgentScoreError` catch behavior.

`assess()` 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.

## Architecture

Expand Down
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,59 @@ try {
}
```

`AgentScoreError.details` carries the rest of the response body — `verify_url`, `linked_wallets`, `claimed_operator`, `actual_signer`, `expected_signer`, `reasons`, `agent_memory` — so callers can branch on granular denial codes without re-parsing:
`AgentScoreError.details` carries the rest of the response body — `verify_url`, `linked_wallets`, `claimed_operator`, `actual_signer`, `expected_signer`, `reasons`, `agent_memory` — so callers can branch on granular denial codes without re-parsing.

### Typed error classes

For status-code-specific recovery, the SDK throws typed subclasses of `AgentScoreError`. All inherit from `AgentScoreError` so existing `catch (err) { if (err instanceof AgentScoreError) ... }` still works.

| Class | Triggered by | What it adds |
|---|---|---|
| `PaymentRequiredError` | HTTP 402 | The endpoint is not enabled for this account |
| `TokenExpiredError` | HTTP 401 with `error.code = "token_expired"` | Parsed body fields exposed on the instance: `verifyUrl`, `sessionId`, `pollSecret`, `pollUrl`, `nextSteps`, `agentMemory` — recover without re-parsing `details` |
| `InvalidCredentialError` | HTTP 401 with `error.code = "invalid_credential"` | Permanent — switch tokens or restart |
| `QuotaExceededError` | HTTP 429 with `error.code = "quota_exceeded"` | Account-level cap reached; don't retry |
| `RateLimitedError` | HTTP 429 with `error.code = "rate_limited"` | Per-second sliding-window cap; retry after `Retry-After` |
| `TimeoutError` | Request aborted before a response arrived | Distinct from generic network errors |

```typescript
import {
AgentScore, AgentScoreError, TokenExpiredError, QuotaExceededError, TimeoutError,
} from "@agent-score/sdk";

try {
await client.assess("0xabc...", { policy: { require_kyc: true } });
} catch (err) {
if (!(err instanceof AgentScoreError)) throw err;
if (err.code === "wallet_signer_mismatch") {
const linked = err.details.linked_wallets as string[] | undefined;
console.log("Re-sign from one of:", linked);
}
if (err.code === "token_expired") {
console.log("Verify at:", err.details.verify_url);
if (err instanceof TokenExpiredError) {
console.log("Verify at:", err.verifyUrl, "poll with:", err.pollSecret);
} else if (err instanceof QuotaExceededError) {
console.log("Account quota reached — surface to user; don't retry.");
} else if (err instanceof TimeoutError) {
console.log("Network timeout — retry with backoff.");
} else if (err instanceof AgentScoreError) {
console.error(err.code, err.message);
}
}
```

## Quota observability

`assess()` responses include an optional `quota` field captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` response headers. Use it to monitor approach-to-cap proactively (warn at 80%, alert at 95%) before a 429:

```typescript
const result = await client.assess("0xabc...", { policy: { require_kyc: true } });
if (result.quota && result.quota.limit && result.quota.used) {
const pct = (result.quota.used / result.quota.limit) * 100;
if (pct > 80) console.warn(`AgentScore quota at ${pct.toFixed(1)}% — resets ${result.quota.reset}`);
}
```

`quota` is `undefined` when the API doesn't emit the headers (Enterprise / unlimited tiers).

## Telemetry

`telemetrySignerMatch(payload)` is a fire-and-forget POST to `/v1/telemetry/signer-match` so AgentScore can track aggregate signer-binding behavior across merchants. Used internally by `@agent-score/commerce`'s gate; available directly for custom integrations that perform their own wallet-signer-match checks.

## Documentation

- [API Reference](https://docs.agentscore.sh)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-score/sdk",
"version": "2.1.1",
"version": "2.2.0",
"description": "TypeScript client for the AgentScore APIs",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
Expand Down
74 changes: 74 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,77 @@ export class AgentScoreError extends Error {
this.details = details;
}
}

/** HTTP 402 — the endpoint is not enabled for this account. */
export class PaymentRequiredError extends AgentScoreError {
constructor(message: string, details: Record<string, unknown> = {}) {
super('payment_required', message, 402, details);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'PaymentRequiredError';
}
}

/** HTTP 401 with `error.code = 'token_expired'` — credential is no longer valid (revoked or
* TTL-expired; the API deliberately doesn't disclose which). The body carries an auto-minted
* verification session — exposed here so callers can recover without re-parsing `details`. */
export class TokenExpiredError extends AgentScoreError {
public readonly verifyUrl?: string;
public readonly sessionId?: string;
public readonly pollSecret?: string;
public readonly pollUrl?: string;
public readonly nextSteps?: unknown;
public readonly agentMemory?: unknown;

constructor(message: string, details: Record<string, unknown> = {}) {
super('token_expired', message, 401, details);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'TokenExpiredError';
this.verifyUrl = typeof details.verify_url === 'string' ? details.verify_url : undefined;
this.sessionId = typeof details.session_id === 'string' ? details.session_id : undefined;
this.pollSecret = typeof details.poll_secret === 'string' ? details.poll_secret : undefined;
this.pollUrl = typeof details.poll_url === 'string' ? details.poll_url : undefined;
this.nextSteps = details.next_steps;
this.agentMemory = details.agent_memory;
}
}

/** HTTP 401 with `error.code = 'invalid_credential'` — the operator_token doesn't match any
* credential. Permanent: no auto-session is issued. Caller should switch tokens or restart. */
export class InvalidCredentialError extends AgentScoreError {
constructor(message: string, details: Record<string, unknown> = {}) {
super('invalid_credential', message, 401, details);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'InvalidCredentialError';
}
}

/** 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 `RateLimitedError`. */
export class QuotaExceededError extends AgentScoreError {
constructor(message: string, details: Record<string, unknown> = {}) {
super('quota_exceeded', message, 429, details);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'QuotaExceededError';
}
}

/** HTTP 429 with `error.code = 'rate_limited'` — per-second sliding-window limit hit. Retry
* after the interval indicated by the `Retry-After` header (typically ≤1s). */
export class RateLimitedError extends AgentScoreError {
constructor(message: string, details: Record<string, unknown> = {}) {
super('rate_limited', message, 429, details);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'RateLimitedError';
}
}

/** Request timed out or was aborted at the network layer (the AbortController fired before a
* response arrived). Distinct from generic network errors so callers can branch on retry vs
* surface-to-user without parsing message strings. */
export class TimeoutError extends AgentScoreError {
constructor(message: string) {
super('timeout', message, 0);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'TimeoutError';
}
}
Loading
Loading