# Changelog

> Changes to the API and webhooks, newest first.

Changes to the API and webhooks that affect an integration, newest first. Changes to the product that do not touch this surface are not listed.

## 2026-10-05: The agent sends again on its own only what one approval may pay

- An `ap_reconcile` or `milestone_reconcile` entry can now carry `notResubmittedBecause: "workspace.two_approvals"`, with `execution.resultingStatus: "held"`. The payment was never sent and is above the workspace's figure for two approvals. Two people's approvals of it had not been used to pay it, so the agent held it rather than sending it again.
- Every ledger entry is still appended the same way, and its shape does not change.
- Nothing changes in `/api/v1`.

## 2026-10-05: Approve and pay draws on the USYC reserve, and a person's cash back stays

- **Approve and pay brings back what the operating wallet lacks.** A person's paying approval of a new USDC payment from the operating wallet, on Arc testnet or through CCTP with its fee, may now first bring cash back from the reserve. It does so when the wallet cannot cover the payment and the reserve can cover the rest. It brings back the difference, rounded up to the next micro-USDC, with a fifth more of a CCTP fee, which is read again just before the burn. A CCTP payout whose fee could not be read is not covered. **Pay now** on a held milestone does the same.
  - A new kind of `cash_brought_back` entry: actor `human`, with `detail` `{ by, reason: "approval", invoiceId | milestoneId, amount, neededUsdc, feeCushionUsdc?, operatingBalance, reserveBalance, earnMode, executed, executionNote?, execution? }`. It comes before the payment's own entry.
  - A redemption that did not move is recorded too, with `executed: false` and why in `executionNote`, and nothing is paid. One Arc testnet had not yet confirmed may still land; asking again for the same payment finds that same redemption rather than sending a second one.
  - `approval_paid` and `milestone_approval_paid` then carry `fromReserveUsdc`.
  - When the wallet and the reserve together fall short, Approve and pay refuses, as before, before anything moves. No ledger entry is written for a refusal.
- **A person's Bring cash back stays for 24 hours.** For 24 hours after a `cash_brought_back` entry with `reason: "person"`, the agent's treasury decisions sweep nothing.
  - Their entries (`hold`, or `redeem_from_usyc` for what falls due) then carry `personCashBack: { amount, at, until }`.
  - A sweep the model chose anyway is recorded as a `hold`, with what it chose in `boundedByCode`.
- Nothing changes in `/api/v1`.

## 2026-10-05: A person's payout to another chain takes the agent's route

- An `approval_paid` entry for a new payment to another chain can now carry `payout.route: "gateway"`, with `gatewayBalanceUsdc`, on a person's first attempt. A person's Approve and pay now chooses the route as the agent does: Gateway when the workspace's Gateway balance covers the amount and Gateway's fee, and Gateway costs no more than CCTP; CCTP otherwise. Before, a person's first attempt always went through CCTP.
- Approve and pay now refuses, before anything is sent, a CCTP payout the operating wallet cannot cover with its fee, and a Gateway payout the Gateway balance cannot cover. No ledger entry is written for a refusal.
- Nothing changes in `/api/v1`.

## 2026-10-05: Two approvals above a figure the workspace sets

- **A new guardrail rule**, `workspace.two_approvals`. Above the figure a workspace's owner sets, the agent never pays or schedules a payable, or releases a milestone: code holds it for two people. Its entry carries `guardrailBlocked: true`, `execution.resultingStatus: "held"` and `observed.twoApprovalsAbove`, the figure it was weighed against. A EURC payable is weighed at its `usdcValue`.
- **New ledger actions**, each with actor `human`:
  - `approval_given` (domain `ap`) and `milestone_approval_given` (domain `contractor`): the first of two approvals, which sends nothing. Its `detail` is `{ by, invoiceId | milestoneId, counterpartyId, amount, currency, address, usdcValue?, twoApprovalsAbove, fewApprovers? }`.
  - `approval_policy_changed` (domain `system`): an owner set, raised, lowered or turned off the figure. Its `detail` is `{ by, from, to }`, each a figure in USDC or null for off.
- **`approval_paid` and `milestone_approval_paid`** gain `approvals: [{ by, at }, …]`, the earlier first, and `twoApprovalsAbove` when two people approved the payment. Their `summary` then ends "(the second of two approvals)".
  - They also carry `fewApprovers: true` when whoever entered the payment, or gave its address, gave one of the two because no one else could. As many of the two as can come from other people must.
- An `invoice_reopened` entry's `followUp.changes` can say that two approvals were turned off, or the figure raised to cover the payment. A `milestone_reopened` entry's can say the same.
- Nothing changes in `/api/v1`.

## 2026-10-05: A payment Circle did not answer is looked for, never sent again blind

- **A payment whose send Circle did not answer is now in flight, not held.** Its decision's `execution.resultingStatus` is `matched`, as for any payment Circle is still settling.
  - Vestiarion then looks for the send on Circle by its reference before anything is sent again.
  - If Circle has it, it is recorded as the payment.
  - If Circle has listed nothing 15 minutes after the send, the payment is sent as a new one.
  - Reject and Return look for it the same way before they act.
- **Payments can also be switched off from the database** (`npm run payments -- off "<reason>"`). Every running deployment stops within 10 seconds, with no redeploy. While they are off, the agent appends no entries.
- Approve and pay still records a transfer that was already sent while payments are off, since that only reads Circle.
- Nothing changes in `/api/v1`.

## 2026-10-05: Checked addresses, a stop switch, and no rejection over an unknown transfer

- `POST /api/v1/counterparties` refuses an `address` that mixes capital and small letters when they do not match its EIP-55 checksum: `400 invalid_request`, with the message `address: This address's capital letters do not match its checksum, so a character is likely wrong.` An address written in one case alone is taken as written, as before.
- **Payments can be switched off for every workspace** by whoever runs the deployment (`PAYMENTS_DISABLED`). While they are, no agent cycle runs, so the agent appends no entries and webhooks receive none from it.
- When a payment's send ends without Circle saying what became of it, the invoice can be neither rejected nor returned until a person approves it, which asks Circle again under the same key. No ledger entry changes shape.
- On a deployment with no screening service, a live workspace's counterparties stay `unscreened` instead of being screened against the demo list. Its `compliance_sweep` entries say the screening is incomplete.
- Otherwise nothing changes in `/api/v1`.

## 2026-10-05: Two people before the first payment to an address

- **A new guardrail rule**, `counterparty.new_payee`. Wherever payments are real, the agent never makes a payment or milestone release that would be the first to an address unless two different parties stand behind it:
  - the payee sent the address and a member confirmed it; or
  - one member gave it and another confirmed it.

  Its entry carries `guardrailBlocked: true` and `execution.resultingStatus: "held"`.
- A decision on a first payment records `observed.newPayee: { addressBy, confirmedBy, twoParties }`. `addressBy` is a member id, `"payee"`, or null when no entry says who gave it.
- `approval_paid` and `milestone_approval_paid` gain `firstPayment: true` when a person made an address's first payment. Approve and pay, and Pay now, now refuse the member who gave the address, unless they are the workspace's only approver.
- An `invoice_reopened` entry's `followUp.changes` can name `the payee's address has received a confirmed payment since` or `a second person now stands behind the payee's address`.
- Nothing changes in `/api/v1`.

## 2026-10-05: A EURC payable decided again when the rate changes what held it

- An `invoice_reopened` entry gains `reevaluation` when a fresh quote cleared what held a EURC payable. Its shape is `{ trigger, previousDecision: { seq, action, guardrailRule }, before: { rate, usdcValue, swapCostPercent }, after: { rate, usdcValue, swapCostPercent, quotedAt } }`.
  - `trigger` is one of `rate_available`, `swap_available`, `swap_cost_within_cap` or `value_within_limit`.
  - `followUp.changes` says the same in words.
- The `ap_*` decision that follows such a reopen in the same cycle gains `reevaluation: { reopenedSeq, trigger, previousDecisionSeq, previousAction }`. Its own `fx` is the rate it was decided at.
- A `cycle_complete` entry's `events` can include `fx_changed`. The FX watch started that cycle, every 5 minutes at most, not a person, so its `by` is absent.
- Nothing changes in `/api/v1`.

## 2026-10-05: The three-way match, checked in code

