# Add Sayso to an agent wallet

This page is for a coding agent (Claude Code, Cursor, Codex, OpenClaw or similar) working inside the project of an AI
agent that holds a wallet on Base. Follow the steps in order. Stop and ask the human only where this page says so.
Sayso checks every transaction the agent is about to sign against rules the owner signs, and never holds keys or money.
SDK: the npm package `saysoio` (MIT). Website: https://saysoio.com. Dashboard: https://app.saysoio.com.

## Rules that hold throughout

1. Ask the human for exactly two things: the **invite code** (looks like `oi_…`) and the **owner's wallet address**
   (`0x` plus 40 hex characters). Never ask for, read, hold or use the owner's private key or seed phrase. The owner
   signs the rules themselves, in the dashboard, with their own wallet.
2. The agent's own key stays where it is. Do not print it, move it, copy it or log it.
3. The API key you get at registration goes into the project's secrets, under the name `OATHLINE_API_KEY`: the
   project's existing secret store, or a `.env` file that is listed in `.gitignore`. Never print it, never commit it,
   never put it in a log line or a chat message.
4. Do not write `mode` in the code. Every wallet starts in **shadow mode** (Sayso watches and records; nothing is
   blocked) and the owner turns enforce on from the dashboard. **Until the owner has signed the rules, Sayso has
   nothing to check against and the wrapped wallet refuses every transaction**, so do not deploy or restart the
   wrapped agent in production before the owner has signed (step 7). Do not write `mode: 'shadow'` either: fixed in code,
   it would keep restarts, outages and signature requests unchecked even after the owner turned enforce on.
5. Make the smallest change that works. Do not refactor the agent, change its chain, or touch anything unrelated.
6. The Sayso server is `https://api.saysoio.com` (Base, chain id 8453). Use another address only when the human names
   one (for example a test server).

## Step 1: find where the wallet is created

Search the project for the place that creates the wallet the agent signs with. It is one of:

- **viem:** `createWalletClient({ account, chain, transport })`, usually with `privateKeyToAccount(...)` or an account
  from a provider (Turnkey, Privy, Coinbase CDP) as `account`.
- **Coinbase AgentKit:** `new ViemWalletProvider(walletClient)`, `CdpEvmWalletProvider.configureWithWallet(...)`,
  `PrivyEvmWalletProvider.configureWithWallet(...)`, then `AgentKit.from({ walletProvider, ... })`.

Note the variable that holds the unwrapped client or provider, and how the agent's address is read from it
(`account.address` for viem, `provider.getAddress()` for AgentKit).

Stop and tell the human, without changing anything, if:

- the chain is not Base (chain id 8453). Sayso's hosted server checks Base only (a test server for Base Sepolia
  exists at `https://sepolia.saysoio.com`; it needs no invite code and has no Telegram);
- the wallet is a smart account: a Safe, a Coinbase Smart Wallet, `CdpSmartWalletProvider`, any ERC-4337 account.
  Sayso cannot register those yet;
- you cannot find where the wallet is created.

## Step 2: install the SDK

Node 20 or newer. In the project folder, with the project's own package manager:

```
npm install saysoio
```

(`pnpm add saysoio` or `yarn add saysoio` where the project uses those.) viem is a peer dependency the project
already has when it uses viem or AgentKit; add it if missing.

## Step 3: ask the human for the two things

Ask for the invite code and the owner's wallet address, in one message. Check the address is `0x` plus 40 hex
characters, and that it is **not** the agent's own address (an agent that owned itself could sign its own rules;
Sayso refuses that). If the human offers a key or a seed phrase instead, refuse it and ask for the address only.

## Step 4: register the wallet once

Registration is one signature by the agent's wallet saying "this owner may set my rules". It happens once per wallet,
never on every deploy. Write a small script, `scripts/sayso-register.ts` (or `.mjs`), that loads the agent's account
or provider the same way the project does, unwrapped, and calls `registerWallet`. The snippet uses top-level `await`,
so it needs an ES module (`"type": "module"` in package.json, or an `.mjs` file); otherwise wrap it in an async `main()`:

```ts
import { registerWallet, providerSigner } from 'saysoio'

const API = 'https://api.saysoio.com'
const OWNER = '0x…'            // the owner's wallet address the human gave you
const INVITE = 'oi_…'          // the invite code the human gave you (leave `invite` out when the server needs none)

// viem: agentAccount is the account the project creates (privateKeyToAccount, or a provider's viem account)
const { apiKey } = await registerWallet(API, agentAccount, OWNER, { chainId: 8453, invite: INVITE })

// AgentKit: pass the UNWRAPPED wallet provider through providerSigner instead of the account
// const { apiKey } = await registerWallet(API, providerSigner(provider), OWNER, { chainId: 8453, invite: INVITE })
```

