Skip to content

Trust

Security

Last updated

How Vestiarion protects the data and the money it handles, for anyone reviewing the app, its API or its integrations. Every statement describes what the code does today, and the code is public, so each one can be checked against it. What we do not claim is at the end.

Architecture and data flow

Vestiarion is one Next.js application, hosted on Vercel, with its data in a Supabase Postgres database. Data moves through it in five steps, and each step is a boundary:

  • In. People use the app in their browser, signed in through Supabase Auth with an emailed link or Google, never a password. Programs use the REST API, the SDK, the CLI or the MCP server with a workspace API key. Connected tools are read with the grant a workspace gave them, and tell Vestiarion about changes with webhooks it verifies. Scheduled jobs call their routes with the deployment's own token. The Chrome extension Vestiarion Invoice Capture uses a token of its own, issued when a member allows it and good for at most 90 days, that can only create and read the drafts it captured. A request is checked by its session, its key, its signature, its token or the token of a shared link, and a person's role decides what they may do.
  • Stored. A workspace's records are rows in Postgres, kept apart from other workspaces' as Tenant isolation describes. Secrets are encrypted before they are stored.
  • Decided. When this deployment has a model provider, the agent sends it the facts of one decision and gets back a proposal, which must match a fixed schema. A proposal is not a payment: rules in code check it, and people decide what the rules hold for them. Without a provider, a written rule-based policy proposes, and nothing is sent.
  • Paid. Payments are transfers on Arc, sent through Circle's Developer-Controlled Wallets. On Arc mainnet, a treasury in the owner's own wallet pays only through a spending limit contract that wallet deployed.
  • Recorded. Every decision, refusal, approval and payment is appended to the workspace's ledger, signed and chained by hash, and sent, signed, to the webhook endpoints the workspace set up.

Tenant isolation

No request for one workspace reads or changes another workspace's records. These answers cross workspaces, and name none:

  • the open numbers: counts and USDC totals across workspaces, such as the USDC paid and the USDC held in wallets, for customers and for the team apart, so on a network with one customer its total is that customer's; and the team's own workspaces' payments, up to the latest 20 with their amounts and transactions, and their USDC by day;
  • the latest decisions on the home page and the page for studios, the team's own workspaces' only, with their amounts and transactions;
  • whether any live workspace runs a real USYC reserve, a yes or a no;
  • payee history, sold over x402, which says for one address how many workspaces paid it, how many such payments were confirmed, and when the first and the last were made, with no amount.

Only the team's own workspaces' payments are ever listed, with amounts; a customer's are counted in totals and never listed.

  • In the server. Workspace tables are reached through db(), which adds the workspace to every query and every insert and refuses a row that names another workspace. The service-role client, platformDb(), reaches only an allowlist of platform tables and functions, such as memberships, API keys, webhook endpoints and connector tokens, many of which hold rows for every workspace. Code that uses it, in about 70 files, names the workspace in its queries itself, except where it looks a row up by a token or a key prefix, or runs a job across workspaces, such as webhook delivery and the daily cleanup. A lint rule forbids importing the raw database client anywhere in src outside the data access layer and the module that makes it.
  • In the database. Each request through db() runs as a dedicated Postgres role, with a token the server signs for that request alone: it names the workspace and the person, or system for scheduled jobs, scripts, and requests made with an API key, through the MCP server or by a webhook, and expires after 5 minutes. db() never falls back to the service role. Every workspace table that role can reach has row-level security with a restrictive policy that holds it to the token's workspace, whatever other policy is added later. A few database functions run only as the service role and write workspace rows after their own checks, such as pausing the agent, a customer's message on a statement link, turning on the USYC reserve and deleting a workspace.