- **A new guardrail rule**, `invoice.match_incomplete`. When the agent chooses to pay or schedule a payable whose goods are not confirmed received, or whose counterparty needs a purchase order and has none on file, code refuses it. Its `ap_pay` or `ap_schedule` entry carries `guardrailBlocked: true`, `guardrailRule: "invoice.match_incomplete"` and `execution.resultingStatus: "awaiting_info"`, and the invoice waits for its details.
- An AP decision's `detail.observed` gains `purchaseOrderRequired`: whether the counterparty needed a purchase order when it was decided. Every counterparty needs one unless an owner or admin marked it as paid without them.
- **A new ledger action**, delivered to webhooks as `ledger.appended` like every entry: `counterparty_purchase_orders_changed`, actor `human`, domain `compliance`. Its `detail` is `{ by, counterpartyId, from, to }`, where `from` and `to` say whether the counterparty needed a purchase order before and after.
- A `cycle_complete` entry's `events` can include `purchase_orders_waived`: a counterparty was marked as paid without purchase orders, so its payables that waited for one were decided again within a minute.
- The `invoice_reopened` entry for such a payable names `the counterparty is now paid without purchase orders` in `followUp.changes`.
- Nothing changes in `/api/v1`.

## 2026-10-04: A merge starts the cycle

- A `cycle_complete` entry's `events` can now include `pull_request_merged`. A pull request was merged in a repository the workspace's GitHub connection covers, and a pending milestone waited on it, so the cycle that verifies and pays it started within a minute of the merge rather than at the next scheduled cycle.
- Nothing changes in `/api/v1`.

## 2026-10-04: Bounties from a pull request comment

