> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getsly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Ed25519 session tokens

> Short-lived sess_* tokens issued via challenge-response. Production-grade agent auth.

Ed25519 session tokens are the production standard for AI agents. The agent's private key never leaves its process — instead, it signs a server-issued nonce to prove ownership, and receives a short-lived `sess_*` token that works everywhere.

## Why Ed25519 sessions

| Aspect             | Agent token (`agent_*`)              | Ed25519 session (`sess_*`)                        |
| ------------------ | ------------------------------------ | ------------------------------------------------- |
| Secret exposure    | Token sent in every header           | Private key never leaves the agent                |
| If intercepted     | Permanent full access until rotation | 1-hour session, revocable                         |
| Replay protection  | None                                 | Nonce-based, single-use challenges                |
| Session revocation | Must rotate the whole token          | Per-session, instant                              |
| Self-rotation      | Requires an API key holder           | Agent signs its own rotation proof                |
| Event delivery     | Poll for updates                     | Push via [persistent SSE](/agents/persistent-sse) |

## Flow

```
┌───────────┐                          ┌──────────┐
│   Agent   │                          │  Sly API │
└─────┬─────┘                          └────┬─────┘
      │                                     │
      │  POST /v1/agents/:id/challenge      │
      ├────────────────────────────────────▶│
      │                                     │
      │  { challenge, expiresIn: 60 }       │
      │◀────────────────────────────────────┤
      │                                     │
      │  sign(challenge, privateKey)        │
      │                                     │
      │  POST /v1/agents/:id/authenticate   │
      │  { challenge, signature }           │
      ├────────────────────────────────────▶│
      │                                     │
      │  { sessionToken: "sess_...",        │
      │    expiresIn: 3600 }                │
      │◀────────────────────────────────────┤
      │                                     │
      │  GET /v1/anything                   │
      │  Authorization: Bearer sess_...     │
      ├────────────────────────────────────▶│
```

## 1. Provision a key pair

When you create the agent, set `generate_keypair: true` and Sly will return the private key once:

```bash theme={null}
curl -X POST https://sandbox.getsly.ai/v1/agents \
  -H "Authorization: Bearer pk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "parent_account_id": "acc_...",
    "name": "Payables Bot",
    "kya_tier": 2,
    "generate_keypair": true
  }'
```

Response:

```json theme={null}
{
  "data": { "id": "agt_...", ... },
  "credentials": { "token": "agent_test_...", ... },
  "authKey": {
    "keyId": "auth_abcdef01_12345678",
    "publicKey": "<base64 Ed25519 public key>",
    "privateKey": "<base64 Ed25519 private key>",
    "algorithm": "ed25519",
    "warning": "SAVE THIS PRIVATE KEY NOW — it will never be shown again!"
  }
}
```

