Free beta · by invitation

Set up oathline

Add oathline to your agent wallet in about 15 minutes. It starts in shadow mode: it watches every transaction and tells you what it would have stopped, without blocking anything.

  • Agent wallets start in shadow mode
  • Never holds your keys or money
  • Turn it off any time

You need an invite code from us. No code yet? Request an invite.

Pick your setup

Agent wallet

Before you start

  • Two people, one invite code. The owner sets the rules with their own wallet; the builder wires the agent. Both use the same code, and it counts once per agent wallet.
  • Node 20 or newer, with viem or Coinbase AgentKit.
  • The agent wallet is a plain key wallet. Smart wallets and other contract accounts cannot register yet.
  1. Owner: sign the rules

    Open app.oathline.io/setup, sign in with Telegram and choose My agent’s own wallet. Enter the agent’s wallet address and the invite code, write the rules, read what oathline understood, then sign with your browser wallet. Signing is free and moves no money. Link Telegram at the end.

    Writing good rules

    Write them the way you would brief a new employee: the tokens it may use, a limit per trade and per day, where money may go, and the contracts it may call.

    “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….”

    If your agent calls contracts of its own, name them with their full function signatures: “my agent may call its own contract 0x… (only addWorker(address))”. A listed call is then allowed when the simulation shows nothing at stake. This needs SDK 0.1.1 or newer. To change the rules later, sign new ones; the newest set counts.

    Prefer a script?
    // 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 setup = { chainId: 8453, invite: 'oi_…' }
    const owner = privateKeyToAccount(process.env.OWNER_KEY as `0x${string}`)
    const AGENT = '0xYourAgentWalletAddress'
    
    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, setup)
    
    const { url } = await linkTelegram(API, owner, AGENT, { chainId: 8453 })
    console.log('Open in Telegram and press Start:', url)                  // works once, for 15 minutes
  2. Builder: install and register the agent once

    npm install @oathline/sdk viem
    import { privateKeyToAccount } from 'viem/accounts'
    import { registerWallet } from '@oathline/sdk'
    
    const agentAccount = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`)
    const API = 'https://api.oathline.io'
    const { apiKey } = await registerWallet(API, agentAccount, '0xOwnerAddress',
      { chainId: 8453, invite: 'oi_…' })
    // shown once: store it as a secret, e.g. OATHLINE_API_KEY

    Run this once, not on every deploy. The owner a wallet names first is permanent, so check the address.

    AgentKit, or no raw key
    // `provider`: your AgentKit wallet provider, not wrapped yet
    const { apiKey } = await registerWallet('https://api.oathline.io', {
      address: provider.getAddress() as `0x${string}`,
      signTypedData: (args) => provider.signTypedData(args),
    }, '0xOwnerAddress', { chainId: 8453, invite: 'oi_…' })
  3. Builder: wrap the wallet, in shadow mode

    import { createWalletClient, http } from 'viem'
    import { base } from 'viem/chains'
    import { Oathline } from '@oathline/sdk'
    
    const oathline = new Oathline({
      apiUrl: 'https://api.oathline.io',
      apiKey: process.env.OATHLINE_API_KEY!,
      chainId: 8453,
      mode: 'shadow',      // watches only; leave it out and you get 'enforce'
      timeoutMs: 8000,
      onDecision: (r, tx, d) => console.log(d, r.verdict, r.reason),
    })
    
    // create the client WITH the agent's account: the check is for that address
    const walletClient = createWalletClient({ account: agentAccount, chain: base, transport: http() })
    const wallet = oathline.guard(walletClient)   // the agent now uses `wallet`, never `walletClient`

    Keep the original client private: anything that signs through it, or through a library you give a raw key to (many x402 payment clients do), skips oathline. On the wrapped wallet, sendTransaction, writeContract and signTransaction are checked; sendCalls, signAuthorization and sendRawTransaction pass straight through, so do not give those to the agent.

    AgentKit
    const walletProvider = oathline.guardWalletProvider(provider)
    const agentkit = await AgentKit.from({ walletProvider, actionProviders: [/* yours */] })
    // give the agent only `agentkit`; keep `provider` private
  4. 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 came back signed for this exact transaction

    Then let the agent run as usual. You get a Telegram notice whenever oathline would have asked or blocked, and every decision with its reason is on the dashboard.

  5. After about a week: enforce

    Shadow and enforce, in one minute. Shadow mode checks everything and blocks nothing. In enforce mode a block stops the signature, an ask waits for your Approve or Reject on Telegram, and if oathline cannot be reached the agent refuses to sign. Today the builder switches in the code, as below; a switch the owner signs on the dashboard is planned.
    const oathline = new Oathline({
      /* same as above, and: */
      mode: 'enforce',
      failClosed: true,
      timeoutMs: 30000,
      signatures: 'refuse',   // permits, x402, signed logins: 'refuse', 'ask' + onAsk, or 'allow'
    })
    VerdictWhat happens
    allowSigned
    askApprove or Reject goes to your Telegram, for up to 10 minutes. No answer means refused. Without a linked Telegram it is refused at once, unless you set an onAsk hook
    blockRefused. A block can never be approved

    Catch OathlineBlocked, OathlineNeedsApproval and OathlineUnavailable in the agent and tell it why. Raise your framework’s tool timeout above 10 minutes so an ask is not cut off.

In enforce mode a tricked agent’s transaction is refused when the check catches it. A hacked agent server is different: it holds the key and can sign without the SDK. For that, put the money behind a Safe cosigner.

Safe cosigner

Base Sepolia test network, test money only. On Base mainnet it opens later, first for a few design partners.

The Safe has three keys and needs two: the agent, oathline (it signs only what passes) and you, kept offline. The agent alone moves nothing. You and the agent together can always act without oathline, including removing it.

You need test ETH from a faucet for the owner (to create the Safe), for the agent (gas) and in the Safe itself. The test server is open: no invite code.

  1. Owner: create a fresh Safe and register your half

    oathline accepts only a plainly created Safe 1.4.1: the standard factory, exactly these three owners, no modules, no guard. Safes made in the Safe web app are not accepted yet; adding oathline to a Safe you already have is coming.

    import { createPublicClient, createWalletClient, http,
      encodeFunctionData, decodeEventLog, getAddress } from 'viem'
    import { baseSepolia } from 'viem/chains'
    import { privateKeyToAccount } from 'viem/accounts'
    import { safeAbi, safeProxyFactoryAbi, registerSafePartial } from '@oathline/sdk'
    
    const API = 'https://sepolia.oathline.io'
    const AGENT = '0xYourAgentWalletAddress'
    const 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 Reference below, and singleton and 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 })
  2. Owner: sign the rules for the Safe

    import { compileMandate, setMandate } from '@oathline/sdk'
    
    const rules = 'Pay only my saved address 0x…. At most 0.05 ETH per payment. Never change owners.'
    const { compiled, shownToOwner } = await compileMandate(API, rules)
    console.log(shownToOwner)                   // read it: this is what you sign
    await setMandate(API, owner, SAFE!, rules, compiled, { chainId: 84532 })

    No invite code on the test server, and no Telegram: an ask there is refused.

  3. Agent: complete the registration and send through the Safe

    import { Oathline, registerSafe, sendViaSafe } from '@oathline/sdk'
    
    const API = 'https://sepolia.oathline.io'
    const SAFE = '0xTheSafe', OWNER = '0xTheOwner'
    const { apiKey } = await registerSafe(API, SAFE, [agentAccount], OWNER)   // once
    
    const oathline = new Oathline({ apiUrl: API, apiKey, chainId: 84532 })
    // walletClient: the agent's own client; it signs the Safe transaction and pays the gas
    const txHash = await sendViaSafe(oathline, { publicClient, walletClient }, SAFE,
      { to, data, value })

    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 transaction stays valid until the next one replaces it. Spending is counted when a transaction is allowed, not when it runs. Exit: you and the agent sign removeOwner together, without oathline.

Watch a Safe

Base mainnet. Warns, cannot stop anything. Nothing about your Safe changes and oathline holds no key.

Every proposal waiting for signatures (from the Safe app or anything using Safe’s Transaction Service) is checked within about 15 seconds. oathline rehearses the exact execution on a copy of Base, compares the Safe’s owners, threshold, modules and guard before and after, and judges the result against your rules. You get “Do not sign this”, “Look before you sign” or “Looks fine” on Telegram and the dashboard.

One owner of the Safe runs this once

One signature from one of the Safe’s owners is enough. (The dashboard’s “Watch a Safe” button is not available on the hosted dashboard 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)

console.log(await watchVerdicts(API, apiKey))               // later: what oathline said about recent proposals

To make oathline’s “no” count, move the money to a Safe cosigner (test network today).

Reference

oathline’s servers and signing addresses
ChainServerFor
Base (8453)https://api.oathline.ioAgent wallets, Watch a Safe. By invitation
Base Sepolia (84532), test moneyhttps://sepolia.oathline.ioSafe cosigner, trials. Open, no Telegram, not on the dashboard

The SDK trusts only these signing addresses, built into it, and refuses a server whose chain is not the wallet’s. You can compare them with GET /health.

ServerSigns verdictsSafe key
Base0x96489eBC8C0ff7FBa482536699B1f78d87f1306d0x7ad359D3E76eCbf460156457cbB61f1643149841
Base Sepolia0x70aA81eB7ED4b1636b50c88E9B0D0CA3107DeE3f0x97d1089DaB43EE469Afd84B0713CdF00FfB08B7d
What oathline sees, and what it never sees
  • Sees: the wallet address, each transaction before signing (to, data, value) and whatever your optional context() returns. Put no secrets in it.
  • Never sees: any key.
  • Outside services: a chain data node, token data services and the decision model see transaction details. Details on the privacy page.
  • Every verdict is signed for that wallet, that transaction and that chain, with an expiry, and the SDK checks it.
If a key is lost or leaked
  • The oathline API key can ask checks for that one wallet and read the answers. It cannot change rules, approve, sign or move money. If it leaks, message @oathlineio at once.
  • The agent’s key stolen: the thief can sign without the SDK. Move the funds to a new wallet and register that one. With a Safe cosigner, the agent’s key alone moves nothing.
  • The owner’s key lost: the rules in force stay, but nobody can sign new ones for that wallet. Use a new agent wallet with a new owner.
Known limits of the beta
  • Chain data can be up to 2 minutes old. A transaction that depends on one sent seconds earlier can show as “fails in simulation”.
  • Contracts oathline cannot read are at best “ask”, never “allow”. For an agent wallet, your rules can name your own contracts and functions; a listed call is then allowed only when the simulation shows nothing at stake.
  • Permits, x402 payments and signed logins are not checked by oathline’s server yet. In enforce mode they follow your signatures setting.
  • Telegram only for sign in and approvals. Wallet sign in is coming.
  • Speed: a check took 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
“an invite code is required”Pass { invite: 'oi_…' }, to setMandate too. No code: request an invite
409 “already registered”Nothing to do: keep the API key from the first time. Register once, not on every deploy
“this wallet is a contract”Use a plain key wallet, or the Safe cosigner
409 “no owner-signed mandate”The owner signs the rules for this exact address (step 1)
401 “unknown API key”Check OATHLINE_API_KEY; register the wallet
429 “too many requests”About 60 checks a minute per key; retry after retry-after
503 “busy” or “the fork does not answer”Retry shortly; shadow mode carries on by itself
“receipt signature is not from oathline”Check apiUrl and chainId; update the SDK
Telegram link does not workLinks work once, for 15 minutes; make a new one
“that invite code is not valid or has been used up”Check the code; ask @oathlineio for more places
409 “already linked to another owner”That owner stays; use a new agent wallet
403 “this API key belongs to a different wallet”Use each wallet’s own key
“could not reach oathline” or “did not say which chain”Check the URL and /health; pin chainId
“could not turn the mandate into rules”The rule compiler is busy; try again in a minute
Safe “cannot be registered”Create a new Safe as in the Safe cosigner step 1 and register it before using it
Turning it off
  • Agent wallet: stop using the wrapped wallet.
  • Safe cosigner: you and the agent remove oathline as an owner.
  • Watch a Safe: nothing to undo; tell us to stop watching.

To delete a wallet’s rules and Telegram link, message @oathlineio. Signed log entries are kept as the record that a verdict was given.

Stuck, or something looks odd? Message @oathlineio on X.