- **A new ledger action**, delivered to webhooks as `ledger.appended` like every entry: `github_bounty_attached`, actor `human`, domain `contractor`. Someone who can write to a repository attached a bounty to a pull request with `/bounty`. Its `detail` is `{ by, via: "github", installationId, login, pullRequest, author, amount, milestoneId, counterpartyId, commentUrl }`. `by` is the member who connected GitHub, under whose name the milestone was added, and `login` is the GitHub user who commented. See [Show payments on GitHub](https://www.vestiarion.xyz/docs/guides/github).
- `create_counterparty` and `create_milestone` entries gain `via: "github"`, `installationId` and `login` for the contractor and the milestone a bounty added.
- `counterparty_address_changed` entries gain `via: "github"`, `installationId`, `login` and `commentUrl` when a pull request's author set their address with `/payto`. Their `by` is `null`, as for an address sent through a payee link, and the address waits for a member's confirmation the same way.
- Nothing changes in `/api/v1`.

## 2026-10-04: Emailed invoices finished by hand

- A `create_invoice` entry with `via: "email"` no longer always carries `changed: []`. An owner or admin can now finish an emailed invoice in the invoice form on AP / AR, with **Edit and add** or **Finish and add**. Its `document.changed` then lists the fields they changed from what was read, as for an invoice added from a document.
- Such an entry carries no `document` when the email could not be read and the invoice was typed in.
- Nothing changes in `/api/v1`.

## 2026-10-04: Cash back for milestones too

- A `cash_brought_back` entry with `reason: "payments_due_today"` now also counts the verified milestones waiting to be paid in `neededUsdc` and `payments`, so a cycle brings their cash back from the reserve before it releases them. A milestone whose USDC is locked in escrow is not counted.
- A person's **Pay now** on a milestone for a pull request now gets its `pull_request_commented` entry once the payment is confirmed, rather than at the next cycle.

## 2026-10-04: Payments said on the pull request

- **A workspace can connect GitHub** from Settings, by installing the Vestiarion app on the repositories it chooses. See [Show payments on GitHub](https://www.vestiarion.xyz/docs/guides/github).
- **Three new ledger actions**, delivered to webhooks as `ledger.appended` like every entry:
  - `github_connected`, with `installationId`, `account`, `accountType` and `repositorySelection`;
  - `github_disconnected`, with `installationId` and `account`;
  - `pull_request_commented`, with `milestoneId`, `pullRequest`, `commentUrl`, `amount`, `token` and `txHash`. It is written when the app commented on the pull request a milestone was paid for. The comment names the amount, the network, the paying workspace and the transaction, never the payee.
- **Private pull requests verify** in a repository the app is installed on, so `verify_milestone_github` can now follow a private pull request's merge.
- Nothing changes in `/api/v1`.

## 2026-10-03: Invoices by email

- **New ledger actions**, delivered to webhooks like every entry:
  - `invoice_inbox_on`, `invoice_inbox_changed` and `invoice_inbox_off`, actor `human`, domain `system`: an owner or admin turned on the workspace's address for invoices by email, replaced it, or turned it off. Their `detail` is `{ by }`; the address is never recorded.
  - `invoice_email_received`, actor `system`, domain `ap`: an email arrived at the address and was read. Its `detail` is `{ inboxEmailId, from, authentication, read, document }`: the sender with most of its name hidden, SPF/DKIM/DMARC as Resend reported them, `read` one of `ready`, `needs_details` or `unreadable`, and the document's `kind` and `sha256`, or `null`.
  - `invoice_email_dismissed`, actor `human`, domain `ap`: `{ by, inboxEmailId }`.
- `create_invoice` entries gain `via: "email"` and `inboxEmailId` when a member added the payable from an email on AP / AR. Such an entry carries `document` as one added from a document does, with `changed: []`. Nothing that arrives by email is added without a person.
- Nothing changes in `/api/v1`.

## 2026-10-03: Add milestones and payee links through the API

- **[Add a milestone](https://www.vestiarion.xyz/docs/api/create-milestone)**, `POST /api/v1/milestones`, with a read-and-write key.
  - It takes `{ contractorId, title, amount, verificationSource? }`, checked by the rules of the console's Add milestone form, and answers `201` with the milestone, shaped as [List milestones](https://www.vestiarion.xyz/docs/api/list-milestones) returns it.
  - The milestone starts `pending`, and the API cannot verify it. A GitHub pull request in `verificationSource` is verified by the agent once it is merged; any other link waits for a person.
  - It takes an `Idempotency-Key`, as every write does.
  - It is recorded as `create_milestone` with `via: "api"` and `apiKeyId`.
- **[Create a payee link](https://www.vestiarion.xyz/docs/api/create-payee-link)**, `POST /api/v1/payee-links`, with a read-and-write key.
  - It takes `{ counterpartyId }`, a vendor or a contractor, and answers `201` with `{ id, counterpartyId, url, expiresAt }`.
  - `url` is in that answer only, which is sent with `Cache-Control: no-store`.
  - The operation keeps no outcome for an `Idempotency-Key`, which would store the link. A repeat makes a new link, and the unused one stops working.
  - It is recorded as `payee_link_created` with `via: "api"` and `apiKeyId`. The address a payee enters through it waits for a person to confirm it, as before.
- **A write now checks the key's creator as they are now.** Only owners and admins can add records. If an owner moves the creator to approver or viewer, the key keeps reading, and every write, invoices and counterparties included, answers `403 forbidden`: "This key's issuer can no longer add records in this workspace."
- **A milestone records who added it**, from the console, Pay a freelancer or the API. So the self-approval rule now applies to milestones too. When the agent held a milestone on its own judgment, the person who added it cannot choose **Pay now** for it, unless they are the workspace's only approver. Milestones added before keep no creator.
- **MCP** gains `create_milestone` and `create_payee_link`. `create_payee_link` is marked destructive, because a new link revokes the unused one.
- **`@vestiarion/sdk` 0.2.0** adds `milestones.create` and `payeeLinks.create`.
- **Settings:** the box that gives a key write access is now **Can also add records**.

## 2026-10-03: Add an invoice from Slack

- `create_invoice` entries gain `via: "slack"` and `linkId` when an owner or admin added the payable from a message in Slack with **Add invoice**. Such an entry carries `document` as one added from a document on AP / AR does, with `changed: []`.
- The Slack app asks for one more permission, `files:read`, to read the file someone chooses. A Slack connected before this is connected again from **Settings** with **Reconnect Slack**; its members stay connected.
- Nothing changes in `/api/v1`.

## 2026-10-03: A member's API keys end with their membership

- A workspace API key now works only while the person who created it is a member of the workspace. When they leave, are removed by an owner or an admin, or delete their account, every active key they created in that workspace, read-only or read-and-write, is revoked in the same step, and from then on answers `401 unauthorized` on `/api/v1` and on MCP, like any revoked key. Keys that other members created, and the person's keys in workspaces they still belong to, keep working.
- Each such key gets its own `api_key_revoked` entry, delivered to webhooks like every entry, right after the member's `member_left` or `member_removed`. Its `detail` gains `reason`: `member_left`, `member_removed` (with `member`, the person removed, while `by` is who removed them) or `account_deleted`. A key revoked in Settings now carries `reason: "person"`.
- A key can no longer be created for someone who is not a member of the workspace.
- A read-and-write key whose issuer has left can no longer add records, so a `create_counterparty` or `create_invoice` entry with `via: "api"` always names its issuer in `by`.
- Nothing was revoked when this shipped: no active key had outlived its creator's membership.

## 2026-10-03: The TypeScript SDK on npm

- `npm install @vestiarion/sdk` installs it from the npm registry.
- Version 0.1.0 there is the same tarball, byte for byte, as `https://www.vestiarion.xyz/sdk/vestiarion-sdk-0.1.0.tgz`, which keeps working.

## 2026-10-03: A member's notifications move to Settings

- `telegram_disconnected` entries for a chat its member disconnected from Vestiarion now carry `via: "settings"`: the **Telegram** card moved from **Members** to the **Notifications** section at the top of **Settings**, with the switch for the email about payments that need a decision. Entries made before keep `via: "members_page"`; `via: "telegram"` and `via: "blocked"` are unchanged.
- Nothing else changes in the ledger, the webhooks or `/api/v1`.

## 2026-10-03: A TypeScript SDK

- **`@vestiarion/sdk` 0.1.0**, installed with `npm install https://www.vestiarion.xyz/sdk/vestiarion-sdk-0.1.0.tgz`. It is a typed client for every `/api/v1` operation, with no dependencies, for Node 20 or later, Deno, Bun and edge runtimes. See [TypeScript SDK](https://www.vestiarion.xyz/docs/get-started/sdk).
- Its types are generated from the OpenAPI document, so they follow each change listed here.
- `listAll` and `pages` follow `page.nextCursor` to the end of a collection.
- Requests are retried after:
  - a `429`, when its `Retry-After` is 60 seconds or less;
  - a `5xx`;
  - a timeout or a network failure.
- Every write carries an `Idempotency-Key`, so a retry never adds a record twice.
- `verifyWebhook` checks a delivery's `Vestiarion-Signature`.
- `verifyLedgerEntry` checks an entry's body hash, Ed25519 signature and chain hash.
- Nothing in the API or the webhooks changed.

## 2026-10-03: The agent's decisions in Slack

- **New ledger actions**, domain `system`, delivered to webhooks like every entry:
  - `slack_installed`, actor `human`: an owner or admin connected a Slack workspace from **Settings**, and picked the channel the agent's decisions go to. Its `detail` is `{ by, teamId, channelId }`.
  - `slack_uninstalled`: the workspace's Slack was disconnected, with every member's Slack link. Actor `human` when an owner or admin chose **Remove Slack** (`via: "settings"`); actor `system` when Slack said the app was removed from its workspace (`via: "slack"`), with `by` `null`. Its `detail` is `{ by, teamId, via }`.
  - `slack_member_connected`, actor `human`: a member connected their own Slack account. Its `detail` is `{ by, linkId }`, plus `via: "install"` for the account of the person who connected the workspace.
  - `slack_member_disconnected`, actor `human`: a member disconnected their own Slack account, with `/vestiarion disconnect` (`via: "slack"`) or from **Settings** (`via: "settings"`). Its `detail` is `{ by, userId, linkId, via }`. A member removed from the workspace loses the link with the membership, and that is recorded as the removal.
  - `slack_decisions_limit_changed`, actor `human`: an owner set the most a payment approved from Slack may be, or turned deciding from Slack off. Its `detail` is `{ by, from, to }`, in USDC, `null` for off.
- `approval_paid`, `approval_rejected` and `approval_returned` entries gain `via: "slack"` and `linkId` when a member decided the payable from its message in Slack, and `agent_paused` does when they paused the agent with `/vestiarion pause`. Everything else in them is what the console writes. Entries for actions taken in the console carry no `via`, as before.
- A cycle's journal has a new stage, `slack`, after `telegram`, the last. It needs no other stage to have succeeded.
- Nothing changes in `/api/v1`.

## 2026-10-03: A payable held for an unconfirmed address goes back to the agent once it is confirmed

- A payable the agent held because its counterparty's address changed and no one had confirmed it (`guardrailRule: "counterparty.address_unconfirmed"`, `observed.addressUnconfirmed: true`) now goes back to the agent when a person confirms the address: an `invoice_reopened` entry whose `followUp.changes` reads "the counterparty's new address has since been confirmed", then a new decision in the same cycle. Until now it stayed held until a person approved it. Approving it still pays it, and confirms the address.
- The API's two write examples are now responses captured in testnet-2.

## 2026-10-03: Add counterparties and invoices through the API

- **New operations:** [`POST /api/v1/counterparties`](https://www.vestiarion.xyz/docs/api/create-counterparty) and [`POST /api/v1/invoices`](https://www.vestiarion.xyz/docs/api/create-invoice). Each takes a JSON body of at most 64 KB, checked by the console form's rules, and answers `201` with the record, shaped as its list returns it. A body that does not validate, including one with a field the operation does not take, answers `400 invalid_request`, naming the field. A `counterpartyId` the workspace does not hold answers `400` too.
- **A new scope, `write`.** A key is read-only, as every existing key stays, or reads and writes; an owner or admin chooses when creating it. A read-only key on a write answers `403 forbidden`. `api_key_created` lists `scopes: ["read", "write"]` for such a key.
- **`Idempotency-Key`** on both writes. A repeat with the same key and body within 24 hours gets the first answer back, with `Idempotent-Replayed: true`. The same key with a different body, or while the first request is still being handled, answers the new error code **`conflict` (`409`)**. A `5xx` answer, and a body that fails validation, are not kept, and a key whose first request never finished is free again after 10 minutes.
- Writes are limited to 30 a minute per key; over that, `429 rate_limited` with `Retry-After: 60`. Reads stay unlimited.
- `create_counterparty` and `create_invoice` entries for records added through the API carry `via: "api"` and `apiKeyId`, with `by` the key's issuer, or `null` once their account is gone. Such a `create_counterparty` also carries `addressNeedsConfirmation`: an address added through the API waits for a person to confirm it, and the agent holds payments to it (`counterparty.address_unconfirmed`) until then.
- A payable added through the API starts a cycle, whose `cycle_complete` entry lists `invoice_added` in `events`, as one added in the console does.
- **MCP:** two new tools, `create_counterparty` and `create_invoice`, annotated `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, with an optional `idempotencyKey`. A read-only key gets the `403` as a tool error.
- The OpenAPI document describes each write's request body, its `Idempotency-Key` header and its `201`.

## 2026-10-03: A sweep's yield is counted over how long the cash would stay

- In treasury entries (`sweep_to_usyc`, `redeem_from_usyc`, `hold`, actor `agent`), `economics.expectedHoldDays` is now how long the swept cash would stay in the reserve, on average over the next 30 days. What falls due is paid from the operating wallet first, and only what it cannot cover comes back, on its day. Before, it was the days until the next obligation. `economics.projectedYieldUsd` follows, and the written policy's `referenceDecision` with it.

## 2026-10-03: Code bounds the treasury's moves, and a payable to a client waits for a person

- A new value of `guardrailRule`: `counterparty.client_payable`. The agent holds a payment, or a schedule, to a counterparty whose role is `client`, for a person: a client pays the business, so a payable to one is a refund, or an invoice entered in the wrong direction. A person may still approve it.
- Treasury entries (`sweep_to_usyc`, `redeem_from_usyc`, `hold`, actor `agent`, domain `treasury`):
  - `decision` is now the move code allowed. A sweep takes at most the cash above the 7-day buffer. A redemption brings back at most 115% of what falls due within 14 days, less the operating balance, and at least the written policy's.
  - When code changed the model's move, `boundedByCode: { chosen: { action, amount }, reason }` keeps what the model chose and why, and `decision.reasoning` ends with "[Code limited this: …]".
  - `agreedWithReference` now also needs the amount within 5% of `referenceDecision.amount`, or 0.01 USDC.
- `ar_reminders_on` gains `replacedLink`: true only when turning reminders on replaced a link made before October 3, which stops working. `madeNewLink` is also true when the receivable had no link.

## 2026-10-03: The agent reminds clients of what they owe

- **New ledger actions**, domain `ar`, delivered to webhooks like every entry:
  - `ar_reminders_on` and `ar_reminders_off`, actor `human`: an owner or admin turned the agent's reminders on or off for a receivable. `detail` is `{ by, invoiceId, counterpartyId }`, and for `ar_reminders_on` also `linkId` and `madeNewLink` (a link made before October 3 was replaced so the reminders can carry it).
  - `ar_reminder_sent`, actor `agent`: the agent emailed the client a reminder. `detail` is `{ invoiceId, counterpartyId, to, number, tone, daysFromDue, amount, currency, linkId, decision, referenceDecision, decisionMode, agreedWithReference }`. `to` is the address with most of its name hidden; `number` is 1 to 4; `tone` is `friendly`, `firm` or `final`; `daysFromDue` is negative before the due date. `toneLimited: { chosen, sent }` is there when code sent a softer tone than the model chose.
  - `ar_reminder_deferred`, actor `agent`: the agent decided to wait before reminding. `detail` is `{ invoiceId, counterpartyId, until, daysFromDue, remindersSent, decision, referenceDecision, decisionMode, agreedWithReference }`.
- Turning reminders on starts a cycle; its `cycle_complete` entry lists the new event kind `reminders_on` in `events`.
- A cycle's journal has a new stage, `collections`, between `proposals` and `notices`. It runs only when `receipts` completed.
- `counterparty_notice_email_changed` now covers a counterparty's billing email, which also receives a client's reminders; its `summary` reads "Billing email for *name*: *address*", or "... removed".

## 2026-10-03: The agent's decisions in Telegram

- **New ledger actions**, domain `system`, delivered to webhooks like every entry:
  - `telegram_connected`, actor `human`: a member connected their own Telegram chat to the workspace, from **Members**. Its `detail` is `{ by, username }`, where `username` is the Telegram username with most of it hidden (`@li***`), or `null`.
  - `telegram_disconnected`: a chat no longer gets the workspace's decisions. Actor `human` when the member disconnected it, from **Members** (`via: "members_page"`) or with `/disconnect` in the chat (`via: "telegram"`); actor `system` when Telegram answered that the member had blocked the bot (`via: "blocked"`). Its `detail` is `{ by, userId, via, username }`, with `by` `null` for `blocked`. A member removed from the workspace loses the connection with the membership, and that is recorded as the removal.
- `create_invoice` entries gain `via: "telegram"` when a member added the payable by tapping **Add** on an invoice the bot read. Such an entry carries `document` as one added from a document on AP / AR does, with `changed: []`.
- A cycle's journal has a new stage, `telegram`, after `notices`, the last. It needs no other stage to have succeeded.
- Nothing changes in `/api/v1`.

## 2026-10-03: Cash comes back from the reserve for payments

- **New ledger action** `cash_brought_back`, domain `treasury`, delivered to webhooks like every entry: USDC came back from the reserve to the operating wallet.
  - Actor `agent`: each cycle, before it decides payments, the agent brings back what the USDC payables due today need beyond the operating balance. Its `detail` is `{ reason: "payments_due_today", amount, neededUsdc, payments, operatingBalance, reserveBalance, executed, executionNote, earnMode, execution? }`. `execution` holds the transactions of a real USYC redemption. When nothing could move, `executed` is `false`, `executionNote` says why, and the entry's `summary` begins "Could not bring"; while the agent is paused, `heldBecause` is `agent_paused`.
  - Actor `human`: an owner or admin chose **Bring cash back**. Its `detail` is `{ by, reason: "person", amount, all, reserveBalance, earnMode, execution? }`, where `all` says they brought back everything. It moves cash even while the agent is paused, and starts a cycle, whose `cycle_complete` entry lists the new event kind `cash_returned` in `events`.
- The redemption itself is also a `treasury_actions` row with `action: "redeem_from_usyc"`, as the treasury stage's are.
- An AP decision held because the cash was not there carries `execution.heldBecause: "cash_shortfall"`, with `execution.cashNeededUsdc` (what it needed, with the payables due before it) and `execution.cashSeen: { operating, reserve }` (the balances it saw). Once cash has come in since and covers it, the next cycle reopens the payable with an `invoice_reopened` entry whose `followUp.changes` reads "the cash it needs is there now (… USDC in the operating wallet and the reserve)", and decides it again.
- A cycle's journal has a new stage, `liquidity`, between `services` and `ap`.

## 2026-10-03: Payment notices

- **New ledger actions**, delivered to webhooks like every entry:
  - `payment_notice_sent`, actor `system`, domain `ap` or `contractor`: a payee was emailed that a payment to it was confirmed. Its `detail` is `{ invoiceId or milestoneId, counterpartyId, to, amount, token, txHash }`, where `to` is the address with most of its name hidden (`li***@example.com`).
  - `counterparty_notice_email_changed`, actor `human`, domain `compliance`: a person set, changed or cleared where a counterparty's payment notices go. Its `detail` is `{ by, counterpartyId, from, to }`, both addresses hidden the same way, `null` for none.
- `create_counterparty` entries gain `noticeEmail`, hidden the same way, or `null`.
- The address itself is not part of `/api/v1/counterparties`.

## 2026-10-03: Add what a held payable was missing

- **New ledger action** `invoice_details_added`, actor `human`, domain `ap`, delivered to webhooks like every entry: a person added the purchase order a held, flagged or awaiting-information payable lacked, or marked its goods or services received. Its `detail` is `{ by, invoiceId, counterpartyId, added }`, where `added` holds `poReference`, `goodsReceived: true`, or both. Nothing already on the invoice changes, and its `status` stays as it was.
- At its next cycle the agent reopens the payable with an `invoice_reopened` entry whose `followUp.changes` names what was added, and decides it again. Adding details starts that cycle within seconds, and its `cycle_complete` entry lists the new event kind `details_added` in `events`.
- In `/api/v1/invoices`, the payable's `poReference` and `goodsReceived` show what was added.

## 2026-10-03: A sandbox screens against the bundled watchlist

- `GET /api/v1/status` reports `screening: "simulate"` for a sandbox workspace, whatever the deployment configures; a live workspace reports `live` when OpenSanctions is configured, as before.
- A sandbox's counterparties are screened against the bundled watchlist at every cycle, and their `compliance_sweep` entries name that source. One screened by OpenSanctions before is screened again against the watchlist at its next cycle.

## 2026-10-03: Nothing is paid to a counterparty never screened

- A new value of `guardrailRule`: `counterparty.unscreened`. The agent holds a payment, a schedule or a milestone release to a counterparty whose `riskLevel` is still `unscreened`, because screening could not run when it was added. The decision is made again once screening gives a verdict. A counterparty whose later screening fails keeps its earlier verdict, as before.

## 2026-10-03: The spending limit, enforced on Arc

- **New ledger actions**, actor `human`, domain `system`, delivered to webhooks like every entry:
  - `spending_limit_enforced`: a person enforced the agent's spending limit on Arc testnet. Its `detail` is `{ by, contract, agent, treasury, dailyUsdc, weeklyUsdc, deployTxHash, approveTxHash, setLimitsTxHash }`, where `agent` is the agent's own wallet and `treasury` the operating wallet.
  - `spending_limit_unenforced`: a person turned it off. Its `detail` is `{ by, contract, txHash }`.
- `agent_budget_changed` gains `onChain: { contract, txHash }` when the figures were changed on the contract too.
- While the limit is enforced, the agent's payment decisions (`ap_*` and `milestone_*` entries, actor `agent`) carry `onChainLimit: { contract, agent, ref, covered, uncoveredBecause?, verdict }`. `covered` says whether the payment can go through the contract; `verdict` is the contract's answer before anything was sent: `{ state: "allowed" }`, `{ state: "refused", error, spent?, amount?, limit? }`, `{ state: "unreadable", reason }`, or `null` when it was not asked.
- Two new values of `guardrailRule`: `workspace.onchain_limit` (the contract would refuse the payment) and `workspace.onchain_limit_route` (the payment is in EURC, or to another chain, which the contract cannot carry).
- A payment sent through the contract has, as its transaction, a call to the contract's `pay(address,uint256,bytes32)` by the agent's wallet. Its logs hold the USDC transfer from the operating wallet to the payee.

## 2026-10-03: A workspace's only approver may approve what they entered

- When the person who entered a payable, or added a milestone, is the only member of the workspace who may approve payments (the only owner, admin or approver), they may now approve it themselves. The `approval_paid` or `milestone_approval_paid` entry then carries `soleApprover: true` in its `detail`, and its `summary` ends "(entered and approved by the workspace's only approver)". An approval by anyone else carries neither. Once a second member may approve, the person who entered a record cannot approve it again.

## 2026-10-02: The agent's reasoning is written in plain English

- New decisions' `agentReasoning` in `/api/v1/invoices` and `/api/v1/milestones`, and `detail.decision.reasoning` in ledger entries, are written as 2 to 4 plain-English sentences, without the field names of the facts the agent was shown. Nothing about earlier records changes: the API and the ledger return what was stored.

## 2026-10-02: One review dismisses every screening match

- A `screening_match_dismissed` entry can now dismiss several matches at once: the ones the person reviewed together. Its `detail` gains `matchedEntities: [{ id, caption, score }]`, every match it dismissed. `matchedEntityId`, `matchedCaption` and `matchedScore` still name the counterparty's verdict match, the first in the list.
- Screening now reads every candidate it needs. Previously, after five dismissals of one name, a re-screen could read `clear` while further matches were never looked at. Such a counterparty may now be screened `medium` or `high` again, with a `risk_level_changed` entry.

## 2026-10-02: A person decides a held milestone

- **Milestones can be `closed`.** In `/api/v1/milestones`, `status` may be `closed`: a person closed the milestone without paying it, and it is never paid after. `?status=closed` lists them. Every milestone has two new fields, `null` unless it is closed:
  - `closedAt`: when it was closed;
  - `closeReason`: the reason the person gave.
- **New ledger actions**, delivered to webhooks like every entry:
  - `milestone_approval_paid` (actor `human`, domain `contractor`): a person paid a held milestone. Its `detail` is `{ by, milestoneId, counterpartyId, amount, currency, overrode, heldFor, txRef, status, attempt?, retriedAfter? }`.
    - `status` is `paid`, `verified` (submitted, not yet confirmed) or `held` (the transfer failed).
    - `heldFor` says what it was held for: for example `transfer_failed`, `agent_held` or `outflow_budget`.
    - `retriedAfter` is present when Circle had failed the previous attempt, which this one sent again.
  - `milestone_closed` (actor `human`, domain `contractor`): a person closed a held milestone without paying it. Its `detail` is `{ by, milestoneId, counterpartyId, amount, currency, heldFor, reason }`.

## 2026-10-02: The agent buys payee history over x402

- **New public endpoint:** `GET /api/x402/payee-history?address=0x…`. It is paid per call over x402: 0.001 USDC on Arc testnet (`eip155:5042002`), scheme `exact`, settled through Circle Gateway batching (`GatewayWalletBatched`, GatewayWallet `0x0077777d7EBA4688BDeF3E311b846F25870A19B9`).
  - Without a `PAYMENT-SIGNATURE` header it answers `402` with the offer in `PAYMENT-REQUIRED`.
  - With a payment Circle's facilitator verifies and settles, it answers `200` with `{ address, workspacesPaid, paymentsConfirmed, firstPaidAt, lastPaidAt, asOf, seller, priceUsdc }` and the settlement in `PAYMENT-RESPONSE`.
  - A missing or malformed address answers `400` before any offer. When the history cannot be read, it answers `503` and nothing is charged.
- **New ledger actions:**
  - `service_purchased` (actor `agent`, domain `compliance`). Its `detail` is `{ purchaseId, counterpartyId, address, seller, priceUsdc, payer, payTo, nonce, settlement, result }`.
  - `service_purchase_refused` (actor `agent`, domain `compliance`). Its `detail` is `{ purchaseId, counterpartyId, address, seller, rule, reason }`. `rule` is one of `seller.not_allowed`, `offer.mismatch`, `price.above_max`, `budget.day` or `purse.short`.
  - `service_purchase_failed` (actor `agent`, domain `compliance`). Its `detail` is `{ purchaseId, counterpartyId, address, seller, reason }`.
  - `service_budget_funded` (actor `human`, domain `treasury`). Its `detail` is `{ by, amountUsdc, signer, depositor, txHash, balanceUsdc }`.
- AP decision entries and `milestone_release` / `milestone_hold` entries carry `observed.addressHistory` when the agent bought the counterparty's address history within 7 days: `{ about, workspacesPaid, paymentsConfirmed, firstPaidAt, lastPaidAt, boughtAt, priceUsdc }`.
- A cycle's `detail.stages` includes `services`, which runs after `recurring` and before `ap`.

## 2026-10-02: Batches through the operating wallet's own executeBatch

- A batch of milestones is now one call to the operating wallet's own `executeBatch`, as Circle documents for its smart accounts. The wallet sends USDC to each payee in turn, and if one transfer fails, none happens.
- The first version sent batches through Arc's Multicall3From. Multicall3From keeps the sender only when the sender signed the transaction itself, and a Circle smart account's transaction is sent by Circle's bundler. Circle refused that batch at estimation (`ESTIMATION_ERROR`), and no money moved. Its milestones were held, and their `milestone_release` entries carry `execution.batch` with `resultingStatus: "held"`.
- For a short time in between, workspaces with live wallets paid each milestone alone.

## 2026-10-02: Several milestones in one transaction

- When a cycle releases two or more milestones paid in USDC on Arc testnet, they go out in one transaction. Each is still decided, checked and signed on its own. A milestone locked in escrow is always released on its own.
- A `milestone_release` entry for a milestone paid this way carries `execution.batch`: `{ key, size }`. `key` identifies the batch, and `size` is how many milestones it paid.
  - `execution.txRef` is the batch's transaction, shared with the other milestones in it.
  - `execution.feeUsd` is this milestone's share of the transaction's fee.
- In `/api/v1/milestones`, several milestones can have the same `tx_ref`. A transaction hash no longer identifies one payment: key on the milestone's `id`.

## 2026-10-02: The agent proposes limits from people's approvals

- When people approve at least two payments to one counterparty above its payment limit within 30 days, and reject none, the agent may propose raising the limit. The counterparty must be screened clear. An owner or admin accepts or dismisses the proposal in Approvals.
- **New ledger actions**, in the `compliance` domain:
  - `policy_proposal_made` (actor `agent`). Its `detail` is `{ proposalId, counterpartyId, kind, fromLimit, toLimit, evidence, reasoning, decisionMode, referenceLimit, agreedWithReference }`. `kind` is `raise_limit`. `evidence` lists the approvals it rests on: `{ invoiceId, amountUsdc, approvedAt, approvedBy, agentAction, guardrailRule }`.
  - `policy_proposal_declined` (actor `agent`): the model decided not to propose. Its `detail` is `{ counterpartyId, currentLimit, evidence, reasoning, decisionMode, agreedWithReference }`.
  - `policy_proposal_accepted` (actor `human`). Its `detail` is `{ by, proposalId, counterpartyId, fromLimit, toLimit, currentLimit }`. A `counterparty_limit_changed` entry precedes it, as for any limit change.
  - `policy_proposal_dismissed` (actor `human`). Its `detail` is `{ by, proposalId, counterpartyId, toLimit }`.
- A cycle's `detail.stages` includes `proposals`, which runs last, after `forecast`.

## 2026-10-02: Recurring payments

- An owner or admin can set up a payment that repeats every N days, weeks or months. Each period's invoice is created as it comes near and decided like any other.
- **New ledger actions**, in the `ap` domain:
  - `recurring_payable_created` (actor `human`). Its `detail` is `{ by, recurringId, counterpartyId, amount, currency, memo, poReference, goodsReceived, everyCount, everyUnit, startsOn, endsOn }`.
  - `recurring_invoice_created` (actor `agent`), when a cycle creates a period's invoice. Its `detail` is `{ invoiceId, recurringId, period, counterpartyId, amount, currency, goodsReceived }`. `period` is the invoice's due date, `YYYY-MM-DD`.
  - `recurring_payable_stopped` (actor `human`). Its `detail` is `{ by, recurringId }`.
- A cycle's `detail.stages` includes `recurring`, which runs after `follow_up` and before `ap`.
- An event cycle's `detail.events` can include **`recurring_added`**: someone set up a recurring payment.
- Two invoices created for different periods of one recurring payment are never reported as duplicates of each other. Their decision entries' `observed.duplicateCheck` reflects that.

## 2026-10-02: A real USYC reserve

- A live workspace's owner or admin can turn on a real USYC reserve on Arc testnet, once Circle has allowlisted its operating and reserve wallets. Until then, the reserve stays simulated, as before.
- **New ledger action `usyc_reserve_enabled`**, in the `treasury` domain, appended when the reserve is turned on. Its `detail` is `{ by, operatingAddress, reserveAddress }`.
- **Treasury decision entries** (`sweep_to_usyc`, `redeem_from_usyc`, `hold`) of a workspace with a real reserve:
  - carry `earnMode: "live"`;
  - carry `usycSubscriptionsOpen`: whether USYC could be bought then, or `null` when that could not be read;
  - when a move was made, carry `execution`. A sweep's is `{ approveTxHash, depositTxHash, shares, price }`; a redemption's is `{ redeemTxHash, shares, price }`. `shares` is in USYC, `price` in USDC per USYC.
- A sweep decided while USYC cannot be bought has `executed: false`, and an `executionNote` saying so.
- In `GET /api/v1/status`, `data.provenance.yield` reads `live` for such a workspace.
- The reserve account's `balance` is its USYC's value in USDC at the latest price, read from Arc testnet every cycle. Its `apy` is the fund's yield over at least the last 5 days, from the same oracle.

## 2026-10-02: The agent's spending limit

- An owner or admin can set what the agent may pay on its own per UTC day and per 7 days, in USDC. A payment past either is held for a person.
- **New value of `guardrailRule`, `workspace.outflow_budget`**, on an `ap_pay` or `milestone_release` entry: paying it would have taken the agent past its limit.
  - The entry's `execution.heldBecause` is `outflow_budget`.
  - A payable goes to Approvals as `held`. A milestone stays `held` until the limit has room for it.
- **New field `outflowBudget`** on an `ap_pay` or `milestone_release` entry, present when a limit is set: `{ dailyUsdc, weeklyUsdc, spentToday, spentThisWeek, remaining, binding }`.
  - It is what the limit left when the payment was weighed.
  - `binding` is `day` or `week`, whichever figure left less.
- **Milestone decision entries now carry `guardrailRule`**: `counterparty.high_risk`, `counterparty.payment_limit`, `workspace.outflow_budget`, or `null`.
- **New ledger action `agent_budget_changed`**, in the `system` domain, appended when someone changes the limit.
  - Its `detail` is `{ by, from, to }`. `from` and `to` are each `{ dailyUsdc, weeklyUsdc }`, and `null` means no figure.
- What counts against the limit is what the agent's own `ap_pay` and `milestone_release` decisions sent: entries whose `execution.resultingStatus` is `paid` or `matched`, at their USDC value. A payment a person approves does not count.
- `invoice_reopened` and `milestone_reopened` entries can list the change "the agent's spending limit has room for it again" or "the agent's spending limit was removed".
- An event cycle's `detail.events` can include **`budget_raised`**: someone raised or removed a figure of the limit.

## 2026-10-01: Receivables paid on Arc

- **New ledger action `pay_link_created`**, in the `ar` domain: an owner or admin made a link for a client to pay a receivable. Its `detail` is `{ by, invoiceId, linkId }`.
- **New ledger action `ar_received`**, in the `ar` domain, appended by the agent when a transfer that arrived in the operating wallet settles an open receivable.
  - Its `detail` is `{ invoiceId, counterpartyId, amount, currency, txHash, from, circleTxId, matchedBy, receivedAt }`.
  - `matchedBy` is `sender` when the client's address on file sent it, and `amount` when it was the only open receivable of that currency and amount.
- A receivable's `status` becomes `received`, with `settledAt` and `txRef` set, when a transfer settles it.
- A cycle's `detail.stages` includes `receipts`, which runs after `reconcile`.
- An event cycle's `detail.events` can include **`payment_received`**: a client's "I have paid" check found the receivable paid.

## 2026-10-01: Dismissing a screening match

- **New ledger action `screening_match_dismissed`**, in the `compliance` domain, delivered as a `ledger.appended` event. It is appended when a member who can decide approvals dismisses a counterparty's screening match as not the same person.
  - Its `detail` is `{ by, counterpartyId, matchedEntityId, matchedCaption, matchedScore, screenedName, reason }`.
  - `matchedEntityId` is the screening service's id for the entity dismissed; `screenedName` is the counterparty's name when it was dismissed.
- Screening skips a dismissed entity for that counterparty while its name stays the same, and judges the next match, if any. The counterparty is screened again at once, so a `screen_counterparty` entry with its new verdict follows.
- An event cycle's `detail.events` can include **`match_dismissed`**.

## 2026-10-01: Held milestones decided again

- **New ledger action `milestone_reopened`**, in the `contractor` domain, delivered as a `ledger.appended` event. The agent appends it when it returns a held milestone to `verified` because a fact its decision rested on changed: the contractor's risk level or payment limit, the milestone's verification source, or the agent was paused when it held it.
  - Its `detail` is `{ milestoneId, previousStatus, followUp: { action, reason, changes } }`. `previousStatus` is `held`, `followUp.action` is `reopen`, and `changes` lists each fact that moved, in words.
  - The milestone's next decision entry follows in the same cycle.
- A milestone's `status` can now go from `held` back to `verified` without anyone verifying it again.
- An event cycle's `detail.events` can include **`limit_raised`**: someone raised a counterparty's payment limit and screening allows more than zero.

## 2026-10-01: EURC payments funded by a swap

- A live workspace can pay a EURC payable after swapping USDC for the EURC it needs, through Circle's Stablecoin Service on Arc testnet.
- **New ledger action `fx_swap`**, in the `treasury` domain, delivered as a `ledger.appended` event when a swap ends.
  - Its `detail` is `{ paysInvoiceId, swapId, state, usdcIn, eurcMinimum, eurcEstimated, eurcReceived, usdcPerEurc, costPercent, provider, adapter, approveTxHash, swapTxHash, failure, reasoning, resumed }`.
  - `state` is `confirmed` or `failed`.
  - `eurcReceived` is the EURC the swap's transaction sent to the operating wallet, read from its receipt, or `null` when it could not be read.
  - `resumed` is `true` for a swap whose first answer was lost, finished by a later cycle. Its `reasoning` is then `null`.
  - It carries `paysInvoiceId`, not `invoiceId`: it is not a decision on the invoice.
- **New fields on a EURC payable's decision entry:**
  - `usdcDueWithin7Days`;
  - `swapOffer`: `{ usdcIn, eurcEstimated, eurcMinimum, usdcPerEurc, costPercent, provider }` when a swap was offered, or `null`;
  - `swapUnavailable`: why there was none, or `null`;
  - `swap`: `{ swapId, state, usdcIn, eurcReceived, swapTxHash, reason }` for the swap made to fund the payment, or `null`. `state` is `confirmed`, `pending` or `failed`.
  - `decision.fundWithSwap` is `true` when the model chose to pay with the swap. It appears only on a payable the wallet's EURC was short of.
  - `swapOffer.costPercent` is what each EURC costs through the swap above the rate the payable was weighed at, in percent, rounded up to two places.
- **Two new values of `guardrailRule`:**
  - `fx.swap_cost_above_cap`, for a swap costing more than 3% above the quoted rate;
  - `fx.swap_usdc_short`, for a swap that would leave the USDC below what falls due in USDC within 7 days.
- **When a swap does not go through:**
  - A failed swap holds the payable with `guardrailRule: null`, as a failed transfer does. Its reason is in `swap.reason`.
  - A swap still in flight at Circle leaves the payable `pending`. The next cycle finishes the swap, then decides the payable again.

## 2026-10-01: Milestone escrow

- A live workspace can lock a milestone's USDC in its own escrow contract on Arc testnet. Three new ledger actions, in the `contractor` domain, are delivered as `ledger.appended` events:
  - `escrow_deployed`, once per workspace. Its `detail` is `{ by, address, payer, deployer, circleContractId, txHash }`.
  - `escrow_funded`, when a milestone is locked. Its `detail` is `{ by, milestoneId, contract, holdId, payee, amountUsdc, refundAfter, fundTxHash }`. `fundTxHash` is `null` when the hold was found already on chain and recorded.
  - `escrow_refunded`, when a hold is taken back after its refund date. Its `detail` is `{ by, milestoneId, contract, holdId, amountUsdc, refundTxHash }`.
- A milestone locked in escrow is paid by releasing its hold rather than by a transfer. Its release decision's `execution.txRef` is that release transaction on Arc testnet.

## 2026-10-01: Payment receipts

- An owner or admin can share a paid payable's receipt as a public link. Three new ledger actions, in the `ap` domain, are delivered as `ledger.appended` events:
  - `receipt_shared`, appended once per payment, the first time it is shared. Its `detail` is `{ receipt, records }` and holds no names and no user ids:
    - `receipt` is `{ amount, token, paidAt, payee, chain, txHash, route, sourceTxHash?, feeUsdc? }`;
    - `chain` and `txHash` are where the payee was paid: the transfer on Arc testnet, or the mint on the payee's chain;
    - `route` is `direct`, `cctp` or `gateway`;
    - `sourceTxHash` is a CCTP payout's burn;
    - `records` is `{ seq, hash }` of the entry that recorded the payment.
  - `receipt_link_renewed`, when a new link replaces an earlier one or a revoked receipt is shared again. Its `detail` is `{ by, receiptId }`.
  - `receipt_revoked`, when a receipt's link is turned off. Its `detail` is `{ by, receiptId }`.
  - None of the three carries `invoiceId` in `detail`: a receipt's entries are about sharing a payment, not deciding an invoice.

## 2026-10-01: Payouts from a Gateway balance

- A payout to a payee on another chain can go from the workspace's Circle Gateway balance as well as through CCTP. Its decision entry's `payout.route` is then `gateway`, `payout.feeUsdc` is Gateway's fee, and `payout.gatewayBalanceUsdc` is the Gateway balance it was weighed against.
- A decision entry for a USDC payout to another chain also carries `payout.quotes: { cctpFeeUsdc, gatewayFeeUsdc }`: both routes' fees as read for that decision, each `null` when there was none. `gatewayFeeUsdc` is `null` for a workspace with no Gateway balance.
- An `approval_paid` entry for a new payment to another chain carries the same `payout`, read when the person approved: `{ chain, route, domain, feeUsdc, quotes }`, with `gatewayBalanceUsdc` when the route is `gateway`. An approval that only records a transfer already sent carries none.
- A payout keeps the route its first attempt took. A new value of `guardrailRule`, `bridge.gateway_balance_short`, holds one that went through Gateway when the Gateway balance no longer covers it with its fee.
- For a Gateway payout:
  - the decision entry's `execution.txRef` is the mint on the payee's chain, with `execution.destinationChain` and `execution.mintTxHash`. No Arc transaction belongs to one Gateway payout: Gateway burns on Arc in batches;
  - `GET /api/v1/invoices` returns that mint as the invoice's transaction once it is paid. Until then the invoice is `matched`, as a CCTP payout is.
- Three new ledger actions, in the `treasury` domain, delivered as `ledger.appended` events:
  - `gateway_signer_created`, whose `detail` is `{ by, signer, circleWalletId }`;
  - `gateway_delegate_added`, whose `detail` is `{ by, depositor, signer, txHash }`;
  - `gateway_deposit`, whose `detail` is `{ by, amountUsdc, depositor, signer, approveTxHash, depositTxHash, balanceUsdc }`. `balanceUsdc` is the Gateway balance once it includes the deposit, or `null` when Gateway had not counted the deposit yet.

## 2026-10-01: Invoices read from a document

- A `create_invoice` entry for an invoice read from a document carries `document` in its `detail`:
  - `kind`: `pdf`, `email` (an `.eml` file: its text and the text of its PDF attachments) or `text`;
  - `sha256`: the hash of the document's bytes, or of the pasted text;
  - `reader`: the model that read it (`anthropic`, `openai`, `deepseek`), or `heuristic` for the rule-based reader;
  - `changed`: the form fields the member changed from what was read, among `amount`, `currency`, `dueDate`, `poReference`, `earlyPayDiscountPct`, `discountDeadline`, `memo` and `counterpartyId`.
- An invoice typed in carries no `document`. The document itself is not stored.

## 2026-10-01: First screens in the compliance sweep

- Each entry in a `compliance_sweep` entry's `detail.screened` now carries `firstScreen`: `true` when the counterparty had no verdict before. `changed` is still `true` for a first screen.
- When a sweep includes first screens, its `summary` counts them apart: "Screened 6 of 6 counterparties: 6 for the first time, 0 changed". A sweep of re-screens only still reads "Re-screened 6 of 6 counterparties; 0 changed".

## 2026-10-01: Payees on other chains

- A counterparty's `chain` is one of `ARC-TESTNET`, `BASE-SEPOLIA`, `ARB-SEPOLIA` or `ETH-SEPOLIA`. `GET /api/v1/counterparties` returns it as before.
- A payee on a chain other than Arc testnet is paid from Arc through CCTP V2 with Circle's Forwarding Service.
  - Its decision entry (`ap_pay`, `ap_hold`, `ap_flag_fraud`, `ap_request_info`, `ap_schedule`) carries `payout: { chain, route: "cctp", domain, feeUsdc }` in `detail`, where `chain` is the chain id (`BASE-SEPOLIA`) and `feeUsdc` is `null` when Circle gave no fee.
  - Only a vendor can be paid on another chain; a contractor's milestones are released on Arc testnet.
  - Its `execution` carries `destinationChain` and `mintTxHash` once the payment was sent. `execution.txRef` is the burn on Arc testnet, or `cctp:<Circle transaction id>` while the burn has no hash yet.
  - `mintTxHash` is `null` until the Forwarding Service mints. A later `ap_reconcile` entry carries it.
- Three new values of `guardrailRule`, each of which holds the payable for a person:
  - `bridge.fee_above_cap`, when the fee is above 10% of the amount;
  - `bridge.fee_unavailable`, when Circle gave no fee;
  - `bridge.unsupported_token`, for an invoice in EURC to a payee on another chain.
- A bridged invoice is `matched` from the burn until the mint, then `paid`. `GET /api/v1/invoices` shows it so. One still not minted two hours after it was sent is `held` for a person, by an `ap_reconcile` entry saying so.

## 2026-10-01: Invoices in EURC

- An invoice can be in EURC as well as USDC. `GET /api/v1/invoices` returns its `currency`, which is now `USDC` or `EURC`; `amount` and `paidAmount` are in that currency.
- A EURC payable is paid in EURC from the operating wallet. Its decision entry (`ap_pay`, `ap_hold`, `ap_flag_fraud`, `ap_request_info`, `ap_schedule`) carries `currency` in `detail`, and, for EURC only:
  - `usdcValue`, its value in USDC, which is what counts against the counterparty's limit;
  - `fx: { rate, source: "circle-stablecoin-quote", quotedAt, usdcMinimum }`, or `null` when Circle gave no quote;
  - `eurcBalance`, the operating wallet's EURC as read for the decision, or `null` when payments are simulated or the balance could not be read.

  Those five decision entries carry `currency: "USDC"` for a USDC invoice. `ap_reconcile` carries no `currency`.
- Two new values of `guardrailRule` for a EURC payable, both of which hold it for a person: `fx.rate_unavailable`, when there was no quote to weigh it at, and `treasury.insufficient_eurc`, when the wallet's EURC cannot cover the payment or could not be read. As with every rule, `guardrailRule` is set when code refuses a payment or schedule the model decided on; a payable the model held itself for the same reason has `guardrailRule: null`, with `usdcValue: null` or an `eurcBalance` below `observed.amount` in its `detail`.
- A duplicate is the same amount only in the same currency. A purchase order billed in one currency and again in the other is reported as `purchase_order_rebilled`, which goes to the model and does not block on its own.
- `approval_paid` and the `create_invoice` and `import_invoice` entries gain `currency` in `detail`, and their summaries name it.
- The invoice CSV import takes an optional `currency` column. A blank or missing value means USDC.

## 2026-10-01: Milestones added in the app

- A member can now add a contractor milestone on the Contractors page. It starts `pending` and unverified, like any milestone in `GET /api/v1/milestones`.
- Its `verificationSource` is the evidence link the member gave, or `null`: a GitHub pull request in its canonical form (`https://github.com/<owner>/<repo>/pull/<number>`, which the cycle checks for a merge as before), or any other `https://` address, which only a person verifies.
- A new ledger action, delivered as a `ledger.appended` event: `create_milestone`, in the `contractor` domain with `actor: "human"`. Its `detail` is `{ by, milestoneId, counterpartyId, counterpartyName, amount, verificationSource }`, with `amount` as the decimal string entered.
- A new event kind, `milestone_added`, may appear in an event cycle's `detail.events`. It is raised only for a milestone whose evidence is a pull request, so the merge is checked within a minute.

No `/api/v1` endpoint, parameter or response schema changed.

## 2026-10-01: Payment timing

- An invoice can carry early-payment terms: a percent off and the date it lapses, set on the invoice form or by CSV import. `GET /api/v1/invoices` now returns `earlyPayDiscount: { percent, deadline } | null` for any invoice that carries them.
- A payable the agent has committed to pay later reaches status `scheduled`, decided again each cycle until the day it pays. `GET /api/v1/invoices`'s `status` filter now accepts `scheduled`.
- `GET /api/v1/invoices` also returns `scheduledFor` (an ISO timestamp, or `null`) and `paidAmount`: what actually left, once an invoice's status is `paid`. `paidAmount` stays `null` before then, even while a submitted transfer already carries an amount.
- A new ledger action, delivered as a `ledger.appended` event: `ap_schedule`, in the `ap` domain, recorded when the agent schedules a payable. Its `detail` carries `decision: { action: "schedule", payOn, reasoning, confidence }`, `timing` and `timingRule` (the policy's figures, and any correction made to the model's date), `requestedPayOn` when a date was corrected, `terms: { earlyPayDiscount }`, `observed`, and `execution.resultingStatus: "scheduled"`.
- Every `ap_*` decision entry (`ap_pay`, `ap_hold`, `ap_flag_fraud`, `ap_request_info`, `ap_schedule`) now carries `timing`, `timingRule` and `terms` in its `detail`; an entry for a previously scheduled invoice also carries `scheduledFor`.
- `ap_pay` and `approval_paid` gain `amountPaid` and `discountTaken` in `detail`: both `null` when nothing went out, or the discounted amount and the percent taken when an invoice was paid by its discount deadline.
- `cycle_complete`'s `detail.outcomes` gains `scheduledCount`.
- A duplicate is now also refused against a payable already `scheduled`, already being paid (`matched`), or being decided by a person on Approvals (`processing`), not only one already `paid` or `received`. The refusing entry's reasoning names which of these it repeats.

## 2026-09-30: Payee links

- Two new ledger actions, delivered as `ledger.appended` events, both in the `compliance` domain with `actor: "human"`:
  - `payee_link_created`, whose `detail` is `{ by, counterpartyId, linkId, expiresAt }`;
  - `payee_link_revoked`, whose `detail` is `{ by, counterpartyId, linkId }`.
- A `counterparty_address_changed` entry made by a payee through a payee link has `by: null`, and carries `via: "payee_link"` and `linkId` next to `counterpartyId`, `from` and `to`.
- Such a change waits for confirmation like any other; a later `counterparty_address_confirmed` records who confirmed it.

No `/api/v1` endpoint, parameter or response schema changed.

## 2026-09-30: What started a cycle

- A `cycle_complete` ledger entry's `detail` can now carry `trigger`, which says what started the cycle:
  - `"schedule"` for the six-hourly run;
  - `"manual"` for **Run cycle now**;
  - `"event"` when something in the workspace gave the agent a decision to make.
- An event cycle's `detail` also carries `events`, the kinds that led to it: `invoice_added`, `milestone_verified`, `payable_returned`, `address_confirmed`, `agent_resumed` and `sample_loaded`. `by`, when present, is the person whose action started the cycle.
- These fields are visible on `ledger.appended` events and in `GET /api/v1/ledger`.
- A cycle no longer starts while another one in the same workspace is running, so one workspace's cycles do not overlap.

No `/api/v1` endpoint, parameter or response schema changed.

## 2026-09-30: Failed-payment retry

- An `approval_paid` ledger entry's `detail` can now carry `attempt`, the transfer attempt this approval ended on (starting at 1), and, only when that attempt is a retry, `retriedAfter`: `{ providerTxId, providerState, failureReason }` for the earlier attempt Circle ended in a terminal failure. Both are visible on `ledger.appended` events and in `GET /api/v1/ledger`.
- Circle's `STUCK` transfer state is no longer a terminal failure; it is now reported the same way a transfer still `pending` is. Only `CANCELLED`, `DENIED` and `FAILED` end a transfer attempt.

No `/api/v1` endpoint, parameter or response schema changed.

## 2026-09-30: Ledger signing key rotation

- An owner can rotate a workspace's ledger signing key from Settings. The rotation is a ledger entry, delivered as a `ledger.appended` event like every other: `ledger_key_rotated`, in the `system` domain, with `actor: "human"`. Its `detail` is `{ from, to, by }`: the retired key's id, the new key's id, and the owner who rotated. See [Key identity and rotation](https://www.vestiarion.xyz/docs/api/verify-ledger#key-identity-and-rotation).
- From that entry on, `signingKeyId` is the new key's id. Earlier entries keep the old id and keep verifying against the retired key.
- `ledgerRetiredKeyCount` in `GET /api/v1/status` now counts the workspace's own retired keys.

No `/api/v1` endpoint, parameter or response schema changed.

## 2026-09-30: Counterparty limit changes

- A new ledger action, delivered as a `ledger.appended` event: `counterparty_limit_changed`, in the `compliance` domain. Its `detail` is `{ by, counterpartyId, from, to, currentLimit }`, where `from` and `to` are the configured limits in USDC and `currentLimit` is the limit the counterparty's risk now allows. A client's limit may be `null`.

No `/api/v1` endpoint, parameter or response changed.

## 2026-09-30: Counterparty address changes

- Two new ledger actions, delivered as `ledger.appended` events like every other entry, both in the `compliance` domain:
  - `counterparty_address_changed`, whose `detail` is `{ by, counterpartyId, from, to }`. `from` or `to` is `null` when there was no address before, or it was cleared.
  - `counterparty_address_confirmed`, whose `detail` is `{ by, counterpartyId, address, via }`. `via` is `"approval"` when approving a payment confirmed it, and `"confirm"` when someone confirmed it on the counterparty.
- An agent decision entry (`ap_pay` and the other `ap_*` actions) can now carry `guardrailRule: "counterparty.address_unconfirmed"`. It means the payment was held because the counterparty's address changed and no one has confirmed it. Its `observed` object gains `addressUnconfirmed`, a boolean.
- An `ap_reconcile` entry can now carry `notResubmittedBecause: "counterparty.address_unconfirmed"`, beside the existing `"counterparty.high_risk"`. A payment that never reached the network is not sent again to a changed address that no one has confirmed; it is held for a person.

No `/api/v1` endpoint, parameter or response changed.

## 2026-09-29: MCP server

- A remote MCP server at `https://www.vestiarion.xyz/api/mcp`, over Streamable HTTP. An AI agent connects with a workspace API key, sent as `Authorization: Bearer <key>`, and reads that workspace's records through one tool per `/api/v1` read operation. A tool returns the operation's JSON exactly. See [MCP server](https://www.vestiarion.xyz/docs/ai-integration/mcp).
- A missing, malformed, unknown or revoked key answers `401` with a `WWW-Authenticate` header; a key without the `read` scope answers `403`.
- There are no write tools, and no OAuth.

No `/api/v1` endpoint, parameter or response changed.

## 2026-09-29: Webhooks sent right after each append

- A `ledger.appended` delivery now goes out right after the request that appended its entry (approving a payment, pausing the agent, changing a member, a key or an endpoint), not on the next scheduled dispatch. Entries appended by the agent's scheduled run still go out right after that run. The 10-minute schedule remains for retries. See [Timing](https://www.vestiarion.xyz/docs/webhooks#timing).

No payload, header or signature changed.

## 2026-09-29: Developer docs, OpenAPI document and llms.txt

- These docs, at [`/docs`](https://www.vestiarion.xyz/docs): a quickstart, guides, and a [reference page](https://www.vestiarion.xyz/docs/api) for every endpoint, with a **Try it** panel that sends a real request with your key.
- [`GET /api/v1/openapi.json`](https://www.vestiarion.xyz/api/v1/openapi.json): an OpenAPI 3.1 document for every `/api/v1` endpoint. It is public and needs no key.
- A Markdown view of every page at its URL plus `.md`, and [`/llms.txt`](https://www.vestiarion.xyz/llms.txt) and [`/llms-full.txt`](https://www.vestiarion.xyz/llms-full.txt): see [AI integration](https://www.vestiarion.xyz/docs/ai-integration).
- **Cursors are checked per endpoint.** A `cursor` that the endpoint could not have issued, such as a ledger cursor sent to `/invoices` or an altered one, now answers `400 invalid_request`. It could reach the database before and answer `500`. See [Pagination](https://www.vestiarion.xyz/docs/get-started/pagination).

No other endpoint, parameter or response changed.

## 2026-09-29: Signed webhooks

- An owner or an admin can add up to 5 HTTPS endpoints per workspace in Settings. Each one receives a `ledger.appended` event for every new ledger entry, and a `webhook.test` event on demand. See [Webhooks](https://www.vestiarion.xyz/docs/webhooks).
- Every delivery is signed with HMAC-SHA256 in the `Vestiarion-Signature` header, using the endpoint's own secret, shown once when it is added. See [Verifying signatures](https://www.vestiarion.xyz/docs/webhooks/verify).
- Delivery is at least once and not in order. A failed delivery is retried up to 7 attempts in total, and an endpoint is disabled after 20 consecutive failed attempts. See [Retries and disabling](https://www.vestiarion.xyz/docs/webhooks/retries).

## 2026-09-29: Workspace API keys

- `/api/v1` now authenticates with a workspace API key, `vxk_<prefix>_<secret>`, sent as `Authorization: Bearer <key>`. An owner or an admin creates keys on the workspace's Settings page; each key is shown once and reads only its own workspace. See [Authentication](https://www.vestiarion.xyz/docs/get-started/authentication).
- **Breaking:** the platform token, `AGENT_API_TOKEN`, no longer opens `/api/v1`, and answers `401`. It had read only the founding workspace. Move any caller that used it to a workspace key.
- A missing, malformed, unknown or revoked key answers the same `401 unauthorized`. A key whose scopes do not cover a route answers `403 forbidden`.
- `GET /api/v1/status` reports the calling key's own workspace, and its `configuration` no longer includes the deployment's `database` block.