<Warning>
  **Save the private key immediately.** Sly never stores it — we only keep the public key to verify signatures. If lost, you must [rotate the key](#5-rotate-a-key) via the emergency API-key-authenticated path.
</Warning>

Or provision a key later on an existing agent:

```bash theme={null}
curl -X POST https://api.getsly.ai/v1/agents/$AGENT_ID/auth-keys \
  -H "Authorization: Bearer pk_test_..."
```

## 2. Request a challenge

This endpoint is **public** — no auth required. The agent calls it when it needs to authenticate.

```bash theme={null}
curl -X POST https://sandbox.getsly.ai/v1/agents/$AGENT_ID/challenge
```

```json theme={null}
{
  "challenge": "challenge_abcdef01_random...",
  "expiresIn": 60,
  "algorithm": "ed25519"
}
```

Challenges are single-use and expire in 60 seconds.

## 3. Sign the challenge

```ts theme={null}
import * as ed25519 from '@noble/ed25519';

const message = new TextEncoder().encode(challenge);
const privateKey = Buffer.from(PRIVATE_KEY_BASE64, 'base64');
const signature = Buffer.from(
  await ed25519.signAsync(message, privateKey)
).toString('base64');
```

```python theme={null}
import base64
from nacl.signing import SigningKey

signing_key = SigningKey(base64.b64decode(PRIVATE_KEY_BASE64))
signed = signing_key.sign(challenge.encode('utf-8'))
signature_b64 = base64.b64encode(signed.signature).decode()
```

```go theme={null}
import (
    "crypto/ed25519"
    "encoding/base64"
)

priv, _ := base64.StdEncoding.DecodeString(privateKeyB64)
sig := ed25519.Sign(ed25519.PrivateKey(priv), []byte(challenge))
sigB64 := base64.StdEncoding.EncodeToString(sig)
```

## 4. Authenticate

Also public — the signature proves you hold the private key.

```bash theme={null}
curl -X POST https://sandbox.getsly.ai/v1/agents/$AGENT_ID/authenticate \
  -H "Content-Type: application/json" \
  -d '{
    "challenge": "challenge_abcdef01_random...",
    "signature": "<base64 Ed25519 signature>"
  }'
```

```json theme={null}
{
  "sessionToken": "sess_xyz789...",
  "expiresIn": 3600,
  "agentId": "agt_..."
}
```

Use the session token exactly like any other bearer token:

```bash theme={null}
curl -H "Authorization: Bearer sess_xyz789..." \
  https://sandbox.getsly.ai/v1/wallets
```

The `sess_*` token works on **every authenticated endpoint** — transfers, streams, A2A, x402, AP2, everything.

## 5. Rotate a key

Agents can self-rotate without needing an API key holder. Sign the message `rotate:<agentId>` with the current private key:

```bash theme={null}
curl -X POST https://api.getsly.ai/v1/agents/$AGENT_ID/auth-keys/rotate \
  -H "Content-Type: application/json" \
  -d '{ "proof": "<base64 signature of rotate:$AGENT_ID>" }'
```

Atomic effect:

1. Old key marked `rotated`
2. **All active sessions under the old key are revoked**
3. New Ed25519 key pair generated
4. New private key returned (shown once)

## 6. Revoke a key (kill-switch)

```bash theme={null}
curl -X DELETE https://api.getsly.ai/v1/agents/$AGENT_ID/auth-keys \
  -H "Authorization: Bearer pk_test_..."
```

Instantly revokes the auth key and all active sessions. The agent cannot authenticate until a new key is provisioned. Unlike [freezing a wallet](/agents/wallet-policies#freeze) (which blocks spending but leaves auth working), key revocation blocks all API access.

## Persistent SSE connection

Once authenticated with `sess_*`, the agent can open a push channel for real-time events:

```bash theme={null}
curl -N -H "Authorization: Bearer sess_xyz789..." \
  https://sandbox.getsly.ai/v1/agents/$AGENT_ID/connect
```

Events pushed to the agent:

* `task_assigned` — new A2A task
* `transfer_completed` — transfer finalized
* `approval_requested` — spending requires manager approval
* `stream_alert` — managed stream needs attention
* `key_rotated` — auth key was rotated, session about to revoke
* `heartbeat` — 30-second keepalive

Supports `Last-Event-ID` for reconnect — missed events are replayed from a 100-event / 5-minute buffer. See [persistent SSE](/agents/persistent-sse) for details.

## Complete example (Node.js)

```ts theme={null}
import * as ed25519 from '@noble/ed25519';

const API = 'https://sandbox.getsly.ai';
const AGENT_ID = 'agt_...';
const PRIVATE_KEY = Buffer.from(process.env.AGENT_PRIVATE_KEY!, 'base64');

async function authenticate(): Promise<string> {
  // 1. Challenge
  const chRes = await fetch(`${API}/v1/agents/${AGENT_ID}/challenge`, {
    method: 'POST',
  });
  const { challenge } = await chRes.json();

  // 2. Sign
  const sig = await ed25519.signAsync(
    new TextEncoder().encode(challenge),
    PRIVATE_KEY
  );
  const signature = Buffer.from(sig).toString('base64');

  // 3. Authenticate
  const authRes = await fetch(`${API}/v1/agents/${AGENT_ID}/authenticate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ challenge, signature }),
  });
  const { sessionToken } = await authRes.json();
  return sessionToken;
}

// Use it
const token = await authenticate();
const wallets = await fetch(`${API}/v1/wallets`, {
  headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());
```

The SDK wraps this into one call:

```ts theme={null}
import { Sly } from '@sly_ai/sdk';

const sly = new Sly({
  agentId: 'agt_...',
  privateKey: process.env.AGENT_PRIVATE_KEY!,
  autoReauth: true, // re-runs handshake on 401
});

const wallets = await sly.wallets.list();
```

## Endpoint reference

| Endpoint                               | Auth required | Purpose                               |
| -------------------------------------- | ------------- | ------------------------------------- |
| `POST /v1/agents/:id/challenge`        | Public        | Request challenge nonce (60s TTL)     |
| `POST /v1/agents/:id/authenticate`     | Public        | Submit signed challenge, get `sess_*` |
| `POST /v1/agents/:id/auth-keys`        | API key       | Provision Ed25519 key pair            |
| `POST /v1/agents/:id/auth-keys/rotate` | Signed proof  | Agent self-rotates                    |
| `DELETE /v1/agents/:id/auth-keys`      | API key       | Revoke key + all sessions             |
| `GET /v1/agents/:id/connect`           | `sess_*`      | Persistent SSE channel                |
| `GET /v1/agents/:id/liveness`          | API key       | Check connection status               |
