# @suila/trustrank

**A credit check for AI agents.** Check an agent's behavioral TrustRank score
*before* you engage, delegate to, or pay it — the read/gate side of the SuilaAI
Agentic TrustRank bureau, for JS/TS agent runtimes (Vercel eve + Passport, edge
functions, Node ≥ 18).

```bash
npm install @suila/trustrank
```

```ts
import { TrustRankClient } from "@suila/trustrank";

const tr = new TrustRankClient({
  baseUrl: "https://api.suila.ai",
  apiKey: process.env.SUILA_API_KEY,
});

// Pull the report and decide yourself
const v = await tr.check("agent://acme/checkout", { stakes: 250 });
if (!v.allowed) return refuse();          // v.score, v.tier, v.regime, v.demonstratedRange

// Or enforce a gate (throws TrustGateError if refused)
await tr.require("agent://acme/checkout", { stakes: 250 });
```

The score is the FICO; `demonstratedRange` is the credit limit; `regime` is the
account status; `decision` is `allow` / `verify` / `approval_required` / `block`.

## Vercel eve + Passport

Passport supplies the counterparty's identity; TrustRank says whether to trust
it, and at what stakes. Gate a tool once, eve-style:

```ts
const transferSafely = tr.withTrustRank(doTransfer, {
  agentId: (to: string) => to,                       // e.g. a Passport agent id
  stakes: (_to: string, amountUsd: number) => amountUsd,
});
// transferSafely("agent://acme/checkout", 250)  // throws TrustGateError if refused
```

## API

| Member | Description |
|---|---|
| `new TrustRankClient({ baseUrl, apiKey?, defaultStakes?, blockOnApproval?, allowVerify?, timeoutMs?, fetchImpl? })` | Construct a client. |
| `check(agentId, { stakes?, chain? })` | Pull the verdict. Never throws on a low score. |
| `handshake(initiator, counterparty, { stakes?, chain? })` | Mutual A2A pre-interaction check (both directions, one call); returns `HandshakeVerdict`. |
| `require(agentId, { stakes?, chain? })` | Check + enforce; throws `TrustGateError` if refused. |
| `withTrustRank(handler, { agentId, stakes?, chain? })` | Wrap a handler so the counterparty is gated before it runs. |
| `CheckVerdict` | `.allowed` `.blocked` `.requiresVerification` `.needsHuman`, plus `score`, `effectiveScore`, `tier`, `regime`, `decision`, `demonstratedRange`. |
| `HandshakeVerdict` | `.connected` `.escalate` `.refused`, plus both legs (`aToB`, `bToA`) and the combined `decision`. |

### Errors (no retries by design)

The client makes exactly one attempt per call — a trust gate must answer
promptly, and the *caller* owns the timeout policy (agents should fail closed).

| Error | Meaning |
|---|---|
| `TrustGateError` | `require()`/`withTrustRank()` refused the engagement; carries `.verdict`. |
| `TrustRankApiError` | Bureau answered non-2xx; carries `.status` and `.endpoint` (404 = unknown agent, 401 = missing/invalid write token). |
| `TrustRankTimeoutError` | No answer within `timeoutMs` (default 2000); carries `.endpoint` and `.timeoutMs`. |

## Wire contract

The client talks to any bureau implementing:

```
POST {baseUrl}/v1/check
Authorization: Bearer {apiKey}
{ "agent_id": "...", "stakes": <number>, "chain": ["...", ...] }
-> 200 { "agent_id","score","effective_score","tier","regime","decision",
         "demonstrated_range","reasons" }
```

A reference bureau (FastAPI + a zero-dependency stdlib fallback) ships in the
[`suila_trustrank.server`](../suila_trustrank/server) package:
`python -m suila_trustrank.server`.

## Build

```bash
npm install
npm run build      # tsc -> dist/ (ESM + .d.ts)
npm run typecheck  # tsc --noEmit
```

Requires a runtime with global `fetch` / `AbortController` (Node ≥ 18, Vercel edge,
Deno, modern browsers). Inject `fetchImpl` to use a custom transport.
