# @aithos/sdk

> High-level developer SDK for building agentic apps on the
> [Aithos](https://aithos.be) protocol.

`@aithos/sdk` is the recommended entry point for app developers. It wraps
[`@aithos/protocol-client`](https://github.com/aithos-protocol/protocol-client)
(low-level cryptography, signed envelopes, DID and mandate primitives) and
adds the Aithos-hosted endpoints — the **compute proxy** for Bedrock /
Claude inference and the **wallet** for Stripe credit-pack top-ups — behind
a single, stable, batteries-included surface.

## Status

**Published — `0.2.0` on npm.** The SDK follows semantic versioning on the
`0.x` line: a breaking change bumps the minor version. As of `0.2.0` the SDK
is **v0.4-only** — brand-new Ethos identities are born in the content-addressed
v0.4 format, and any read or write against a legacy v0.3/v0.2 Ethos throws
`EthosMigrationRequiredError` (migrate first via `ethos.me().migrateToV04()`
or on [app.aithos.be](https://app.aithos.be)). Pin exact versions in production.

## Installation

```bash
npm install @aithos/sdk @aithos/protocol-client
```

`@aithos/protocol-client` is a peer dependency. Apps that already vend their
own copy keep using it; the SDK re-exports its primitives so you do not
need to import both directly in app code.

## Quick start

```ts
import { AithosSDK, createIdentity } from "@aithos/sdk";

// 1. Get or restore the user's identity (a key pair + DID).
const identity = await createIdentity();

// 2. Construct the SDK. Endpoints default to the production Aithos hosts;
//    pass `endpoints` to override (staging, self-host, tests).
const sdk = new AithosSDK({ identity });

// 3. Top up the wallet via Stripe Checkout.
const { checkoutUrl } = await sdk.wallet.createTopupSession({
  packId: "credits-100k",
  successUrl: "https://my-app.example.com/?topup=success",
  cancelUrl: "https://my-app.example.com/?topup=cancel",
});
window.location.href = checkoutUrl;

// 4. Once the user has credits, invoke Bedrock through the compute proxy.
const reply = await sdk.compute.invokeBedrock({
  model: "claude-sonnet-4-6",
  mandateId: "mandate:…",
  messages: [{ role: "user", content: "Hello, Aithos!" }],
});

console.log(reply.content);
```

## Environments — prod & dev endpoints

The SDK targets the **production** Aithos infrastructure by default
(`DEFAULT_SDK_ENDPOINTS`, every service on `*.aithos.be`). An isolated **dev**
account exists on `*.dev.aithos.be`; the `DEV_SDK_ENDPOINTS` preset points
every SDK surface at it (compute, wallet, web, pds, assets, api, cdn — the
api/cdn pair flows down to `@aithos/protocol-client` too):

```ts
import { AithosSDK, DEV_SDK_ENDPOINTS } from "@aithos/sdk";

const sdk = new AithosSDK({ auth, appDid, endpoints: DEV_SDK_ENDPOINTS });
```

Partial overrides compose on top of the prod defaults — pass only the keys you
want to redirect (staging, self-host, tests):

```ts
const sdk = new AithosSDK({ auth, appDid, endpoints: { pds: "https://pds.staging.example" } });
```

The data-client factories accept the same per-service override
(`auth.ownerDataClient({ pdsUrl })`, `auth.delegateDataClient({ pdsUrl })`,
`createAppendDataClient({ pdsUrl })`).

**Custodial auth (`AithosAuth`) is configured separately** — its base URLs are
NOT part of `AithosSdkEndpoints`, so the `DEV_SDK_ENDPOINTS` preset does not
move them. Set them explicitly for dev:

```ts
const auth = import.meta.env.VITE_AITHOS_ENV === "prod"
  ? new AithosAuth({ publicKey })
  : new AithosAuth({
      publicKey,
      authBaseUrl: "https://auth.dev.aithos.be",   // sign-in/up, invite, accept
      apiBaseUrl: DEV_SDK_ENDPOINTS.api,            // identity publish/resolve
    });
```

Both `auth.dev.aithos.be` and `api.dev.aithos.be` are live on the dev account.
Omit both in prod to take the SDK defaults (`auth.aithos.be` / `api.aithos.be`).

## Transcribing audio → text

`sdk.compute.invokeTranscribe` turns an audio `Blob` into text through AWS
Transcribe. It does one thing — audio → text — and **stores nothing**: it
returns the transcript and you decide what to do with it (write it to an
ethos, a PDS, your own database, email it, or throw it away).

```ts
// Browser: a Blob from MediaRecorder; backend: a Blob from a Buffer.
const result = await sdk.compute.invokeTranscribe({
  audio: blob,                          // Blob/File (Node 18+ has global Blob)
  model: "transcribe:aws-fr-standard",  // default; also aws-en-standard
  languageCode: "fr-FR",                // optional
  // durationSecOverride: 127,          // REQUIRED on backends (no DOM probe)
  onProgress: (s) => console.log(s.phase), // uploading → starting → processing → completed
});

console.log(result.text);          // "Bonjour, je voulais te dire que…"
console.log(result.segments);      // [{ start_sec, end_sec, text }]
console.log(result.creditsCharged);

// Then YOU choose where it goes — the compute has no opinion:
await myEthos.addRevision(result.text);   // or PDS, DB, email, nothing…
```

The core is isomorphic (Node + browser) and depends only on `Blob`, `fetch`
and timers. Browser-only resilience is opt-in and framework-agnostic:
`sdk.compute.transcribeDraft` (IndexedDB queue of recordings) and
`sdk.compute.listLocalPendingTranscribes()` /
`subscribeLocalPendingTranscribes()` / `resumeTranscribe(jobId)` recover jobs
across reloads. React users get `useAithosTranscribePendingJobs(sdk.compute)`
from `@aithos/sdk/react`. Advanced callers can drive the flow manually with
`prepareTranscribe` / `startTranscribe` / `getTranscribeStatus`.

## Delegating compute to an agent — opt-in token spending

To let an agent (or another user, or a third-party app) invoke Bedrock
**in your name**, with **your credits**, you mint a mandate. Token
spending is its own opt-in capability — passing it is a separate,
named, validated input that a consent UI can review. It is NEVER an
implicit side-effect of an ethos read/write scope.

```ts
// Mint a mandate that lets agent Bob read your public ethos AND
// spend up to 5 000 microcredits/day on Haiku, capped at 100 000
// microcredits over the whole mandate lifetime.
const mandate = await sdk.mandates.create({
  granteeId: "urn:agent:bob",
  scopes: ["ethos.read.public"],
  ttlSeconds: 86_400,
  compute: {
    dailyCapMicrocredits: 5_000,
    totalCapMicrocredits: 100_000,
    maxCreditsPerCall: 500,
    allowedModels: ["claude-haiku-4-5"],
  },
});

// Hand `mandate.bundle` (a `.aithos-delegate.json` Blob) to Bob.
// He imports it, then signs his own envelopes and calls
// sdk.compute.invokeBedrock({ mandateId: mandate.mandateId, … })
// — every invocation debits *your* wallet, capped per the budget
// you set.
```

Three invariants the SDK enforces synchronously, before reaching the
network — they fail fast with a precise `AithosSDKError`:

- **No smuggling.** Adding `"compute.invoke"` directly to `scopes[]`
  throws `mandates_invalid_scopes`. The `compute` namespace is the
  only path, so a UI reviewing `compute` can never be bypassed.
- **No bearer compute.** A `compute` namespace without at least one
  of `dailyCapMicrocredits` or `totalCapMicrocredits` throws
  `mandates_invalid_compute`. Unbounded compute mandates are forbidden
  by construction.
- **Compute-only is fine.** `scopes: []` is allowed when `compute` is
  set — useful for agents that only consume tokens (e.g. creative
  assistants) without seeing any of your data.

## Custodial auth — onboarding users without a recovery file

Three new methods on `AithosAuth` let an app create and authenticate
its end-users via a server-managed custody flow — the user only needs
an email address and a password sent by mail. No recovery file, no
Google account, no client-side cryptography to handle.

The model is honest custody: Aithos KMS-wraps the user's Ed25519
identity seeds, and unwraps them on every sign-in after password
verification. Equivalent to how Coinbase or any hosted SaaS keeps your
private key. Annunciated to the user in the welcome email.

```ts
import { AithosSDK } from "@aithos/sdk";

// ─── Server-side: sign-up ───────────────────────────────────────────
// MUST run on your backend. The API key is a server secret —
// provisioned by Aithos via the operator runbook.
const sdk = new AithosSDK({ identity });
const result = await sdk.auth.signUpCustodial({
  apiKey: process.env.AITHOS_API_KEY!,
  email: "alice@example.com",
  displayName: "Alice",
});
// → { userId, did, handle, email, mailSent }
// The user receives an email with their password and a sign-in link.

// ─── Browser-side: sign-in ──────────────────────────────────────────
// User pastes the password from their mail into your sign-in form,
// then your frontend calls this. No API key needed — the password
// is the credential.
const { session, passwordMustChange } = await sdk.auth.signInCustodial({
  email: "alice@example.com",
  password: "MyTempPass32chars",
});
// Local KeyStore is now hydrated with the 5 Ed25519 sphere seeds
// (root, public, circle, self, #data) — the user can publish ethos
// editions, mint mandates, invoke compute, and own PDS data/asset
// collections (signed under the dedicated #data sphere), exactly as if
// they had signed in via a recovery file or Google SSO.
if (passwordMustChange) {
  // Optional: nudge the user to set their own password via the
  // standard reset flow.
}

// ─── Browser-side: request password reset ───────────────────────────
// The backend always returns silently (anti-enumeration). If the email
// is registered AND in custodial mode AND not in cooldown AND under the
// daily cap, a magic-link email is sent to the address.
await sdk.auth.requestPasswordReset({ email: "alice@example.com" });
```

The reset finalization (collecting the new password from the user) is
done on a small web page hosted by Aithos at `https://app.aithos.be/reset`
(or your app's own `reset_base_url` if you've registered one — see the
operator runbook). The page POSTs to `/auth/custodial/reset/finalize`
and returns the user to your sign-in page on success.

### Getting an API key

API keys are provisioned out-of-band by Aithos. Contact the maintainer
(or use the self-service console at `aithos.be/console` when it ships
in V2). The pattern is `aithos_<env>_<32 chars b58>`. Keep it in your
backend's secrets manager — never in browser code.

### Trade-offs vs. the zk and Google SSO flows

|                | zk (recovery file)         | Google SSO (KMS)     | **Custodial** |
|----------------|----------------------------|----------------------|---------------|
| User burden    | downloads `recovery.json`  | Google consent       | email only    |
| Password reset | requires recovery file     | re-auth via Google   | magic-link mail |
| Trust model    | zero-knowledge (you only)  | Aithos + Google      | Aithos only   |
| Multi-device   | re-import recovery         | re-Google            | email + password |
| SDK signing capability | full                | full                 | full          |

Custodial is the right default for SDK-integrated apps that want
SaaS-grade UX. zk is the right default for power users who want
sovereign custody. SSO is the right default for users already invested
in the Google ecosystem.

## Extracting webpages without an LLM

`sdk.web` is a token-priced primitive that lets your agent read a
public webpage and get back cleaned HTML, purged CSS and a
deterministic visual signature — all computed server-side without an
LLM in the loop. Pricing is a flat **1 microcredit** per successful
extraction (refunded on failure), versus ~30 mc for a comparable
LLM-based extraction.

```ts
import { AithosSDK } from "@aithos/sdk";

const sdk = new AithosSDK({ auth, appDid });

const { data, creditsCharged } = await sdk.web.extract({
  url: "https://example.com",
});

console.log(data.meta.title);             // "Example Domain"
console.log(data.visual_signature.colors.primary); // "#0078d4"
console.log(data.styles.css.length);      // purged + minified CSS
```

Owners can mint a mandate for delegate-only extraction:

```ts
import { WEB_EXTRACT_SCOPE } from "@aithos/sdk";

await sdk.mandates.create({
  appDid: "did:aithos:app:my-agent",
  scopes: [WEB_EXTRACT_SCOPE],
  // ...
});
```

## Calling a third-party Aithos-aware backend

If your app talks to its own backend (a service you built that verifies
Aithos envelopes per spec §11.2 using
`@aithos/protocol-core/envelope`), use `sdk.auth.signEnvelope` to sign
the request with the same primitive that SDK namespaces use internally
for `api.aithos.be`. No JWT, no shadow session — the user's DID in the
envelope's `iss` field is the identity.

```ts
import { AithosSDK, type SignedEnvelope } from "@aithos/sdk";

// Sign a request to your own backend with the active owner's
// public-sphere key. Default TTL is 60 s.
const envelope: SignedEnvelope = await sdk.auth.signEnvelope({
  aud: "https://api.example.com/v1/widgets",
  method: "myapp.widgets.create",
  params: { name: "Widget #1" },
});

await fetch("https://api.example.com/v1/widgets", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: crypto.randomUUID(),
    method: "myapp.widgets.create",
    params: { name: "Widget #1", _envelope: envelope },
  }),
});
```

The envelope binds the signature to `(iss, aud, method, params_hash,
nonce, iat, exp)`, so a single envelope cannot be replayed against a
different endpoint, method, or payload. Throws
`AithosSDKError("auth_not_signed_in")` if no owner is loaded; throws
`AithosSDKError("auth_invalid_sphere")` if you pass a sphere outside
`"root" | "public" | "circle" | "self"` (default is `"public"`).

Server-side, your backend verifies the envelope with
`@aithos/protocol-core`'s `verifyEnvelope` (the 9-step check from spec
§11.4) — same algorithm that `api.aithos.be` uses, no re-implementation
needed.

## Ethos format policy

The SDK is **v0.4-only**. Authoring is always v0.4: `publish()` and
`ensureInitialized()` create a subject's first edition directly in the
content-addressed v0.4 format (via `createEditionV04Owner`), so brand-new
subjects are born v0.4.

Reading or writing a **legacy v0.3/v0.2** Ethos throws
`EthosMigrationRequiredError` (`code: "ethos_migration_required"`), whose
message points to [app.aithos.be](https://app.aithos.be). The only reader
still allowed to touch a legacy source is the migration engine itself
(`ethos.me().migrateToV04()`, owner-only). The deprecated no-op
`setEthosV04OptIn` export was removed in `0.2.0`.

```ts
import { AithosSDK, EthosMigrationRequiredError } from "@aithos/sdk";

try {
  const zone = await sdk.ethos.me().zone("public");
  const sections = await zone.sections();
  // …read/write as usual…
} catch (err) {
  if (err instanceof EthosMigrationRequiredError) {
    // A legacy v0.3/v0.2 Ethos — the SDK will not read or write it.
    // Owner path: migrate in place, then retry.
    await sdk.ethos.me().migrateToV04();
    // Or send the user to https://app.aithos.be to migrate.
  } else {
    throw err;
  }
}
```

## What lives where

| Namespace                  | Purpose                                                                                    |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| `sdk.auth`                 | Sign-in, sign-up, key custody — and `signEnvelope` for calls to your own Aithos-aware backend. |
| `sdk.compute`              | Bedrock invocation through the Aithos compute proxy (signed envelope, wallet enforcement). |
| `sdk.web`                  | Webpage extraction without an LLM through the web extractor proxy (1 mc / call).           |
| `sdk.wallet`               | Stripe Checkout sessions for credit-pack top-ups, balance helpers.                         |
| `sdk.ethos`                | Ethos-zone composition / parsing — re-exported from `@aithos/protocol-client` (v0.4 content-addressed; includes `migrateToV04`). |
| `sdk.onboarding`           | First-run identity / DID flows — re-exported.                                              |
| `sdk.mandates`             | Mint / verify mandates — re-exported. `createBundle` grants a cumulative multi-zone mandate under one shared grantee key; `revokeByGrantee` revokes the whole bundle in one shot. |

## License

Apache-2.0 © 2026 Mathieu Colla. See [LICENSE](./LICENSE).
