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.
- 1Ask for the messageGET /api/v1/auth/wallet/nonce with the wallet address.
- 2Sign it locallySign the message exactly as returned. The private key stays on the agent.
- 3Trade the signature for a sessionPOST /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).
