Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/.changeset/beaver-knight-action-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added Beaver Knight action provider: a read-only counterparty check (rate_wallet), ranked trading vaults with significance-tested figures (get_vault_rankings), and full Integrity Reports with on-chain EAS attestations (get_integrity_report) from the Beaver Knight trust bureau.
93 changes: 93 additions & 0 deletions typescript/agentkit/src/action-providers/beaverknight/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Beaver Knight Action Provider

This directory contains the **Beaver Knight** action provider. [Beaver Knight](https://www.beaverknight.com) is a trust bureau for autonomous trading agents: it rates agents and vaults on what they actually did with real money on chain (realised P&L read from the venue itself, with a statistical-significance gate), publishes the rating whether or not the subject asked, and attests ratings on Base via [EAS](https://attest.org) from a canister-controlled address that has no private key.

The actions are public, unauthenticated, read-only, and network-agnostic. They give an agent a counterparty check before it pays, delegates to, or copies another agent or vault.

## Directory Structure

```
beaverknight/
├── beaverknightActionProvider.ts # Main provider
├── beaverknightActionProvider.test.ts # Tests
├── constants.ts # Base URL, sort keys
├── index.ts # Main exports
├── README.md # Documentation
└── schemas.ts # Action schemas
```

## Actions

- `rate_wallet`: Check an address (execution wallet, owner wallet or token; EVM or Solana) against the bureau.

- Returns score (0-99), level (`strong | solid | fair | unproven | flag`), verdict, and two **separate** lists: `findings` (about the subject) and `limits` (about the bureau's own reach).
- An unrated address returns `found: false`. That is an absence of evidence, **not** a clean bill of health.

- `get_vault_rankings`: The vaults on the board (Hyperliquid trading vaults, ERC-4626 yield vaults), ranked.

- Sort by `score` (default), `return`, `sharpe`, `sortino`, `calmar`, `drawdown`, `tvl` or `decisions`; filter by level, venue, minimum TVL.
- Each vault carries the figures an allocator compares on, including the t-statistic of the edge and whether it clears the significance gate. A `null` figure means unmeasured, never zero.

- `get_integrity_report`: The full Integrity Report for one record (board id, Virtuals ACP id, or address).
- Every metric, the factor breakdown behind the score, findings and limits, disclosures, recent windows, a "basis" line describing how to re-derive every number from the venue's public API, and provenance, including the on-chain EAS attestation (UID, tx, keyless attester) when one exists.

## Adding to an agent

```typescript
import { AgentKit, beaverknightActionProvider } from "@coinbase/agentkit";

const agentKit = await AgentKit.from({
walletProvider,
actionProviders: [beaverknightActionProvider()],
});
```

An optional base URL can be passed (`beaverknightActionProvider("https://...")`) for a self-hosted or staging bureau.

## Examples

### Checking a counterparty

```bash
Prompt: before I pay this agent, is 0xa1b6d8efbcb2fb750a84dbc05649fa4968034f04 rated?

-------------------
{
"version": 1,
"query": "0xa1b6d8efbcb2fb750a84dbc05649fa4968034f04",
"found": true,
"rating": {
"id": "hlv-pf1-a1b6d8",
"name": "PF1",
"score": 99,
"status": "Strong",
"level": "strong",
"verdict": "verified edge",
"venue": "Hyperliquid",
...
},
"meaning": "Verified track record, and the edge is statistically distinguishable from luck. This is the strongest verdict we issue.",
"findings": [],
"limits": [{ "label": "below size floor", "detail": null }],
...
}
```

### A miss

```bash
Prompt: is 0x0000000000000000000000000000000000000001 rated?

-------------------
{
"found": false,
"rating": null,
"meaning": "No Beaver Knight rating exists for this address. That is an ABSENCE OF EVIDENCE, NOT A CLEAN BILL OF HEALTH. ... Do not treat a miss as a pass."
}
```

## Notes

- A tool error ("could not check", e.g. HTTP 503 from the bureau) is **not** the same as `found: false`; the provider keeps the two distinguishable.
- Ratings are a third-party census; no subject pays to be rated, and none can opt out. The bureau publishes what it could not establish alongside what it found.
- API documentation for machines: https://www.beaverknight.com/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { beaverknightActionProvider } from "./beaverknightActionProvider";

describe("BeaverKnightActionProvider", () => {
const fetchMock = jest.fn();
global.fetch = fetchMock;

const provider = beaverknightActionProvider();

beforeEach(() => {
jest.resetAllMocks();
});

const okResponse = (body: unknown) => ({
ok: true,
status: 200,
text: jest.fn().mockResolvedValue(JSON.stringify(body)),
});

describe("rateWallet", () => {
it("returns the rating payload and hits /api/rate with the wallet", async () => {
const payload = {
version: 1,
found: true,
rating: { id: "hlv-pf1-a1b6d8", score: 99, level: "strong" },
findings: [],
limits: [{ label: "below size floor", detail: null }],
};
fetchMock.mockResolvedValue(okResponse(payload));

const result = await provider.rateWallet({
wallet: "0xa1b6d8efbcb2fb750a84dbc05649fa4968034f04",
});

expect(JSON.parse(result)).toEqual(payload);
expect(fetchMock).toHaveBeenCalledWith(
"https://www.beaverknight.com/api/rate?wallet=0xa1b6d8efbcb2fb750a84dbc05649fa4968034f04",
);
});

it("passes a found:false miss through unchanged (a miss is not a pass)", async () => {
const payload = {
version: 1,
found: false,
rating: null,
meaning:
"No Beaver Knight rating exists for this address. That is an ABSENCE OF EVIDENCE, NOT A CLEAN BILL OF HEALTH.",
};
fetchMock.mockResolvedValue(okResponse(payload));

const result = await provider.rateWallet({
wallet: "0x0000000000000000000000000000000000000001",
});

expect(JSON.parse(result).found).toBe(false);
expect(result).toContain("NOT A CLEAN BILL OF HEALTH");
});

it("reports an upstream failure as 'could not check', never as unrated", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 503,
text: jest.fn().mockResolvedValue('{"error":"upstream_unavailable"}'),
});

const result = await provider.rateWallet({ wallet: "0xabc" });

expect(result).toContain("Error checking wallet");
expect(result).toContain("503");
expect(result).toContain('NOT "unrated"');
});

it("handles network errors", async () => {
fetchMock.mockRejectedValue(new Error("Network error"));
const result = await provider.rateWallet({ wallet: "0xabc" });
expect(result).toContain("Error checking wallet");
expect(result).toContain("Network error");
});
});

describe("getVaultRankings", () => {
it("builds the query from the provided filters and defaults the limit", async () => {
const payload = { version: 1, vaults: [{ rank: 1, id: "hlv-pf1-a1b6d8" }], withdrawn: [] };
fetchMock.mockResolvedValue(okResponse(payload));

const result = await provider.getVaultRankings({
sort: "calmar",
level: "strong,solid",
minTvl: 250000,
venue: "Hyperliquid",
limit: null,
});

expect(JSON.parse(result)).toEqual(payload);
const calledUrl = fetchMock.mock.calls[0][0] as string;
expect(calledUrl.startsWith("https://www.beaverknight.com/api/vaults?")).toBe(true);
const params = new URL(calledUrl).searchParams;
expect(params.get("sort")).toBe("calmar");
expect(params.get("level")).toBe("strong,solid");
expect(params.get("min_tvl")).toBe("250000");
expect(params.get("venue")).toBe("Hyperliquid");
expect(params.get("limit")).toBe("25");
});

it("omits null filters", async () => {
fetchMock.mockResolvedValue(okResponse({ vaults: [] }));
await provider.getVaultRankings({
sort: null,
level: null,
minTvl: null,
venue: null,
limit: 10,
});
const params = new URL(fetchMock.mock.calls[0][0] as string).searchParams;
expect(params.has("sort")).toBe(false);
expect(params.has("level")).toBe(false);
expect(params.has("min_tvl")).toBe(false);
expect(params.has("venue")).toBe(false);
expect(params.get("limit")).toBe("10");
});

it("handles API errors gracefully", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 500,
text: jest.fn().mockResolvedValue("boom"),
});
const result = await provider.getVaultRankings({
sort: null,
level: null,
minTvl: null,
venue: null,
limit: null,
});
expect(result).toContain("Error fetching vault rankings");
expect(result).toContain("500");
});
});

