Sign in with a wallet

An agent can hold its own account, or run yours, with nothing but a wallet. It signs a Sign-In with Ethereum message on its side. Only the signature is sent.

Its own account

The session works on the whole Management API: keys, usage and top-ups, and on the organization side brand, prices, domains, customers and earnings.

  1. 1
    Ask for the message
    GET /api/v1/auth/wallet/nonce with the wallet address.
  2. 2
    Sign it locally
    Sign the message exactly as returned. The private key stays on the agent.
  3. 3
    Trade the signature for a session
    POST /api/v1/auth/wallet. Sessions last 24 hours.
import { privateKeyToAccount } from "viem/accounts";

const KLINN = "https://app.klinn.dev";
const wallet = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);

const { message } = await fetch(`${KLINN}/api/v1/auth/wallet/nonce?address=${wallet.address}`).then((r) => r.json());
const signature = await wallet.signMessage({ message });
const { token } = await fetch(`${KLINN}/api/v1/auth/wallet`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message, signature }),
}).then((r) => r.json());

const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
await fetch(`${KLINN}/api/v1/reseller`, { method: "PATCH", headers, body: JSON.stringify({ enabled: true, brand: { name: "Acme AI" } }) });
await fetch(`${KLINN}/api/v1/customers`, { method: "POST", headers: { ...headers, "Idempotency-Key": "signup-42" }, body: JSON.stringify({ name: "Northwind", externalRef: "user_42" }) });

A wallet we have not seen before gets a new account of its own, starting at $0, and that wallet is its owner. It tops up in USDC with POST /api/v1/topup. Each nonce is single use, valid 10 minutes, and bound to the address you ask with. GET /api/v1/agent lists every call in one JSON document written for agents.

Join an organization

To let an agent run your organization instead, add its wallet on the Wallet page, or POST /api/v1/wallets. That only invites it. Nothing happens until the agent signs in with join set to your account id (on that page, and as account.id in GET /api/v1/whoami). The message it signs then names your account.

A sign-in without join never uses the invite. A join without an invite gets 403 not_invited. A wallet that already has an account gets 409 wallet_taken until it leaves that account.

const { message } = await fetch(`${KLINN}/api/v1/auth/wallet/nonce?address=${wallet.address}&join=${ACCOUNT_ID}`).then((r) => r.json());

A wallet that joined acts as a delegate. It can do everything except decide who has access or set up payments, and it can leave with DELETE /api/v1/wallets/{its address}. Removing the wallet ends its sessions at once and revokes every key it created, including keys it made for your customers. The response says how many (revokedKeys).