The roles a browser session or an anonymous visitor carries hold no privileges on the tables at all, and the platform's own tables grant the workspace role nothing. Most links between workspace records carry the workspace in their foreign key, so the database refuses a link into another workspace's row. Exceptions: a EURC swap's link to its invoice, and a webhook delivery's links to its endpoint and its ledger entry, carry no workspace; and a column that may point at more than one kind of record, such as the payment an approval is for, has no foreign key at all. The swap stays under row-level security, and webhook deliveries are a platform table the workspace role cannot reach.

Encryption

The app and its API are served over HTTPS. Secrets are encrypted in the app before they reach the database, with AES-256-GCM and a fresh 96-bit IV for each one, under a master key the deployment holds as a secret. Each ciphertext is bound to its workspace and its field, and OAuth tokens and webhook secrets to their row too, so a copy moved into another workspace or field does not decrypt. Encrypted this way:

  • the Circle API key and entity secret a workspace connects;
  • each workspace's Ed25519 ledger signing key;
  • each webhook endpoint's signing secret, and the secret of each webhook Asana sends;
  • the OAuth access and refresh tokens of Xero, QuickBooks Online, Zoho Books, HubSpot, Pipedrive, Zoho CRM and Asana;
  • a Slack connection's bot token and posting address, bound to the workspace and the field;
  • a client pay link's token, so the link can be shown again on its card and the customer's statement page and sent in reminders; it is also hashed, and looked up by the hash.

Each envelope names the master key that sealed it. The first key listed encrypts and any listed key decrypts, so a new master key can be added without breaking what an older one sealed.

Some secrets are kept only as a SHA-256 hash: an API key's 256-bit secret, beside its prefix, compared in constant time; the tokens of invitations, payee links, customer statement links, early-payment offers, and shared receipts and reports; the codes that link a Telegram chat or a Slack account; and the Chrome extension's access and refresh tokens and its one-time sign-in codes. Some are never stored: the GitHub token of the person connecting GitHub is used once and dropped, a GitHub App installation's tokens are made for each run and kept in memory, and the password Pipedrive sends with its webhooks is derived from the master key for each company.

Access control

Every member of a workspace has one role: owner, admin, approver, accountant or viewer. Privacy says what each can see and do. The rules that decide whether money moves:

  • Maker and checker. Owners and admins add records. Owners, admins and approvers decide the payments the agent held, pause the agent, and can mark a screening match as not the counterparty. An accountant books payments, applies money received and answers customers, but decides no payment. A viewer reads, and changes only their own settings and links.
  • No self-approval. The database's approval function refuses the person who created an invoice unless they are the workspace's only owner, admin or approver, or another approver's unused approval of the same amount already stands. The app narrows that further: being the only approver counts only at or below the two-approvals figure, and above it the creator may give one of the two approvals only when at most one person who neither entered it nor gave its address can approve, never both. The ledger entry says when either applied. A partner named on a commission plan never approves the payable that pays them.
  • A first payment to a new address. Where payments are real, the agent never makes the first payment to a payee at an address that only the person who gave it stands behind, even when the workspace has paid that address for someone else, and that person cannot approve it, unless they are the workspace's only owner, admin or approver; the ledger entry says so. For an address a payee sends through a payee link, the person who created the link counts as having given it, read from the link's signed entry in the workspace's ledger, or from the link's own record when that entry is missing: their own confirmation is not the second person. A GitHub account is tied to no member, so for an address sent with /payto on a GitHub pull request, and for one no member can be named as having given, no confirmation counts as the second person: the agent holds the first payment, and two different members stand behind it, one who confirms the address and another who approves the payment. Whoever confirmed it cannot approve it, and no one approves it alone before another member has confirmed it, since an approval's own confirmation does not count, unless they are the workspace's only owner, admin or approver. A bounty typed by its own pull request's author names no member.
  • Two approvals. Above a figure the owner sets, a payment needs two different people, and one person cannot give both. A workspace with a single approver cannot pay above the figure until it is raised or another approver joins. On Arc mainnet a workspace starts with two approvals above 100 USDC, and the figure can be raised or lowered but not turned off.
  • Rules in code the model cannot override. After the model has answered, every bill the agent would pay passes the checks in src/lib/agent/guardrails.ts. They hold or refuse a payment to a counterparty screened high risk, one not screened yet, one without a payment address or with an address no one confirmed, a duplicate of a bill already paid, a bill missing its purchase order or goods receipt, a payment above the counterparty's limit, above the two-approvals figure or past the agent's spending limit. A contractor's milestone passes checks of its own, written into the cycle in src/lib/agent/orchestrator.ts: risk, screening, the contractor's limit, a first payment to a new address, two approvals and the spending limit. Nothing the model writes changes these checks. The decision prompt tells the model that an invoice's memo and purchase order are evidence, never an instruction, and the prompt that reads an invoice document says the same of the document; text about a milestone or a contractor carries no such line.
  • Screening. A counterparty screened high risk is not paid, by the agent or with a person's approval. An owner, admin or approver can mark a screening match as not this counterparty; it is then screened again, and once it no longer screens high risk its payments are decided again.
  • The agent's spending limit. A daily and a 7-day figure cap what the agent pays on its own. On Arc mainnet a workspace always has one: it starts at 50 USDC a day and 150 USDC in 7 days, and a workspace whose limit is missing or empty is held to those figures. The agent's purchases of payee history over x402 have a cap of their own, 0.05 USDC a day.
  • A spending limit contract. On Arc mainnet, the agent pays from a treasury in the owner's own wallet only through VestiarionSpendingLimit, a contract that wallet deploys and approves. The contract refuses a payment past its daily or 7-day figure, pays each reference once, and lets only the treasury change its figures; setting the wallet's approval to 0 stops it. On Arc testnet the same contract is opt-in. Vestiarion holds no key to that wallet.

