How Covert works.
One page, with an index. Each section is marked with where its piece stands. For a walk-through, command by command, see the quickstart for agents.
Live. Each piece on this page is marked with where it stands. See the status page.
Overview
Covert is a private wallet built for AI agents on Robinhood Chain. The agent is the user. People show up to set budgets and to approve what crosses a line they drew.
There are three pieces. The vault runs on the owner's machine, holds the seed and every private note, and checks every action against the agent's rules. Agent keys are revocable tokens, one per agent, that can ask the vault for actions and can sign nothing on chain. The owner approves what crosses the line and freezes agents, at the machine today and from a phone app once it exists.
An agent can hold ETH, USDG, SPY, NVDA, QQQ and TSLA, each in its own shielded pool. It can swap between them, pay an address, and pay other Covert agents through pay codes, transfers and tabs. Live
The sections below say what each part does. The security page says who is trusted and what stops each failure. The fees page says what it costs.
Install Live
The vault is one npm package, covertd, for macOS and Linux with Node 24 or later. It has the mainnet contract addresses and https://covert.money built in. The one setting it needs is an RPC of its own, below. Its SHA-256 is published beside it, at covertd-0.1.1.tgz.sha256.
npm install -g https://covert.money/dl/covertd-0.1.1.tgz
Then, as the owner, at a terminal. Under the owner's own login the vault keeps its files in ~/.config/covert:
covert init --auto-passphrase # the seed, encrypted, and a random passphrase file
covert seed backup # the 24 words once, then three of them back
covert fetch-artifacts # circuit files, checked against pinned SHA-256
printf '%s\n' 'https://<your RPC for Robinhood Chain>' > ~/.config/covert/verify_rpc; chmod 600 ~/.config/covert/verify_rpc
echo '{ "verifyRpcFile": "~/.config/covert/verify_rpc" }' > ~/.config/covert/config.json
covert daemon # the vault; prints its socket
covert shield USDG 100 --key-file /absolute/path/to/wallet.key
covert agent mint --name my-agent --rules-file rules.json
covert fund ag_<id> USDG 50
The backup. The seed is the money: anyone with the words can take every note. The words are shown once on the terminal for paper, and the owner types three of them back. seed backup --to <file> writes an encrypted file instead, under a separate passphrase typed twice. The vault refuses to start, and no money command works, until the backup is confirmed, so this step needs a person. The words restore the notes; state.json in the same folder holds the agents, their rules and their balances, and needs a backup of its own.
Your own RPC. The vault settles every payment from the finalized block of an RPC that Covert does not run. Robinhood Chain's public endpoint keeps only recent state and cannot answer at the finalized block, so a vault left on it sends payments that never settle, and their money stays held. Any provider that serves Robinhood Chain with full state works; a free Alchemy app for Robinhood Chain does.
Deposits come in fixed sizes so that an odd amount cannot tie a later action to its deposit: 1, 10, 100 or 1000 USDG, and 0.001, 0.01, 0.1 or 1 of ETH, SPY, NVDA, QQQ or TSLA. The key file holds the depositing wallet's private key as hex, mode 600, and that wallet pays the gas. A deposit is checked against sanctions and exploit lists, usually within a few minutes, and becomes usable once that is done and its block is final, about 15 minutes on Robinhood Chain.
As a service. Under the owner's login, anything that runs as the owner, the agent included, can read the seed. To run the vault under its own OS user, covert install --agent-user <the agent's OS user> writes a service definition, setup.sh and uninstall.sh into ./covert-install and runs nothing. sudo sh ./covert-install/setup.sh creates the vault user, copies the code to /usr/local/lib/covert owned by root, makes the vault's home /var/lib/covert (mode 700) and the socket folder, adds the agent's user to the vault's group, installs the service without starting it, and writes the covert and covert-owner commands. Then the owner creates the seed as the vault user, starts the service, and runs every later owner command through covert-owner. The quickstart has the full list.
covert install --agent-user <the agent's OS user>
sudo sh ./covert-install/setup.sh
covert-owner init --auto-passphrase
covert-owner fetch-artifacts
sudo launchctl bootstrap system /Library/LaunchDaemons/com.covert.covertd.plist # macOS
sudo systemctl enable --now covertd # Linux
The vault Live
The vault is a background process called covertd. Installed as a service, it runs under its own operating system user: a LaunchDaemon with a user name on macOS, or a systemd unit with a dedicated, sandboxed user on Linux. Its home is readable by that user alone. It listens on a Unix socket that members of its group can connect to, and on 127.0.0.1 when the owner starts it with --http-port. It answers nowhere else, so the agent runs on the same machine.
The seed is encrypted at rest. One process lock keeps two copies from running on one home. Owner commands go through a second socket inside the vault's private home, so an agent running as a different user cannot read the seed, edit the rules or reach an owner command, even with shell access. covert install writes the setup files, and running setup.sh is the one step that needs an admin password.
An attacker with admin rights on the machine can take everything on it. Keep on that machine only what you would accept losing that way.
Agent keys Live
An agent key is cvk_ and 64 hex characters, shown once and stored hashed. It is not a crypto key and can sign nothing on chain. Each key has one sub-account, with its own balances and ledger entries, and one set of rules. The owner mints one with covert agent mint --name <name> --rules-file <file>, lists them with covert agent list and revokes one with covert agent revoke <id>.
A key the owner minted with --can-mint can mint child keys for sub-agents, with covert keys mint --name <name> --key <idempotency key> --budget '{"USDG":"10"}' or POST /v1/keys. A child's rules are never wider than its parent's, its budget is carved from the parent's balance, and its spending counts against every ancestor's caps. Children cannot mint. Revoking a key revokes its children, and their money returns up. Moving value between sub-accounts of one owner is a ledger entry inside the vault: instant, free, and not on chain.
If a key leaks, whoever holds it can do what the agent could do, inside its rules, until the owner or the parent revokes it. Agents that share one OS user can read each other's key files, so a child key is a real boundary only when it runs as its own user.
Rules and hard ceilings Live
Each agent has its own rules, set by the owner at the machine with --rules-file at mint or covert rules set <id> --rules-file <file>:
- assets
- The assets the agent may touch. Default: USDG.
- caps
- Per action and per rolling day, in US dollars, for each action type:
pay,swap,transfer,tab,invoice,credits,room. A type with no cap is switched off. - maxOpenTabs, maxTabUsd
- How many tabs the agent may have open as the payer, and the most one may hold. Zero, the default, switches tabs off.
- payees
{"mode": "open"}, or an allowlist of addresses (for pay) and pay codes (for transfers and tabs). An empty allowlist, the default, pays nobody.covert payee add <id> <address or cvt1 code>adds one.- approvalAboveUsd
- An action worth more than this waits for the owner.
- hours
- Optional days and hours, in a UTC offset.
- maxSlippageBps
- The price impact and slippage the agent accepts on assets that need a quote. Default 100, at most 300.
- receive
- The agent's side as a payee: transfers and tabs on or off, which assets, minimum and maximum amounts, an incoming dollar limit per day, how many tabs it holds, and the accepted value at which its vault closes a tab early.
USDG counts at par. Other assets are valued by an on-chain quote at the time. If the vault cannot value an asset, it refuses.
Two settings change at the machine and nowhere else, never from the phone and never by an agent. The hard ceilings are the most any rule or approval can allow: by default $250 per action, $500 per agent per day and $1,000 per day for the whole vault, set with covert ceilings set --values '{"perActionUsd":100}'. The new payee wait means a payee added remotely cannot be paid for 24 hours; one added at the machine is active at once.
Intents Live
An agent never builds a transaction. It states an intent, and the vault handles notes, proofs, routes, gas, fees and retries. The agent needs no ETH. Amounts are decimal strings in whole units, such as "12.5", and assets are symbols.
- balance, history
{};{ limit?, status? }. What the agent holds, what its rules still allow today, and its recent jobs.- quote
{ intent: "pay" | "swap", ...that intent's fields }. The fee, the minimum, what goes public, the live set size for a pay, whether approval is needed, and whether it fits the balance and the rules.- pay
{ asset, amount, to, key, wait_s? }. Pay a 0x address with a valid checksum. Contracts and EIP-7702 delegated addresses are refused. The recipient gets the amount less the relay fee.- swap
{ from, to, amount, slippage_bps?, key, wait_s? }. Exchange one asset for another through allowlisted pools. The output stays shielded in the agent's balance.- paycode
{}. This agent's pay code.- transfer
{ to: <pay code>, asset, amount, memo?, key, wait_s? }. One payment to another Covert wallet.- invoice, invoice.status
{ asset, amount, memo?, expires_s?, key };{ invoice_id }. Ask for money from any wallet, and see whether it came.- tab.open, tab.topup, tab.list, tab.pay, tab.close
- The buyer's side of a tab: open one to a pay code, add to it, list them, sign a voucher, ask the seller to close.
- tab.accept
{ voucher, price, asset, key }. The seller's side: count a voucher a buyer sent.- credits.buy, chat
- Defined, but the credits gateway is closed, so they fail before anything is paid. See inference.
- room.open, room.close
- Defined; they answer
not_available. See rooms.
Ways in. Three, all one API. A command line with JSON output: covert do <intent> '<json>', with shortcuts such as covert balance and covert paycode. HTTP over the vault's Unix socket, or over 127.0.0.1 when the owner enables it: POST /v1/intents/<intent>, GET /v1/balance, GET /v1/history, GET /v1/jobs/<id>, GET /v1/approvals/<id>, GET /v1/events and GET /v1/whoami, each with Authorization: Bearer <agent key>, described by an OpenAPI 3.1 document at /openapi.json and printed by covert openapi. And an MCP server, covert mcp, a thin client on stdio whose tools are named covert_<intent> and take idempotency_key in place of key. The key comes from COVERT_AGENT_KEY_FILE (a mode 600 file) and the socket from COVERT_SOCKET.
Untrusted text. Memos, receipts, refusal reasons, model replies and any text from other agents or services come back as labelled, length-capped fields marked untrusted: true. They never appear inside tool descriptions or in messages addressed to the agent. Tool descriptions are fixed strings. An unknown field in a request is an error, never ignored.
Jobs and retries Live
Jobs. Anything that moves money returns a job: { job, intent, status, eta_s, created_at, request, result?, error?, approval? }. wait_s, 0 to 120, makes the call wait for it; the default is 0, or 60 through MCP. Status is queued, asking (the payee's vault is being asked for a fresh note), awaiting_approval, running, unconfirmed, succeeded, failed, cancelled or interrupted. unconfirmed means the transaction was sent: the job settles from the finalized block of an independent RPC, about 15 minutes on Robinhood Chain, and never from a relayer's word. Completion arrives as an MCP notification, on the event stream (job.completed), or as a signed webhook the owner sets at the machine.
Safe to retry. Every action that changes something takes a key of 8 to 128 characters from A-Za-z0-9._:-. The same key with the same request returns the same job, marked replayed: true, and never runs twice, including after a restart. The same key with a different request is idempotency_conflict. Keys are kept seven days, and a refusal that creates no job does not use one up.
Limits. A key may have 50 open jobs, 32 waiting calls and 4 event streams. Each agent and its children share 60 changes a minute, 30 quotes a minute, and 6,000 voucher calls a minute.
Errors Live
Every refusal has a stable code and says which rule applies and what would work.
{ "error": "<stable code>", "message": "<text>",
"rule": "<which rule>", "field": "<which field>", "over_by_usd": <number>,
"resets_at": "<time>", "retry_after_s": <number>, "works": { ... },
"detail": { "text": "<text from outside the vault>", "untrusted": true } }
message appear when they apply. A refusal before a job exists is an HTTP error; a job that fails is HTTP 200 with "status": "failed" and this shape in error.Branch on error, not on message. The codes:
- Keys and requests
unauthorized,revoked,frozen,forbidden,machine_only,invalid_request,unknown_intent,not_found,not_available,idempotency_key_required,idempotency_conflict,rate_limited.- Rules
asset_not_allowed,cap_per_action,cap_per_day,ceiling_exceeded,outside_hours,payee_not_allowed,payee_waiting,carve_exceeds_parent,approval_denied,approval_expired.- Money
insufficient_balance,amount_too_small,funds_not_ready,unvaluable,price_impact_too_high,destination_unavailable,ledger_exceeds_notes.- Agent payments
payee_offline,payee_refused,voucher_invalid,voucher_short,voucher_replayed,tab_not_open,tab_expiring.- The vault and its services
not_configured,upstream_error,outcome_unknown,internal.
Each code's HTTP status and meaning are in the OpenAPI document, under x-error-codes.
Approvals and freeze Coming later
When an action is over the approval line, it waits as a pending approval and the agent gets a job in awaiting_approval with the approval's id. Today the owner decides at the machine: covert approvals lists what waits, covert approve <id> runs exactly the stored request, and covert deny <id> refuses it. Approving checks everything again, and an approval expires after 24 hours. covert freeze [agent] stops one agent and its children, or with no argument the whole vault, at once; covert unfreeze [agent] reverses it. An agent can read its own approvals and nothing else, and cannot approve, deny or unfreeze.
The phone app is not built. As designed, it is a web app installed to the Home Screen, with no send screens. The vault prints a QR code that carries its public key, a mailbox id and a one-time code. The phone creates a passkey and sends its public key back, encrypted. After pairing, every message between phone and vault is end-to-end encrypted, and our server passes ciphertext and ids. An approval is a passkey signature over the exact action, which the vault checks itself. Each one works once and expires. Our server could delay or drop a message; it could not forge an approval.
Pay codes Live
A pay code is a public string of 89 characters starting cvt1, from the paycode intent. An agent can post it on a site, an MCP server or an agent card. It holds no address. It holds a mailbox id and an encryption key. Each agent has its own, so two agents of one vault cannot be linked by their codes. A restore of the seed together with the backed-up state.json keeps every pay code.
To pay one, the payer's vault sends a sealed request to the mailbox: an amount, an asset, whether it is a transfer or a tab, and an optional memo. The payee's vault applies its receive rules and answers, sealed, with details made for this one payment: a fresh private note, a fresh exit owner, and for a tab a fresh key and its terms. Nothing in an answer is reused, so nothing on chain links back to the pay code.
Our server sees sealed messages and mailbox ids. It cannot read them, and it cannot change them without the vaults noticing. The payee's vault must be running: if it does not answer within about two minutes, the payer's job fails with payee_offline, nothing is sent and the money is back. The payee's receive rules decide what it takes: transfers and tabs on or off, which assets, minimum and maximum amounts, a daily incoming limit, and how many tabs it holds.
Transfers Live
A transfer is one payment from one Covert wallet to another, with transfer and a pay code. While the payee's vault answers, the job is asking and the money is held. Then the payer's vault proves it owns a private note and moves the amount into the payee's one-time note in the same pool, in one transaction sent by a relayer.
The amount is what leaves the payer's balance; the payee's note is that less the relay fee and any vetting fee. The result carries the transfer id, the transaction, and a receipt signed by the payee's one-time key over the transfer id, the amount, the time and the note. If the payee's rules refuse, the job fails with payee_refused and the payee's reason in detail, as untrusted text.
On chain, an amount moves within one pool, at a time. Neither wallet and neither pay code appears. If the pool cannot take the note, the payment does not happen and the money stays in the payer's balance. Nothing is ever paid out in public instead, and money never sits in the contract. The money arriving needs no new screening; it is spendable once its block is final.
Tabs Live
A tab is a shielded payment channel for paying per call.
- Open.
tab.open { to, asset, amount, expiry_s?, key }. One private transaction moves the amount plus the relay fee out of the payer's balance into the tabs contract, locked to two one-time keys, one per side. The payee's vault takes tabs of 2 to 30 days; the default is 7. The tab appears intab.listand takes vouchers once its open is final, about 15 minutes after it is sent.tab.topupadds to it later. - Pay.
tab.pay { tab_id, amount, key }signs a voucher for the running total and answers{ tab_id, cumulative, remaining, voucher }. A voucher is a signature. It costs nothing, takes milliseconds, and goes straight from agent to agent, not through our servers. The vault never signs past the tab's amount or the agent's rules. - Accept. The seller's agent runs
tab.accept { voucher, price, asset, key }and serves when it answers{ ok: true, tab_id, cumulative, accepted, remaining }, which happens when the voucher raises the tab's accepted total by at least the price. Two requests carrying one voucher never both pass. - Close. The seller's vault closes the tab with the highest voucher, at least a day before the expiry, or earlier once the accepted value passes its owner's threshold, or when the payer asks with
tab.close. The contract pays that amount, less fees, into the seller's private note and returns the rest to the payer's. A relayer sends the close, so the seller needs no ETH. A tab closes once. - Refund. If the seller never closes, the payer's vault takes everything left after the expiry by itself.
The seller can claim no more than the payer signed. The payer pays as it goes, so the most either side risks is one call. Pausing the contract stops new tabs and top-ups, never a close or a refund.
On chain there are two moments: a tab opened for an amount, and a tab closed with two amounts. The wallets, the pay codes, the number of calls and the price per call stay off chain.
x402 tabs Live
x402 is the open pay-per-call standard for HTTP. Covert adds a scheme called covert-tab, on x402 version 2. A seller answers an unpaid request with 402 Payment Required and a PAYMENT-REQUIRED header carrying a price, an asset and its pay code. The buyer's vault opens a tab or reuses one, signs a voucher, and the buyer retries with it in a PAYMENT-SIGNATURE header. There is no facilitator and no transaction per call.
Covert has an x402 kit for Node with two parts. The seller middleware (for Node http, Express, fetch-style handlers and Hono) prices each route, gets the pay code from the seller's vault once, answers 402, and on a paid request calls tab.accept and runs the handler only on ok: true. The buyer fetch wraps fetch: it pays a covert-tab 402 at or below a maximum price per asset by finding an open tab with tab.list or opening one with tab.open, then signing a voucher with tab.pay. Neither part holds a key or reads the chain; the vault decides everything.
Getting it. npm install https://covert.money/dl/covert-x402-0.1.0.tgz, with its SHA-256 beside it at covert-x402-0.1.0.tgz.sha256. It has no dependencies and talks to the agent's own vault. A seller or a buyer can also run the same intents itself, to this format. Each header is base64 of a JSON object.
402, PAYMENT-REQUIRED
{ "x402Version": 2, "error": "payment_required", "resource": { "url": "<the request's URL>" },
"accepts": [ { "scheme": "covert-tab", "network": "eip155:4663", "amount": "10000", "asset": "USDG",
"payTo": "cvt1<seller's pay code>", "maxTimeoutSeconds": 60,
"extra": { "decimals": 6, "paymentFlow": "upfront" } } ] }
retry, PAYMENT-SIGNATURE
{ "x402Version": 2, "accepted": { <the entry above> }, "payload": { "voucher": "cvtv1.<...>" } }
200, PAYMENT-RESPONSE
{ "success": true, "transaction": "", "network": "eip155:4663", "amount": "",
"extra": { "chargedAmount": "10000", "asset": "USDG", "tab": { ... } } }
amount is in the asset's smallest units (USDG has 6 decimals, the others 18); the vault's intents take decimal strings. A refusal is another 402 with fresh requirements and PAYMENT-RESPONSE { "success": false, "errorReason": "<code>" }.Limits. It speaks x402 version 2 alone, and the covert-tab scheme alone. A new tab takes vouchers once its open is final, about 15 minutes. Each tab carries one paid request at a time, so concurrency means more tabs. A tab must be large enough to pay for its own close, or the seller's vault refuses it with payee_refused. If a response is lost after the seller's vault accepted the voucher, the buyer has paid for that one call. Paying ordinary x402 sellers needs a room, which is not built.
Invoices Coming later
An agent can ask for money from anyone. invoice answers with a transaction any wallet can send, pay: { to, value, data, uri }, and for a token the approval to send ahead of it, pay.approve. The vault sends nothing itself. The money lands as a fresh note in the agent's shielded balance, less the 0.10% invoice fee. The agent's rules need an invoice cap.
invoice.status answers open, paid or expired, and the paying transaction once there is one. A payment made after the expiry still lands and is still credited. The payer's wallet, the amount and the time are public. The agent is not. Invoice payers are screened against the same lists as any outside deposit, which usually takes a few minutes.
Rooms Coming later
Not built: room.open and room.close answer not_available.
As designed, a room is for things that have to be public: trading on a public exchange, lending on a public market, paying an ordinary x402 API. The vault derives a fresh address, funds it with a private payment, and sweeps what is left back into the pool when the job is done. Everything a room does is public. Who owns it is not. The vault would warn before any action that links a room to its owner, such as funding it from a known wallet.
Inference Coming later
Not open. The credits.buy and chat intents exist in the vault, but the credits gateway is closed, so they fail before anything is paid. Do not use them yet.
As designed, the agent buys model credits from its shielded USDG balance as blind-signed tokens and spends them on model calls under a per-agent budget. The gateway sees the prompt. It cannot tie the prompt to the wallet that paid. The public record is the purchase amount. Inference opens once the model provider's approval covers Covert, and not before.
Statements and SDKs Coming later
Not built. The vault keeps each agent's jobs and an append-only ledger. A tax ledger export, where every sale of a stock token is a taxable event, and a statement for one counterparty showing where a balance came from, are planned.
SDKs for TypeScript and Python, and tools for the Claude Agent SDK, the OpenAI Agents SDK, LangChain, the Vercel AI SDK, CrewAI and ElizaOS, are planned. Coming later Until then, use the command line, HTTP or MCP.
What is public and what is not
| Action | Public | Not public |
|---|---|---|
| Deposit into a pool | The wallet, the amount, the time | Which later action came from it |
| Swap | The amount, the time, the assets | Who swapped |
| Pay an address | The recipient, the amount, the time | Who paid |
| Transfer | The amount, the time | Both sides |
| Tab | The open amount and expiry, and the two close amounts | Both sides, the calls, the price per call |
| Invoice | The payer's wallet, the amount, the time | The agent |
| Room | Everything the room does | Who owns it |
| Inference | The purchase amount | Which wallet a model call belongs to |
Balances, rules, approvals, pay-code traffic and history stay in the vault. The relayer sees the IP address that submits each job. One vault watches all its agents' mailboxes from one IP address, so the mailbox server can link those mailboxes by address and timing, though not by anything in them.
Privacy here means hiding among the other approved deposits in a pool. A small crowd hides less. Timing, matching amounts and careless funding can still give an action away, and a service you call can see where the request comes from unless you route it through a proxy. Every quote for a pay shows the live set size for its pool.
Issuer powers
Covert holds tokens that other companies control, and those companies keep powers over them. The issuer of USDG can freeze balances. The registry for the stock tokens can pause or burn them, including the balance held by a pool or by the tabs contract. If that happens to a pool, the money in it is affected, and Covert cannot undo it.
Low per-deposit and per-pool caps and one pool per asset limit how much any one action can touch. Caps rise through a 48 hour delay and no other way.
Minimums and fees
Anything that goes on chain from the private balance has to cover its own gas inside the pools' relay-fee cap, which is 1% today. The smallest workable amounts are about $7 for a payment, $9 for a transfer or a tab, and $11 for a swap. A quote returns the exact minimum for a payment or a swap at the time; a transfer or a tab below it fails with amount_too_small, and works.minimum gives the figure. Per-call payments go through tabs, and vouchers have no minimum.
For every fee, see fees.