Set up oathline in 15 minutes
oathline checks every transaction your AI agent is about to sign against rules you write in plain words. It decodes and simulates the exact transaction, judges what it really does (not what the agent says), and answers allow, ask or block with a short reason and a signed receipt.
Choose your path
| Path | You have | What oathline does | Can it stop a transaction? | Time |
|---|---|---|---|---|
| A. Agent wallet + SDK | An agent that signs on Base with viem or Coinbase AgentKit | Checks each transaction inside the agent before signing | Shadow: no. Enforce: stops a tricked agent; a hacked agent server could skip it | 15 min |
| B. Safe cosigner | An agent that should not move funds alone | oathline holds 1 of 3 keys in a new two of three Safe and signs only what passes | Yes, if the agent's key is the only one stolen: without oathline's signature the agent alone cannot move the Safe | 30 min (test network) |
| C. Treasury watch | An existing Safe run by people | Judges every proposal waiting for signatures and tells you in Telegram | No: it warns before you sign | 10 min |
What you need (all paths)
- Node 18 or newer and viem.
- The SDK:
npm install @oathline/sdk viem(MIT, on npm). - An owner wallet. This wallet sets the rules; the agent can never change them. A browser wallet (MetaMask, Rabby) or, for a trial, a fresh key that holds nothing: it only signs messages. Never put the owner key on the agent's server.
- Telegram (recommended): approvals, notices and dashboard sign in. Turn on Telegram's two step verification.
- oathline's servers. One server works on one chain and names that chain in everything it signs.
| Chain | Check API | Use |
|---|---|---|
| Base (8453) | https://api.oathline.io | Paths A and C |
| Base Sepolia (84532), test money only | https://sepolia.oathline.io | Path B (no Telegram, not shown on the dashboard) |
Dashboard: app.oathline.io, sign in with Telegram.
oathline's own addresses, built into the SDK. The SDK trusts only these signing addresses and refuses a server whose chain is not the wallet's. You never take them from the server; you can check them against GET /health.
| Server | signer (signs verdicts) | cosigner (Safe key) |
|---|---|---|
| Base | 0x96489eBC8C0ff7FBa482536699B1f78d87f1306d | 0xAA5541299230c6D8036273Afa3F26a4A084B2C7D |
| Base Sepolia | 0x70aA81eB7ED4b1636b50c88E9B0D0CA3107DeE3f | 0x97d1089DaB43EE469Afd84B0713CdF00FfB08B7d |
Writing good rules (all paths)
Write the rules the way you would brief a new employee. Name the tokens the agent may use, a limit per trade and per day, the addresses money may go to, and the apps or contracts it may call.
Example: "Trade only ETH, WETH and USDC. At most 0.5 ETH per trade and 2 ETH per day. Money may only be sent to my saved address 0x…."
oathline turns this into fixed limits and shows you what it understood (shownToOwner) before you sign. If compiled.unresolved lists anything, that part did not become a rule; reword it. To change the rules, sign new ones; the newest signed set counts.
Path A: agent wallet + SDK
A1. Owner: sign the rules (on your own computer)
Option 1, dashboard. Open app.oathline.io, sign in with Telegram, go to Set up, connect your browser wallet (it is switched to Base for you) and choose My agent's own wallet. Enter the agent's wallet address and your invite code, write the rules, read what oathline understood, sign, and link Telegram.
Option 2, script (for a trial, the owner key is a fresh key holding nothing):
// owner.ts: run on YOUR computer, never on the agent's server
import { privateKeyToAccount } from 'viem/accounts'
import { compileMandate, setMandate, linkTelegram } from '@oathline/sdk'
const API = 'https://api.oathline.io'
const owner = privateKeyToAccount(process.env.OWNER_KEY as `0x${string}`) // a fresh key for the trial
const AGENT = '0xYourAgentWalletAddress' // the agent's wallet address
const rules = 'Trade only ETH, WETH and USDC. At most 0.5 ETH per trade and 2 ETH per day. ' +
'Money may only be sent to my saved address 0x….'
const { compiled, shownToOwner } = await compileMandate(API, rules) // about 20 seconds
console.log(shownToOwner) // read it: this is what you sign
if (compiled.unresolved?.length) console.log('Not turned into a rule:', compiled.unresolved)
await setMandate(API, owner, AGENT, rules, compiled, { invite: 'oi_…' }) // kept until the agent registers naming you
const { url } = await linkTelegram(API, owner, AGENT)
console.log('Open in Telegram and press Start:', url) // works once, expires in 15 minutes
A2. Agent: register once
import { registerWallet } from '@oathline/sdk'
const { apiKey } = await registerWallet('https://api.oathline.io', agentAccount, '0xOwnerAddress', { invite: 'oi_…' })
// shown once: store it as a secret, e.g. OATHLINE_API_KEY
The first owner a wallet names is the one that counts, so check the address; it cannot be changed later.
A3. Agent: wrap the wallet
import { Oathline } from '@oathline/sdk'
const oathline = new Oathline({
apiUrl: 'https://api.oathline.io',
apiKey: process.env.OATHLINE_API_KEY!,
chainId: 8453, // Base: pin it; oathline's signing address is built into the SDK
mode: 'shadow', // logs only; nothing is stopped
timeoutMs: 8000, // the longest oathline may delay a transaction in shadow mode
context: () => ({ reason: agent.lastThought, inputs: agent.lastSources }), // optional, treated as untrusted
onDecision: (r, tx, d) => console.log(`[oathline] ${d} ${r.verdict}: ${r.reason}`),
})
const wallet = oathline.guard(walletClient) // viem WalletClient
// AgentKit: const provider = oathline.guardWalletProvider(walletProvider)
Use wallet everywhere the agent used walletClient. Keep the original client private: anything that signs through it bypasses oathline, including libraries you hand a raw key to, such as many x402 payment clients.
A4. Check that it works
const r = await oathline.check(agentAccount.address, { to: '0xYourSavedAddress', data: '0x', value: 0n })
console.log(r.verdict, r.reason) // any verdict is fine: it arrived signed by oathline for this exact transaction
Then let the agent run as normal. Telegram: a notice whenever oathline would have asked or blocked. Dashboard: sign in with the same Telegram account to see every decision, what the transaction really did, and why.
A5. After a week: enforce
const oathline = new Oathline({ /* same as above */ mode: 'enforce', failClosed: true,
signatures: 'ask', // permits / x402 / messages: 'ask' (your onAsk hook decides), 'refuse', or 'allow'
onWaiting: (r, tx, a) => console.log(`waiting for the owner on ${a.channel}`) })
| Verdict | What happens |
|---|---|
| allow | Signed |
| ask | Approve / Reject goes to your Telegram (expires in 10 minutes). An approval covers that one transaction for 2 minutes. No answer means refused (OathlineNeedsApproval) |
| block | Refused (OathlineBlocked). A block can never be approved |
| oathline unreachable | Refused (OathlineUnavailable), unless failClosed: false |
Catch these errors in the agent and tell it why. Each carries .tx; the first two carry .result.reason.
Path B: Safe cosigner (Base Sepolia test network)
The Safe has three keys and needs two: the agent, oathline (signs only what passes), and you (kept offline). The agent alone moves nothing. You plus the agent can always act without oathline, including removing it. Base mainnet follows an outside security review of the cosigner.
You need: test ETH from a faucet for the owner (to create the Safe) and for the agent (to pay gas), and some test ETH in the Safe itself.
B1. Owner: create the Safe the plain way
oathline accepts only a fresh, plainly created Safe 1.4.1: the standard factory, exactly these owners, no modules, no guard. Safes made in the Safe web app are not accepted yet; importing an existing Safe is coming.
import { createPublicClient, createWalletClient, http, encodeFunctionData, decodeEventLog, getAddress } from 'viem'
import { baseSepolia } from 'viem/chains'
import { safeAbi, safeProxyFactoryAbi, registerSafePartial } from '@oathline/sdk'
const API = 'https://sepolia.oathline.io' // the test server: open, no invite code needed
const AGENT = '0xYourAgentWalletAddress', owner = privateKeyToAccount(process.env.OWNER_KEY as `0x${string}`)
const ZERO = '0x0000000000000000000000000000000000000000'
const pub = createPublicClient({ chain: baseSepolia, transport: http() })
const ownerWallet = createWalletClient({ account: owner, chain: baseSepolia, transport: http() })
const c = await (await fetch(`${API}/safe/contracts`)).json() // { singleton, factory, cosigner, chainId }
if (c.chainId !== 84532) throw new Error('wrong server')
// check c.cosigner against the table above, and singleton/factory against Safe's published deployments
const owners = [getAddress(AGENT), getAddress(c.cosigner), owner.address]
const saltNonce = BigInt(Date.now())
const initializer = encodeFunctionData({ abi: safeAbi, functionName: 'setup', args: [owners, 2n, ZERO, '0x', ZERO, ZERO, 0n, ZERO] })
const hash = await ownerWallet.writeContract({ address: c.factory, abi: safeProxyFactoryAbi, functionName: 'createProxyWithNonce', args: [c.singleton, initializer, saltNonce] })
const receipt = await pub.waitForTransactionReceipt({ hash })
let SAFE: `0x${string}` | undefined
for (const l of receipt.logs) { try { const ev = decodeEventLog({ abi: safeProxyFactoryAbi, data: l.data, topics: l.topics }); if (ev.eventName === 'ProxyCreation') SAFE = ev.args.proxy } catch {} }
await registerSafePartial(API, SAFE!, [owner], owner.address, { owners, threshold: 2, saltNonce }) // your half
Then sign the rules for SAFE exactly as in A1, Option 2, with API set to the test server. Skip linkTelegram: the test server has no Telegram, so an "ask" there is refused.
B2. Agent: complete the registration and send through the Safe
import { Oathline, registerSafe, sendViaSafe } from '@oathline/sdk'
const API = 'https://sepolia.oathline.io'
const SAFE = '0xTheSafeFromB1', OWNER = '0xTheOwnerAddressFromB1'
const { apiKey } = await registerSafe(API, SAFE, [agentAccount], OWNER) // once; completes the owner's half
const oathline = new Oathline({ apiUrl: API, apiKey, chainId: 84532 }) // the test server's addresses are built in
const txHash = await sendViaSafe(oathline, { publicClient, walletClient }, SAFE, { to, data, value })
// walletClient = the agent's own client: it signs the Safe transaction and pays the gas
There is no shadow mode here: without oathline's signature, the Safe does not move.
Limits today: single calls only (approve, then swap, is two transactions); no delegatecall, no batches; a cosigned Safe transaction does not expire until the next one replaces it; spending is counted when a transaction is allowed, not when it is executed.
Exit: you and the agent sign removeOwner (or anything else) together, without oathline.
Path C: treasury watch (Base mainnet)
Nothing about your Safe changes, and oathline holds no key. Every proposal waiting for signatures (from the Safe web app or anything using Safe's Transaction Service) is checked within about 15 seconds: Safe takeovers, delegatecall and gas refund tricks are flagged first; the exact execution is rehearsed on a copy of Base and the Safe's owners, threshold, modules and guard are compared before and after; the result is judged against your rules. The verdict goes to your Telegram ("Do not sign this" / "Look before you sign" / "Looks fine") and the dashboard.
Today this is one short script, signed once by one of the Safe's owners (the dashboard's "Watch a Safe" choice needs a chain connection the hosted dashboard does not have yet):
import { watchSafe, compileMandate, setMandate, linkTelegram, watchVerdicts } from '@oathline/sdk'
const API = 'https://api.oathline.io'
const SAFE = '0xYourSafeAddress' // safeOwner: a viem account of one of the Safe's owners
const { apiKey } = await watchSafe(API, SAFE, safeOwner, safeOwner.address, { invite: 'oi_…' })
const rules = 'Payments only to our three saved vendor addresses 0x…, 0x…, 0x…. At most 50,000 USDC per payment. Never change owners or threshold.'
const { compiled, shownToOwner } = await compileMandate(API, rules)
console.log(shownToOwner)
await setMandate(API, safeOwner, SAFE, rules, compiled)
const { url } = await linkTelegram(API, safeOwner, SAFE) // open in Telegram, press Start
console.log(url)
// later, from code: what oathline has said about recent proposals
console.log(await watchVerdicts(API, apiKey))
oathline cannot stop anything in watch mode. To make its "no" count, move to a cosigner Safe (Path B; Base mainnet after the outside review).
What oathline sees, and what it never sees
- Sees: the wallet address; each transaction before signing (
to,data,value); and anything yourcontext()returns. Do not put secrets in it. - Never sees: any key.
- Outside services involved: a simulation node, token data services and the decision model see transaction details.
- Every verdict is signed for that wallet, that transaction and that chain, with an expiry, and the SDK checks this.
Known limits (beta)
- Chain data can be up to 2 minutes old. A transaction that depends on something seconds earlier (an approval just before a swap) can show as "fails in simulation".
- Unreadable contracts. A call to a contract oathline cannot read is at best "ask", never "allow". Letting your rules name your own contracts is being built; expect false alarms on calls to your own contracts until then.
- x402 payments and token permits are not yet checked by oathline's server. In shadow mode they are only logged locally; in enforce mode they follow
signatures. - Sign in and approvals are through Telegram only. Wallet sign in, a signed approve page, Slack, Discord, email and webhooks are coming.
- Speed: a check takes about 1.2 seconds at the median on our benchmark; 1 in 10 took 7.7 seconds or longer.
- Not externally audited yet.
Troubleshooting
| You see | Do this |
|---|---|
could not reach oathline / did not say which chain | Check the URL and /health; pin chainId |
receipt signature is not from oathline | The server's signer does not match the one built into the SDK; check again /health and the SDK version |
could not turn the mandate into rules | The rule compiler is busy; try again in a minute |
| 429 "too many …" | Rate limits (about 60 checks a minute per key); retry with backoff |
| 503 "busy, try again" | The simulation queue is full or the chain copy is being refreshed; retry in a few seconds (shadow mode proceeds on its own after timeoutMs) |
| Telegram link "does not work any more" | Links work once and expire after 15 minutes; make a new one |
| Safe "cannot be registered" | Only a fresh, plainly created Safe with oathline's cosigner as an owner, and nothing executed yet |
| "invite code required" | The beta is by invitation: ask for a code |
Turning it off
- Path A: stop using the wrapped wallet.
- Path B: you and the agent remove oathline as an owner.
- Path C: nothing to undo; tell us to stop watching.
Questions or anything odd: message @oathlineio.