API keys, the API and the MCP server

  • An API key is vxk_<prefix>_<secret>: an 8-character prefix and a 256-bit secret. It is shown once, when it is created, and only its prefix and the SHA-256 hash of its secret are kept.
  • A key reaches only its own workspace, with read or read and write access. Only owners and admins create and revoke keys, a workspace has at most 20, and a key stops working when the member who created it leaves the workspace.
  • Most requests read. Four add a record, with a write key whose creator can still add records: a counterparty, an invoice, a milestone or a payee link. Writes from one key come in bursts of up to 30, then one every 2 seconds.
  • Nothing in the API approves or pays directly. A payable added through it starts the agent's next cycle, and so does a milestone that names a pull request on a deployment with a GitHub token; any other milestone waits for the next scheduled cycle to check its pull request through the workspace's GitHub connection, or for a person to verify it. The agent decides what is added under the same rules and checks as in the app, so it may pay a counterparty whose address is confirmed, within its limits. An address given through the API is held as unconfirmed, so the agent pays nothing to it until a person confirms it. A new payee link replaces the counterparty's earlier one that was not used yet.
  • The MCP server's tools are the same operations, under the same keys, and can do nothing the API cannot.

The Chrome extension and its tokens

  • Connecting. The Chrome extension Vestiarion Invoice Capture connects with an OAuth authorization code and PKCE (S256 only), for the extension ids the deployment lists in CHROME_EXTENSION_IDS, and, once it lists any, the copy offered for download, whose manifest's public key gives it one fixed id. Its one redirect address is exactly https://<id>.chromiumapp.org/; any other client or address is refused on the page and never redirected to, and the page cannot be framed. Only an owner or admin, whose role is read again, allows it, for one workspace.
  • Tokens. A one-time code (vxc_, 5 minutes), an access token (vxa_, an hour) and a refresh token (vxr_, 30 days), each from 32 random bytes and kept only as a SHA-256 hash, in tables only the platform reads. Their audience is vestiarion-chrome and their scopes invoice_draft:create and invoice_draft:read: they create drafts and read the ones their connection made, and cannot add a bill, approve, pay or read any other record. They are not API keys: /api/v1 and the MCP server refuse them.
  • Rotation and revocation. Every refresh replaces both tokens. A code used twice, a replaced refresh token used again, or two refreshes with one token end the connection. A connection lasts at most 90 days from connecting, ends when its member can no longer add bills, and is deleted with the membership. The extension's Disconnect revokes it (RFC 7009), and Settings lists the connections, each with Disconnect. Each connection and its end are in the ledger, never a token.
  • Its routes. The extension reaches only /api/extension/v1: the token and revoke endpoints and its draft endpoints. Each answers CORS only for chrome-extension://<id> of an allowed id and refuses any other origin with 403, authenticates by the token alone, never a cookie, checks its audience and scope, and reads the member's role again on every request. A capture (POST) carries an Idempotency-Key. A JSON body is at most 100 KB, counted as it is read; a file is at most 4 MB by the Content-Length it declares, and one sent without it is refused with 411. Draft requests from one connection come in bursts of up to 60, then one a second, and token or revoke requests from one address in bursts of up to 10, then one every 6 seconds. Answers are never cached.

