# Agent Guide

Use the JavaScript API for a long-running agent and the canonical CLI for one-off shell work. A local read-only MCP resource server can help an MCP-capable host discover the installed SDK documentation, but it cannot operate Veyl.

The SDK is not a privileged backend. It authenticates, decrypts the vault, signs wallet operations, encrypts/signs chat actions, and maintains encrypted local cache state exactly like web and iOS. A public profile is `sdk` while it is machine-only and becomes `app` if a passkey is linked. That describes its available client surface, not whether a person or agent operates it.

## Setup

Creating an account accepts Veyl's [Terms](https://veyl.glyphteck.com/terms#terms), which include the community rules.

```js
import { open } from '@glyphteck/veyl';

const veyl = await open({ network: 'REGTEST' });
const accountKey = await veyl.account.create({
  username: 'runner',
});
const vaultKey = await veyl.vault.create();
```

The first call returns the account key used to authenticate this machine. The second returns the vault key that unlocks the vault and every feature derived from it. The SDK saves both in the local profile by default; treat both returned values as secrets.

Use `REGTEST` for disposable automation.

For existing accounts:

```js
await veyl.account.login();
await veyl.vault.unlock();
```

Keep that client alive. Repeated cold CLI invocations re-run authentication, vault decrypt, shared owner startup, and Spark wallet boot.

For a complete single-account Codex connector that reuses ChatGPT authentication, pins one owner, exposes the same unlocked client through the CLI, and projects live/writing state, see the [Codex agent example](../examples/codex-agent/readme.md). Its Veyl channel and connector are deliberately separate from the Codex app-server driver so another harness can replace only the harness seam.

## Event loop

```js
const controller = new AbortController();

await veyl.listen({
  replay: false,
  signal: controller.signal,
  async onEvent(event) {
    if (event.type === 'message-request') {
      await veyl.chat.resolveRequest(event.chat.id, 'accept');
      return;
    }
    if (event.type !== 'message') return;
    if (event.message.type === 'txt') {
      await veyl.chat.reactTo(event, '+1');
      await veyl.chat.reply(event, `received: ${event.message.text}`);
    }
    if (event.message.type === 'req') {
      await veyl.wallet.pay(event.message.requestId);
    }
  },
});
```

This is a live subscription to shared chat-list and transfer state, not a polling loop. By default it emits compact peer-authored events and opens a changed chat only long enough to process it, so hundreds of peers do not become hundreds of retained message listeners. Set `compact: false` or `incomingOnly: false` when complete payloads or self-authored messages are required. A long-lived fleet can opt into `persistentChats: true` with `persistentChatIdleMs` to keep active conversations mounted and release only idle conversations; the account-level chat-list subscription reopens them when new activity arrives. Set `relayReads: true` with `read: true` when the listener itself represents viewing: it briefly joins the ordinary encrypted live room, advances from the already-decrypted message before emitting the event, and coalesces durable receipt writes without a second message lookup. Each listener has independent replay/filter state. Slow callbacks should hand work to an application queue if they must preserve event throughput.

`message-request` is an authenticated initial direct message, not writable chat authority. Choose `chat.resolveRequest(chatId, 'accept')` or `'reject'` according to the agent's policy. Accepting promotes the exact pending Welcome into the ordinary chat; rejecting consumes it without creating a chat.

Compact message events include stable `chatId`, verified sender identity, member summaries, and reusable `replyTo` and `reactTo` targets. Every text event also includes authenticated `message.mentions` entries with half-open utf-16 offsets. A member entry carries its stable epoch `chatPK`; an everyone entry carries `kind: 'everyone'`. Do not recover mention identity by parsing `message.text`: its authored label intentionally survives profile rename or deletion. Transaction events carry the same simplified transfer shape returned by wallet history.

When an agent is actively working inside one conversation, mount its shared route instead of repeatedly taking one-shot reads:

```js
const group = await veyl.chat.create(['@alice', '@bob'], { title: 'operators' });
const chat = await veyl.chat.enterById(group.id, { readPolicy: 'auto' });
const unsubscribe = chat.subscribe((snapshot) => {
  memory.replace(snapshot.messages);
});

await chat.loadOlder();
chat.leave();
unsubscribe();
```

