Skip to main content
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

AspectAgent token (agent_*)Ed25519 session (sess_*)
Secret exposureToken sent in every headerPrivate key never leaves the agent
If interceptedPermanent full access until rotation1-hour session, revocable
Replay protectionNoneNonce-based, single-use challenges
Session revocationMust rotate the whole tokenPer-session, instant
Self-rotationRequires an API key holderAgent signs its own rotation proof
Event deliveryPoll for updatesPush via 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:
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:
{
  "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!"
  }
}
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 via the emergency API-key-authenticated path.
Or provision a key later on an existing agent:
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.
curl -X POST https://sandbox.getsly.ai/v1/agents/$AGENT_ID/challenge
{
  "challenge": "challenge_abcdef01_random...",
  "expiresIn": 60,
  "algorithm": "ed25519"
}
Challenges are single-use and expire in 60 seconds.

3. Sign the challenge

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');
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()
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.
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>"
  }'
{
  "sessionToken": "sess_xyz789...",
  "expiresIn": 3600,
  "agentId": "agt_..."
}
Use the session token exactly like any other bearer token:
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:
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)

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 (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:
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 for details.

Complete example (Node.js)

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:
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

EndpointAuth requiredPurpose
POST /v1/agents/:id/challengePublicRequest challenge nonce (60s TTL)
POST /v1/agents/:id/authenticatePublicSubmit signed challenge, get sess_*
POST /v1/agents/:id/auth-keysAPI keyProvision Ed25519 key pair
POST /v1/agents/:id/auth-keys/rotateSigned proofAgent self-rotates
DELETE /v1/agents/:id/auth-keysAPI keyRevoke key + all sessions
GET /v1/agents/:id/connectsess_*Persistent SSE channel
GET /v1/agents/:id/livenessAPI keyCheck connection status