Early beta, free · by invitation

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.

On Base mainnet start in shadow mode: it records what oathline would have said and blocks nothing. Enforcement through a Safe cosigner runs on the Base Sepolia test network (test money only) until the cosigner has had an outside security review. The beta is by invitation: the code came with the message that sent you here, or ask for one.

Choose your path

PathYou haveWhat oathline doesCan it stop a transaction?Time
A. Agent wallet + SDKAn agent that signs on Base with viem or Coinbase AgentKitChecks each transaction inside the agent before signingShadow: no. Enforce: stops a tricked agent; a hacked agent server could skip it15 min
B. Safe cosignerAn agent that should not move funds aloneoathline holds 1 of 3 keys in a new two of three Safe and signs only what passesYes, if the agent's key is the only one stolen: without oathline's signature the agent alone cannot move the Safe30 min (test network)
C. Treasury watchAn existing Safe run by peopleJudges every proposal waiting for signatures and tells you in TelegramNo: it warns before you sign10 min

What you need (all paths)

  1. Node 18 or newer and viem.
  2. The SDK: npm install @oathline/sdk viem (MIT, on npm).
  3. 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.
  4. Telegram (recommended): approvals, notices and dashboard sign in. Turn on Telegram's two step verification.
  5. oathline's servers. One server works on one chain and names that chain in everything it signs.
ChainCheck APIUse
Base (8453)https://api.oathline.ioPaths A and C
Base Sepolia (84532), test money onlyhttps://sepolia.oathline.ioPath 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.

Serversigner (signs verdicts)cosigner (Safe key)
Base0x96489eBC8C0ff7FBa482536699B1f78d87f1306d0xAA5541299230c6D8036273Afa3F26a4A084B2C7D
Base Sepolia0x70aA81eB7ED4b1636b50c88E9B0D0CA3107DeE3f0x97d1089DaB43EE469Afd84B0713CdF00FfB08B7d

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}`) })
VerdictWhat happens
allowSigned
askApprove / Reject goes to your Telegram (expires in 10 minutes). An approval covers that one transaction for 2 minutes. No answer means refused (OathlineNeedsApproval)
blockRefused (OathlineBlocked). A block can never be approved
oathline unreachableRefused (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 your context() 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 seeDo this
could not reach oathline / did not say which chainCheck the URL and /health; pin chainId
receipt signature is not from oathlineThe 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 rulesThe 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.