Then put `apiKey` into the project's secrets as `OATHLINE_API_KEY` without printing it: append
`OATHLINE_API_KEY=<the key>` to the git-ignored `.env` the project already loads (resolve its path from the project's
root, not from the working directory the script happens to run in), or write it to the secret store the project uses.
Print only "registered <agent address>" and the name of the variable you set. Delete nothing; the script
may stay (it does nothing harmful if run again: the server answers 409 "already registered" and makes no new key).

Run the script once. If it answers:

- `409 already registered`: the wallet was registered before; keep the key from that time (ask the human where it is)
  and go on;
- `403 an invite code is required` or `that invite code is not valid or has been used up`: ask the human for the right
  code;
- `the agent's wallet cannot be its own owner`: the address the human gave is the agent's; ask for the owner's;
- `this wallet is a contract` or `an EIP-7702 wallet … not accepted`: stop and tell the human; Sayso cannot register
  it from here.

## Step 5: wrap the wallet, with `mode` left out

In the file where the wallet client (or AgentKit provider) is created, right after it:

```ts
import { Oathline } from 'saysoio'

const oathline = new Oathline({
  apiUrl: 'https://api.saysoio.com',
  apiKey: process.env.OATHLINE_API_KEY!,
  chainId: 8453,
  // mode left out on purpose: the wallet starts in shadow mode and the owner switches it in the dashboard
  // timeoutMs left at its default (20 s): a shorter one skips the slow checks in shadow mode and loses their record
  onDecision: (result, tx, decision) => console.log(`[sayso] ${decision} ${result.verdict}: ${result.reason}`),
})
```

Then, as the **last** step on the client (after any `.extend(...)` add-ons the project uses, so none of them can
send around the check):

- **viem:** `const wallet = oathline.guard(walletClient)` and make every later use of `walletClient`
  (`sendTransaction`, `writeContract`, `signTransaction`, …) use `wallet` instead. Keep `walletClient`, its
  `account` and `transport.request` private: nothing else in the project may receive them.
- **AgentKit:** `const walletProvider = oathline.guardWalletProvider(provider)` and pass `walletProvider` (never
  `provider`) to `AgentKit.from`. Keep `provider`, `provider.getClient()` and the underlying client private.

Anything that signs through the raw client or provider, or through a library the project hands a raw key to (many
x402 payment clients do), skips Sayso. If you find such a path, do not rewire it; list it for the human in your report.

## Step 6: run one test check

In a small script (or once by hand), not in the agent's startup code. `AGENT_ADDRESS` is `agentAccount.address` for
viem and `provider.getAddress()` for AgentKit:

```ts
const r = await oathline.check(AGENT_ADDRESS, { to: OWNER, data: '0x', value: 0n })
console.log(r.verdict, r.reason)
```

Any verdict (`allow`, `ask`, `block`) means the setup works: the answer came back signed by Sayso for this exact
transaction. An error means something is wrong:

- `409 no owner-signed mandate is registered for this wallet`: expected when the owner has not signed the rules yet.
  Until they sign, every transaction of the wrapped wallet is refused: do not deploy or restart the wrapped agent in
  production yet. The check works the moment they sign. Tell the human and go on to step 7;
- `401 unknown API key`: `OATHLINE_API_KEY` is not set where the agent runs, or belongs to another wallet;
- `could not reach sayso` or `did not say which chain`: check `apiUrl`; `/health` on it answers with the chain.

Run the agent's usual tests once more if it has them.

## Step 7: tell the human what to do

Finish with this message, filled in:

> Sayso is wired in. Files changed: <list>. The agent's wallet <AGENT_ADDRESS> is registered to owner <OWNER>; the API
> key is in <variable name and where>. **Until you sign the rules, the wrapped wallet refuses every transaction, so I
> have not deployed or restarted the agent.** Once you have signed, the wallet runs in shadow mode: Sayso records what it
> would do and stops nothing.
> Now you: 1) sign the rules at https://app.saysoio.com/setup for <AGENT_ADDRESS> with your own wallet and the invite
> code (free, moves no money), then deploy or restart the agent; 2) link Telegram on the wallet's page when you want notices; 3) after a trial, turn
> enforce on from the wallet's page (it asks for Telegram first). The mode is never set in code.

The owner may sign the rules before or after the agent registers; both orders work, and the invite code counts once per
agent wallet either way. Done when the wallet's page in the dashboard says "Registered".

## Reference

- SDK README with every option and error: https://www.npmjs.com/package/saysoio
- The guide for people: https://saysoio.com/get-started
- What is checked and what is not: https://saysoio.com/proof
- Security and what Sayso sees: https://saysoio.com/security