A draft becomes a bill only when an owner or admin adds it in the app, through the same checks as any bill. What the extension captures, and who reads it, is on the Privacy page.

Connected tools and their tokens

  • Connecting. Only an owner or admin connects a workspace to Xero, QuickBooks Online, Zoho Books, HubSpot, Pipedrive, Zoho CRM, Asana, Slack or GitHub, and for the first seven only after agreeing to what Vestiarion reads and writes there. The OAuth state is signed under the master key and expires after 10 minutes, a nonce in an HttpOnly cookie must come back with it, and the callback is accepted only for the same signed-in person, whose role is read again.
  • Zoho's data centers. The callback of Zoho Books and of Zoho CRM names the data center the person's Zoho account lives in. It is read only after every other check, and only from a fixed list of Zoho's data centers: the code, the client secret and the tokens go only to that data center's Zoho servers, never to an address the callback names, and each token is kept with its data center so it is never sent to another. Zoho CRM is called only at that data center's own API address, never at one an answer from Zoho names.
  • BlindPay. Bank payouts run under the deployment's own BlindPay key, a server secret never stored in the database or shown in the browser. An owner sets a workspace up, and an owner or admin adds a vendor's bank account; its CLABE is checked, sent to BlindPay and not kept, only its bank and last four digits. Every payout is held for a person, takes exactly the bill's amount or sends nothing, and is asked for under a key derived from the payment's, so BlindPay makes it once. BlindPay's notices are accepted only under its Svix signature, within five minutes, and only start a cycle that reads the payout from BlindPay itself.
  • Storing. The tokens of Xero, QuickBooks Online, Zoho Books, HubSpot, Pipedrive, Zoho CRM and Asana are encrypted, in a table only the platform reads, and are opened only in their own workspace's scope. They are never shown in the browser, a log, a model's prompt or a webhook.
  • Refreshing. A token is refreshed a minute before it expires, under a lease in the database, so one refresh runs at a time for a connection, and a new token is stored only against the version that was read.
  • Disconnecting. Disconnect revokes the grant at the provider, then deletes the tokens, even when the provider could not be reached, and records the disconnection in the ledger, best effort: a ledger write that fails does not stop it. Asana's webhooks are deleted before its grant is revoked, and revoking Pipedrive's grant uninstalls the app from the company, as Pipedrive documents it.
  • Disconnected on the provider's side. When someone disconnects QuickBooks Online at Intuit, its Disconnect page asks Intuit whether the connection still works, and deletes the tokens once Intuit refuses them. Pipedrive's notice that the app was uninstalled marks the connection to be connected again and keeps its tokens, which no longer work. Zoho sends no notice: access removed in Zoho is found when the next sync's refresh is refused, which marks the connection the same way.
  • Slack keeps its bot token and posting address encrypted. Disconnecting uninstalls the app from the Slack workspace, which revokes both, and deletes them; Slack's own uninstall and revocation notices delete them too.
  • GitHub is a GitHub App, and Vestiarion stores no token for it. Disconnecting deletes the installation's record; the app's access ends when it is uninstalled on GitHub.
  • Deleting a workspace, from its Settings or with its last member's account, deletes its tokens, its Slack connection and its GitHub record without calling any provider or uninstalling the Slack app, so disconnect each tool first to revoke its grant there.