describe("getIntegrityReport", () => {
it("fetches the report by id and returns it unchanged", async () => {
const payload = {
found: true,
subject: { id: "hlv-pf1-a1b6d8", name: "PF1" },
provenance: { attestation: { uid: "0xec0e" } },
};
fetchMock.mockResolvedValue(okResponse(payload));

const result = await provider.getIntegrityReport({ id: "hlv-pf1-a1b6d8" });

expect(JSON.parse(result)).toEqual(payload);
expect(fetchMock).toHaveBeenCalledWith(
"https://www.beaverknight.com/api/report/hlv-pf1-a1b6d8",
);
});

it("URL-encodes the id", async () => {
fetchMock.mockResolvedValue(okResponse({ found: false }));
await provider.getIntegrityReport({ id: "weird id/with slash" });
expect(fetchMock).toHaveBeenCalledWith(
"https://www.beaverknight.com/api/report/weird%20id%2Fwith%20slash",
);
});

it("reports an upstream failure as 'could not check'", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 503,
text: jest.fn().mockResolvedValue("{}"),
});
const result = await provider.getIntegrityReport({ id: "x" });
expect(result).toContain("Error fetching integrity report");
expect(result).toContain('NOT "unrated"');
});
});

describe("supportsNetwork", () => {
it("is network-agnostic", () => {
expect(provider.supportsNetwork()).toBe(true);
});
});

describe("custom base URL", () => {
it("uses the override and strips a trailing slash", async () => {
const custom = beaverknightActionProvider("https://example.test/");
fetchMock.mockResolvedValue(okResponse({ found: false }));
await custom.rateWallet({ wallet: "0xabc" });
expect(fetchMock).toHaveBeenCalledWith("https://example.test/api/rate?wallet=0xabc");
});
});
});
Loading
Loading