# Authentication model

Ed25519 API-key request signing for ConnectRPC and private realtime channels.

Programmatic access uses **Ed25519 API keys**. The SDK signs each authenticated request.

## Credentials

| Value           | Where to find it                              |
| --------------- | --------------------------------------------- |
| API key id      | App → **API** → create or view key            |
| API private key | Shown **once** at creation (64-char hex seed) |
| Account ID      | App → **Profile** → **Account ID**            |

Use the Account ID string exactly as shown. Do not use internal numeric ids. Account-ID authentication is valid even when the account has no username; username is optional profile metadata, not an authentication requirement or a value that must be set to `"-"`.

> **Subaccount keys need their own policy**
>
> A key scoped to a subaccount must have an attached **API-key policy**. That policy must permit ledger reads for balance snapshots/private balance streams and the relevant trading actions for order mutations. This is separate from the subaccount policy; authorization evaluates both.

## Signing (what the SDK does)

For each authenticated unary call the SDK builds a signature over:

```text
timestamp_ms
METHOD
pathname
canonical_query
hex(sha256(body))
```

and sends headers `X-API-KEY-ID`, `X-API-TIMESTAMP`, `X-API-SIGNATURE`.

The client allocates a distinct timestamp for every call, including concurrent identical calls, so replay protection does not confuse them. You normally never assemble this yourself; pass credentials via `Config`. Cloned and independently constructed credentials with the same key id share a process-wide allocator. The protocol has no cross-process nonce, so use one API key per process; sharing a key between processes can still produce identical tuples. Automatic timestamps may lead the local wall clock by at most five seconds, leaving half of the API's 10-second freshness window for clock and network skew. A burst that exhausts that capacity is backpressured; prolonged exhaustion returns a retryable rate-limit error instead of reusing an authentication tuple.

> **Use the service wrappers**
>
> Methods on `polyester::Client` services apply the appropriate public or signed call path. Types under `polyester::connect` are low-level generated protocol clients; they do not automatically add Polyester API-key signatures. Do not use a generated client as an authentication shortcut.

## Public vs private

- **Public** market-data calls work with no key.
- **Private** RPCs need a key. Private WebSocket channels also need the root Account ID, supplied through `Config.default_account_id` or the subscribe method's `account_id` argument.
- Realtime is always included (the Cargo `realtime` feature flag is a no-op stub). Consume streams with `recv_result().await` so terminal transport and decode failures are observable.
- Hyphenated channel segments (for example `api-keys`) must be signed with RFC 3986-preserving query encoding (built into the SDK, unreserved `-` `_` `.` `~` stay unescaped).

## Key lifecycle on this SDK

| Action                       | Supported                          |
| ---------------------------- | ---------------------------------- |
| `generate_keypair`           | Yes (local only)                   |
| `list` / `get` / `subscribe` | Yes (API-key auth)                 |
| create / update / revoke     | No, JWT/session (TypeScript / app) |

> **Not JWT**
>
> This SDK does not implement wallet login or session MFA. Those are TypeScript / browser flows.

## Related

- [Authentication guide](https://testnet.polyester.com/docs/sdk/rust/guides/authentication)
- [Ed25519 API keys](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys)
- [API key replay policy](https://testnet.polyester.com/docs/developer-docs/authentication-security/api-key-replay-policy)