What Vestiarion reads from and writes to each tool is on the Privacy page. QuickBooks Online is never written to, though the access Intuit grants could. Zoho Books and Zoho CRM are never written to, and the access Zoho grants each is read only.

Webhooks and scheduled jobs

Each webhook Vestiarion receives is checked as follows, and every shared secret is compared in constant time:

  • Circle: an ECDSA P-256 signature over the body, checked against Circle's public key for the key id it names. Circle's test notification is answered without a check and starts nothing. For any other, the body names the payment or wallet, which is looked up first; a cycle starts only after the signature verifies;
  • QuickBooks Online: an HMAC-SHA256 of the body, under the app's verifier token;
  • Pipedrive: a deal notice carries HTTP Basic credentials derived for each company, checked before the body is read, and its body is never used; the notice that the app was uninstalled carries the app's own client id and secret as HTTP Basic credentials, and only its company id is read;
  • Asana: the handshake secret, accepted once, for a webhook Vestiarion just created, then an HMAC-SHA256 of each body under that secret, kept encrypted;
  • Slack: an HMAC-SHA256 over the timestamp and the body, refused when the timestamp is more than 5 minutes away;
  • GitHub: an HMAC-SHA256 of the body;
  • Telegram: the secret token set for the bot;
  • Resend, for invoices by email: a Svix signature, refused when its timestamp is more than 5 minutes away.

Xero, Zoho Books, HubSpot and Zoho CRM send Vestiarion no webhooks: their records are read on a schedule.

The scheduled routes, the agent's tick, the FX and transfer watches, webhook delivery, accounting sync, the daily cleanup and the FX rates, take the deployment's AGENT_API_TOKEN as a bearer token, compared as SHA-256 digests in constant time. So does the demo reset, which is not scheduled: it also needs a typed confirmation, and is refused in production unless the deployment allows it.

The webhooks Vestiarion sends carry Vestiarion-Signature, an HMAC-SHA256 over a timestamp and the body under the endpoint's 256-bit secret, which is encrypted at rest. An endpoint must be HTTPS on port 443, with no credentials in its address, and every address it resolves to must be public: the address is checked and pinned when each delivery connects, and redirects are not followed, so a webhook cannot be pointed at Vestiarion's own network. A delivery waits at most 10 seconds and is tried up to 7 times; an endpoint that fails 20 times in a row is disabled. A workspace has at most 5 active endpoints. The verification guide has the check for a receiver.

The signed ledger

  • Each workspace signs its ledger with its own Ed25519 key, made when the workspace is created and encrypted as above. An entry's body hash is the SHA-256 of the canonical JSON of its actor, domain, action, summary and detail; its signature is over that hash; and its hash covers the previous entry's hash, its body hash and its signature. So changing any of those in an entry breaks every link after it. An entry's time, sequence number and signing key id are outside the hashes.
  • The database role a workspace's requests run as can only read and add ledger entries, never change or delete one. For that role, a database trigger takes the workspace's ledger lock and refuses an entry whose previous hash is not the current head, or whose hash is not the hash of its previous hash, body hash and signature. The trigger does not check the signature or the body hash; the verifier does. The sequence number is assigned by the database, and gaps in it are normal.
  • Any member can check the whole chain with Verify hash chain on the Audit log, and an API key can ask the same through GET /api/v1/ledger/verify.
  • The ledger exports as JSON, each entry with its signature and the public keys that vouch for them; the file itself is not signed. A standalone verifier, one file with no dependencies, checks an export without Vestiarion, and so does the CLI. Each export is recorded in the ledger, best effort.
  • An owner can rotate a workspace's signing key. The retired public keys are kept, so earlier entries still verify, and the rotation is an entry signed by the new key.

