> For the complete documentation index, see [llms.txt](https://docs.buoy.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.buoy.finance/developers/automating-a-strategy.md).

# Automating a Vault Strategy

This page is for Leaders running a **bot** on a vault's trading agent key, rather than trading by hand. It answers the questions that only come up once something automated is sizing positions and reacting to events: which number to trust, how to observe withdrawals, and how to behave while the protocol is settling one.

If you haven't read [Liquidity & Withdrawals](/for-vault-leaders/liquidity-and-withdrawals.md) and [Request Lifecycle](/protocol-deep-dive/request-lifecycle.md), start there — this page assumes both.

## The premise: you don't own the book

The single most important fact for an automated integration:

> The settlement service is a **co-operator** of your vault's HyperCore account. It can cancel nothing of yours, but it *can* close your positions and pull your margin, at any moment, without warning, to fund a depositor's withdrawal.

Everything else on this page follows from that. A bot that keeps internal state about "the positions I opened" will be wrong the first time a withdrawal lands. A bot that re-derives its target book from live venue state each cycle will not.

Note also what protects what: the vault's **interaction nonce** guards *settlement* against stale valuations. It does not gate *trading*. Nothing in the contracts stops your agent from re-opening a position one second after the settlement service closed it — the two of you are simply acting on the same account.

## 1. Which value to size from

Three plausible sources, all different. None of them is the right answer on its own:

| Source                                                | Gross or net?                                                   | Freshness                                                                                  | Covers                                                                                      |
| ----------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| REST `tvl` (`GET /vaults/{address}`)                  | **Gross** — does **not** subtract locked withdrawal obligations | Live, cached ≤5 min (invalidated on any on-chain event for the vault)                      | HyperCore spot + perps + HIP-3/4, **plus** EVM-side USDC, minus uncredited pending deposits |
| On-chain `totalAssets()` / `pricePerShare()`          | **Net** — subtracts `totalOwedUsdc`                             | **Cached, and only rewritten when a request settles.** No NAV heartbeat runs in production | Same coverage, but frozen at the last settlement                                            |
| HyperCore account value (`clearinghouseState` + spot) | Gross, and vault-account only                                   | Live                                                                                       | Misses EVM-side USDC; doesn't know about obligations at all                                 |

**Recommended: live gross NAV − `totalOwedUsdc`.**

```
netNav = tvl (from GET /vaults/{address})  −  vault.totalOwedUsdc()
```

Equivalently, take `pricePerShare` from a quote endpoint (`GET /vaults/{address}/quote/deposit`) and multiply by `totalSupply()` — quotes are computed from a **freshly read** live NAV, bypassing the cache entirely, and already net out obligations. That's the freshest public number available.

{% hint style="danger" %}
**Do not size from `totalAssets()` alone.** It is net, which is what you want, but it reads a cache that is only written at settlement. On a vault with no recent deposits or withdrawals it can be hours or days stale while your positions move every second. Always check `latestNavTimestamp()` before trusting it.

**Do not size from REST `tvl` alone either.** It is live, but gross. Between `WithdrawalLocked` and the payout actually leaving HyperCore, `tvl` still counts USDC that is already legally owed to an exiting depositor — precisely the window where over-sizing hurts. Subtract `totalOwedUsdc()`.
{% endhint %}

`GET /vaults` (the list endpoint) is not a substitute: its TVL overlay is non-blocking and falls back to a stored snapshot value. Use the single-vault endpoint or a quote.

## 2. `getNavSnapshot()` — one call for everything

The vault exposes an atomic view of exactly the state an automated integration needs. It's the same view the settlement service prices against:

```solidity
function getNavSnapshot() external view returns (
    uint256 nonce,            // interactionNonce — bumps on every state change
    uint256 pendingDeposits,  // USDC escrowed but not yet credited as shares
    uint256 owed,             // totalOwedUsdc — locked withdrawal obligations
    uint256 evmUsdcBalance,   // the vault's USDC on HyperEVM
    uint256 supply,           // totalSupply of shares
    uint256 cachedNav,        // latestNav (GROSS, and possibly stale — see above)
    uint64  cachedNavTs       // when cachedNav was written
);
```

One RPC call gives you `owed` for the netting above, `pendingDeposits` (capital that is on its way in but isn't yours to deploy yet), and `nonce` — a cheap change-detector. If `nonce` hasn't moved, no deposit settled, no withdrawal locked or paid, no cancellation happened.

## 3. Observing withdrawal state

### On-chain, no indexer required

Request IDs are dense and the counter is public, so a bot can enumerate its own vault's entire request history directly:

```solidity
uint256 n = vault.nextLocalRequestId();       // ids run 1..n
// localId space is SHARED between deposits and withdrawals
vault.getDepositRequest(id);                  // status None if id is a withdrawal
vault.getWithdrawalRequest(id);               // status None if id is a deposit
```

`WithdrawalRequest` gives you `user`, `shares`, `owedUsdc`, `minOut`, `createdAt`, `status`. The status enums are:

| Deposit                                     | Withdrawal                                           |
| ------------------------------------------- | ---------------------------------------------------- |
| `None`, `Pending`, `Fulfilled`, `Cancelled` | `None`, `Pending`, `Locked`, `Released`, `Cancelled` |

Note `Released` is the on-chain name for what the app and these docs call **Fulfilled**.

In practice you only need to scan from the highest id you've already seen, plus re-check the ids you know are still open.

### Via events

Router events carry the `globalId`. One trap worth knowing before you write the filter:

* `DepositRequested` and `WithdrawalRequested` are indexed by **`globalId`, `vault`, and `localId`** — you can filter these by vault directly.
* Every settlement event — `WithdrawalLocked`, `WithdrawalFulfilled`, `DepositFulfilled`, `*Cancelled`, `*Skipped` — is indexed by **`globalId` only**. There is no `vault` topic to filter on.

So event-based tracking is necessarily two-stage: learn your vault's `globalId`s from the `*Requested` events, then watch settlement events for those ids. If you only subscribe to the settlement events, you'll either miss your vault or have to decode every vault's traffic.

`Router.getGlobalRequest(globalId)` maps a global id back to `(vault, localId, kind)`.

Also remember that settlement is **batched**: a request can be quietly passed over with a `DepositSkipped` / `WithdrawalSkipped` event carrying the raw revert reason, and stay open for the next attempt. Absence of a `*Fulfilled` event is not a failure signal.

## 4. `needs-liquidity` has no on-chain signal

There is **no event and no on-chain state** for it. A withdrawal the settlement service cannot fund stays exactly `Locked`, indefinitely, indistinguishable on-chain from one that is about to be paid in the next block. `needs-liquidity` is an internal status inside the settlement service, surfaced only through its own operational alerting.

The practical consequence for a bot: **you cannot wait for "settlement finished"**. A `Locked` request that can't be funded will sit there while the service re-drives it every \~15 seconds, forever, until conditions change. Any logic of the form *"pause trading until the withdrawal clears"* is a logic that can deadlock your strategy for days.

What you *can* observe is the thing that actually matters: `owed` from `getNavSnapshot()`. If it's non-zero, the vault owes money it hasn't paid yet. If it stays non-zero and the vault's free USDC isn't rising, the service is stuck and needs your capital freed — that's the same signal a human Leader acts on.

## 5. Two integration patterns

### A. Converge (what Buoy's own engine does)

Don't watch withdrawals at all. Every cycle, read live venue state, compute the desired book from current equity, diff against what's actually open, and issue the difference. If the settlement service closed half a position since the last cycle, the diff simply re-establishes it — or doesn't, if equity dropped enough that the target shrank.

Buoy's own strategy engine runs this way: a full reconciliation every 30 seconds, zero cash buffer, all capital in margin, no withdrawal awareness anywhere in the code. It's simple, it cannot deadlock, and it has no internal state to corrupt. The cost is that you may re-establish a position the service just closed, paying the spread twice.

Suits: liquid majors, mechanical strategies, cheap re-entry.

### B. Defend (yield to settlement)

Detect an open withdrawal, cancel resting orders, allow only reduce-only orders until the obligation clears, then resume. This avoids fighting the settlement service for the same margin.

If you build this, two rules are non-negotiable:

1. **Gate on `owed > 0`, not on events.** Events tell you a lock happened; `owed` tells you whether it's still outstanding. Only the latter has a reliable falling edge.
2. **Time-box the pause.** Because `needs-liquidity` is invisible and unbounded, an un-bounded pause is a deadlock. After a threshold, either resume reduced trading or alert a human — and if the vault genuinely can't fund the exit, the correct action is to *free capital*, not to keep waiting.

Suits: concentrated or illiquid books, expensive re-entry.

{% hint style="warning" %}
What doesn't work is the middle: deploying all capital *and* assuming your positions persist. Pick A or B deliberately.
{% endhint %}

## 6. Liquidity buffer

There is no protocol-mandated buffer and no universally correct number. Under unified accounts a "buffer" simply means leaving part of the vault's spot USDC un-held by margin.

Buoy's own engine runs **zero** and relies on convergence. A buffer of a few percent of equity buys you control over *what* gets closed and *when*, at the cost of deployed capital — worth it when a forced partial close is expensive to reverse, or when your depositor base is concentrated enough that one exit is material. Size it against your actual redemption pattern, not a rule of thumb.

## Checklist

1. Size from **live gross NAV − `totalOwedUsdc`**, or from a quote's `pricePerShare × totalSupply`. Never from `totalAssets()` without checking `latestNavTimestamp()`.
2. Exclude `pendingDeposits` — that capital isn't yours to deploy yet (the live NAV already nets it out).
3. Poll `getNavSnapshot()` as your cheap change-detector; `nonce` unchanged means nothing settled.
4. Re-derive your book from live venue state every cycle. Keep no authoritative internal position state.
5. If you gate on withdrawals, gate on `owed > 0` and time-box the pause.
6. Expect `*Skipped` events; a request with no `*Fulfilled` event is waiting, not failed.
7. Handle agent expiry: a vault whose trading agent lapsed can't trade at all. See [Trading Agent](/for-vault-leaders/trading-agent.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.buoy.finance/developers/automating-a-strategy.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