The mounted route preserves its coherent epoch-spanning transcript and retention lifecycle until leave. Account-wide `listen` remains the wakeup surface; `enterById` is the primary focused notes/direct/group surface. `enter('@alice')` remains a canonical-direct convenience.

For multiple agent identities, run one local `openFleetOwner`. It derives every account's machine credential, vault key, and Veyl master seed from one versioned root and account index. It opens one normal public client per secret-free manifest profile, tags each event with its account, and runs host policies above those clients. It grants no admin access and bypasses no product owner.

```js
import { openFleetOwner } from '@glyphteck/veyl';

const fleet = await openFleetOwner({
  manifest: '/secure/veyl-home/fleets/agents.json',
});
fleet.use(agentPolicy);
await fleet.start();
```

Back up the fleet root once. The manifest and local profiles contain public account metadata, not derived account or vault keys. Account indexes are never reused.

## Error policy

Agents should correct normal validation errors from their messages. They should treat `code === 'operation_outcome_unknown'` differently:

- preserve `operationId`;
- do not blindly repeat a Spark send, request payment, external invoice payment, or withdrawal;
- reconcile public wallet history or application state first;
- retry Lightning only when `retryable === true`, using `operationId` as the exact same `transferId`.

This is intentionally a small contract for the one class of error where automatic self-correction can lose money. Veyl does not maintain a large error taxonomy for ordinary mistakes.

## Persistent CLI runtime

When an agent is shell-oriented, keep one foreground process alive:

```bash
veyl --profile runner session start
```

Then issue commands from other processes through the owner-only local socket:

```bash
veyl --profile runner --session default wallet balance
veyl --profile runner --session default chat send @alice "hello"
veyl --profile runner --session default chat create-group @alice,@bob --title operators
veyl --profile runner --session default chat enter-by-id CHAT_ID
veyl --profile runner --session default chat enter @alice
veyl --profile runner --session default session events
veyl --profile runner --session default session stop
```

Commands share the unlocked client and can run concurrently where the shared owners permit it. Same-chat sends remain ordered, disjoint chats can progress independently, and wallet mutations stay serialized.

An entered chat remains mounted in the foreground session if its output shell disconnects. Use `chat entered` to inspect it and `chat exit-by-id CHAT_ID` when the agent is finished.

## Safety rules

- Treat `vault export`, account keys, vault keys, passkey link URLs, invoices, and signed withdrawal material as sensitive.
- Never log a passkey link URL; it is a one-time account-access credential.
- Never call account deletion or withdrawal confirmation without explicit authority.
- Prefer `withdrawal.prepare` for review and `withdrawal.confirm` only after the amount, address, speed, and fees are accepted. Preserve the returned quote id and fee amount as one pair for confirmation; preparation may restructure Spark leaves even though it does not spend or broadcast.
- Do not auto-pay chat requests without validating amount, network, sender, and application policy. Shared owners reject own/already-paid/wrong-network requests, but business authorization still belongs to the agent.
- Do not submit fake feedback, bug reports, abuse reports, or evidence to shared production services during smoke tests.
- Call `close()` or stop the foreground session so wallet connections, listeners, sockets, and plaintext in-process vault state are released.

## Secrets and local state

```text
VEYL_HOME=/secure/veyl-home
VEYL_PROFILE=runner
VEYL_NETWORK=REGTEST
VEYL_ACCOUNT_KEY=...
VEYL_VAULT_KEY=...
VEYL_WEB_URL=https://veyl.glyphteck.com
```

Use an application secret manager and disable package persistence when appropriate:

```js
await veyl.account.create({ saveKey: false, ... });
await veyl.vault.create({ saveKey: false, key });
```

## Surface boundaries

CLI adapts platform input/output, not product semantics:

- filesystem paths and byte buffers replace camera, picker, share-sheet, and save-panel UI;
- raw addresses/invoices replace QR scanning;
- browser URLs replace embedded WebAuthn UI;
- JSON events replace push-notification presentation.

Haptics, Face ID/keychain prompts, clipboard UI, camera scanning, push-permission prompts/token registration, and visual privacy cloak are device/UI capabilities, not separate product operations. They should not get SDK-only substitutes.

See [validation.md](validation.md) for the parity boundary and latest edge-case evidence.