Data retention and deletion

  • A sandbox workspace with no activity for 60 days is deleted by a daily cleanup, unless it holds Circle credentials, a hosted wallet or a contract its owner's wallet deployed. A webhook delivery is deleted 30 days after it is delivered, or after it was created if it failed. The outcome of an API write sent with an Idempotency-Key, its response included, is given back for 24 hours, and deleted by the daily cleanup after that. Everything else in a workspace is kept until the workspace is deleted, or until a person removes it: disconnecting a tool, removing sample data or turning a feature off deletes what it held.
  • Text captured with the Chrome extension is removed when its draft is added or discarded, and 30 days after capture at the latest; a draft never sent for review is deleted within 7 days, and a file it sent is never kept, only its SHA-256. A Chrome extension connection is deleted 30 days after it ended or expired, and a one-time sign-in code a day after it expired.
  • An owner deletes a workspace from its Settings. Its records, ledger, API keys, webhooks, connector tokens, members and invitations are deleted in one step. A tombstone only the platform reads keeps its slug and name, who deleted it and when, and its ledger's length, head hash and signing key id. Its wallets and their USDC stay where they are held. The Vestiarion team's own records of a business it is in touch with, its lead and any revenue recorded for it, stay: their link to the workspace is cleared, and the lead's history keeps the workspace's id.
  • Anyone can delete their own account from the account menu, within the rules Privacy gives. A workspace where they are the only member is deleted with it, as above. In a workspace they share, their membership, the invitations they sent, the payment approvals they gave and their Slack and Telegram links go; the API keys they created are revoked and kept without their name; and the connections they made and the records they added stay, without their name. The ledger entries they caused keep their account's id, never their email, because the ledger is append-only.
  • After a tool is disconnected, the records already read from it stay with the workspace, and the ledger keeps only ids and names from it.

Privacy has the rest, including what a workspace stores and the rules for deleting an account.

AI use

  • The agent's model provider is Anthropic, OpenAI or DeepSeek, whichever this deployment configures. It receives what Privacy lists: the facts of each decision it is asked about; for each email that reaches a workspace's invoice address, its first PDF, .eml or .txt attachment, or else its text, and nothing for one that is too large, holds only images or comes over the workspace's rate; each document a member has it read; and each plain-text message a linked member sends the Telegram bot. Without a provider, a written rule-based policy decides, and nothing is sent.
  • Each provider handles these requests under its own API terms: Anthropic, OpenAI and DeepSeek. Someone running their own copy can point ANTHROPIC_BASE_URL, which Anthropic's SDK reads, at another Anthropic-compatible host, and OPENAI_BASE_URL or DEEPSEEK_BASE_URL at another OpenAI-compatible one.
  • A model's answer must parse against a fixed schema. An answer that does not is shown its error once, and if it still fails, the rule-based policy decides and the ledger says so. Either way, the answer is a proposal the rules in code check.
  • Vestiarion trains and fine-tunes no model, and never uses data from Xero, QuickBooks, Zoho Books, HubSpot, Pipedrive, Zoho CRM or Asana to train or improve any AI model.

Subprocessors

As of , these services process data for Vestiarion. Some receive data only when this deployment configures them or someone connects them. Privacy says in full what each receives.

  • Vercel: hosts the app and runs its server code. Data: every request to the app and its API, and the app's answers.
  • Supabase: holds the Postgres database and runs sign-in. Data: everything a workspace stores, and each person's sign-in account.
  • Circle: creates the wallets and moves the payments, through Developer-Controlled Wallets, CCTP, Gateway, its Smart Contract Platform, its Stablecoin Service and Modular Wallets. Data: wallet addresses, payment amounts and signed x402 payments, and a passkey's public key for a passkey wallet.
  • Resend: sends transactional email, and receives the emails sent to a workspace's invoice address, when this deployment configures it. Data: each recipient's email address and the message, and the emails that reach an invoice address.
  • Anthropic, OpenAI or DeepSeek: proposes the agent's decisions and reads invoices, when this deployment configures it. Data: the facts of each decision the agent asks it about; for each email to a workspace's invoice address, its first PDF, .eml or .txt attachment, or else its text; each document a member has it read; and each plain-text message a linked member sends the Telegram bot.
  • OpenSanctions: screens counterparties against sanctions lists, when this deployment configures it. Data: the names and jurisdictions of a live workspace's counterparties.
  • ExchangeRate-API: gives the day's rates against the US dollar. Data: nothing about you, your workspace or your bills.
  • RPC nodes: answer the server's reads of balances, fees and receipts: Arc's node (rpc.testnet.arc.network and rpc.mainnet.arc.io, and, where set, the ones ARC_RPC_URL and ARC_MAINNET_RPC_URL name, which take some reads, while receipts stay on the public node), and, for a receipt of a payout to another chain from Arc testnet, sepolia.base.org, sepolia-rollup.arbitrum.io and ethereum-sepolia-rpc.publicnode.com, PublicNode's among them. Data: the wallet addresses and transaction hashes read, which are already public on chain.
  • Telegram: sends the bot's messages to a member who connects their chat, when someone connects it. Data: the agent's decisions with counterparties' names, amounts and reasons, and the answers to the member's questions.
  • Slack: receives the posts and answers for a workspace connected to a Slack channel, when someone connects it. Data: the agent's decisions with counterparties' names, amounts and reasons, who decided a payment from Slack, and the sender's email address of an email to the invoice address that names no vendor or cannot be read.
  • GitHub: answers whether a pull request is merged or can still be opened, and runs the GitHub App, only when a milestone or its evidence names a pull request, or a workspace installs the app. Data: a pull request's owner, repository and number, and, for an installed app, a comment on the pull request a milestone was paid for.
  • Xero: read for bills and suppliers, and written to only to book a payment a person approved, when someone connects it. Data: the requests that read the organisation's bills and suppliers, and each booking: the bill's id, the account's id, the day, the amount and the Arc transaction.
  • Intuit (QuickBooks Online): read for open bills, vendors and bank accounts, and never written to, when someone connects it. Data: only the requests that read the company's bills, vendors and bank accounts.
  • Zoho (Zoho Books and Zoho CRM): read for open bills and vendors in Zoho Books and for deals with their accounts and contacts in Zoho CRM, at the data center the account lives in, and never written to, when someone connects it. Data: only the requests that read the Zoho Books organizations a person can reach and the bills and vendors of the one they pick, and the deals, accounts, contacts, deal pipelines and organization of the Zoho CRM organization connected.
  • BlindPay: pays a vendor's bills to its bank account in Mexico, in pesos over SPEI, from the workspace's USDC, when an owner sets up bank payouts. Data: the workspace's name and the owner's email, each vendor's CLABE and the name its account is held in, each payout's amount, and the operating wallet's address.
  • HubSpot: read for deals, companies and contacts, and written to only once an owner or admin allows it, when someone connects it. Data: what was collected on each planned deal, what is still owed, the last receipt and a link to the deal in Vestiarion.
  • Pipedrive: read for deals, organizations and persons, and written to only once an owner or admin allows it, when someone connects it. Data: what was collected on each planned deal, what is still owed, the last receipt and a link to the deal in Vestiarion.
  • Asana: read for the tasks linked to milestones, and written to only to make and delete their webhooks, when someone connects it. Data: the requests that read linked tasks, the webhooks made on them, and the email addresses it asks about to tell whether one belongs to an Asana user.
  • Google: counts page views with Google Analytics on every page, signed-in ones included, and signs people in where Google sign-in is turned on, when this deployment configures it. Data: each page's redacted path, its title, with a workspace page's replaced by its redacted path, its redacted referrer, which button started someone off, and what Google Analytics collects itself, such as the IP address, its cookies and the device; for Google sign-in, the sign-in itself.
  • Product Hunt: serves the badge image on three public pages. Data: the IP address and browser of whoever loads the image.

Secure development

  • The code is public on GitHub under the MIT License. Changes reach main through pull requests, and Vercel deploys main.
  • Changes are reviewed by an independent AI code reviewer before merge, and every defect it finds is fixed with a test that fails first; when a release's changes were not each reviewed so, its release notes say so. Each change must pass CI before it is merged.
  • CI runs on every pull request and every push to main: it checks that the lockfile describes the dependencies, type-checks, lints, runs the test suite and builds the app. It needs no secret.
  • The tests cover the ledger's tamper cases, the rules in code, tenant isolation and every migration, run on an in-process Postgres. Others hold the docs to the code: the webhook verification snippets run against the signing code, and this page and the privacy page are checked against the figures and settings they quote.
  • Secrets live in the deployment's environment, never in the repository: environment files are ignored by git, and the one committed example holds placeholders and no secrets. GitHub secret scanning, with push protection, is on for the repository.

Incident response

What can be done at once when something goes wrong:

  • Payments can be switched off for every workspace from the database, which every running deployment reads within 10 seconds, with no redeploy, or by the deployment's environment and a redeploy. While they are off, Vestiarion sends nothing that moves money, a person's approval included; what an owner's own wallet signs in their browser, and payments made to Vestiarion over x402, are not stopped by it. Payments on Arc mainnet can be held on their own, by an environment change and a redeploy. Reads and every page keep working.
  • In a workspace, any owner, admin or approver can pause the agent: it then runs no cycle and pays nothing on its own, while a person can still approve and pay. On Arc mainnet, an owner's wallet can set its approval for the spending limit contract to 0.
  • An owner can rotate the workspace's ledger signing key, and an owner or admin can revoke an API key. Disconnecting a tool revokes its grant, except GitHub, whose app is uninstalled on GitHub, since Vestiarion holds no grant for it.

When an incident affects workspaces, we tell their owners what happened and what we changed.

Reporting a vulnerability

Please report a vulnerability privately, with Report a vulnerability under the repository's Security tab on GitHub, and not in a public issue. The security policy says what is in scope and what to expect.

What we do not claim

  • Vestiarion holds no SOC 2 report or ISO 27001 certification, and has had no outside security audit or penetration test.
  • The contracts, VestiarionSpendingLimit and VestiarionEscrow, were written for Vestiarion and have not been audited.
  • Vestiarion is in beta. It changes every day and is provided as is, as the Terms say.
  • The master keys are a deployment secret, not held in a hardware security module. Someone who took over Vestiarion's server could use them, and move money from the wallets Vestiarion reaches through Circle: hosted wallets, and those in a Circle account a workspace connected. A limit enforced on Arc keeps the agent within its figures, but does not stop that. A treasury in the owner's own wallet on Arc mainnet is reachable only through its contract, within its figures.
  • The ledger is not anchored on chain. A copy kept outside Vestiarion, an export or a webhook receiver's, is what shows its history was not rewritten by someone in control of the server.
  • main has no branch protection and no required human second approval: the independent AI review and CI before each merge are how the team works, not a rule GitHub enforces. Dependencies are not updated automatically.
  • Rate limits are kept by each server instance, so they slow abuse rather than set a hard quota, and API reads are not limited.
  • The app does not send a Content Security Policy yet, except frame-ancestors 'none' on the Chrome extension's sign-in page, so that page cannot be framed.