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 .changeset/tame-lions-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added a new action provider to interact with invinoveritas
13 changes: 13 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,19 @@ const agent = createAgent({
</table>
</details>
<details>
<summary><strong>invinoveritas</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>review</code></td>
<td width="768">Gets an independent, capital/risk-aware verdict on a proposed action (trade, on-chain transaction, sanctions screening, code diff, etc.) before it executes. Advisory, never blocks; optionally returns a portable signed proof any third party can recompute.</td>
</tr>
<tr>
<td width="200"><code>verify_proof</code></td>
<td width="768">Verifies a signed invinoveritas proof another agent handed you — free, no auth, recomputed against a published key rather than trusted.</td>
</tr>
</table>
</details>
<details>
<summary><strong>Messari</strong></summary>
<table width="100%">
<tr>
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from "./enso";
export * from "./erc20";
export * from "./erc721";
export * from "./erc8004";
export * from "./invinoveritas";
export * from "./farcaster";
export * from "./jupiter";
export * from "./messari";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# invinoveritas Action Provider

This directory contains the invinoveritas action provider implementation, which provides actions to
get an independent, capital/risk-aware verdict on a proposed agent action before it executes, and to
verify a signed proof another agent hands you.

invinoveritas is a verification layer for autonomous agents: a neutral second opinion before an
irreversible action (`review`), and a free, no-auth way to check a proof another agent presents
(`verify_proof`) without trusting the presenter or invinoveritas — only the math (BIP-340 schnorr
against a published key). The verifier keeps a public, Bitcoin-anchored track record of past verdicts
(wins and losses) at `/ledger`.

Both actions are automated and advisory — the agent decides, and a review call never blocks or throws
into the agent's main flow. This is not a human-in-the-loop approval step.

## Getting Started

`verify_proof` requires no configuration — it's free and unauthenticated. `review` needs an API key:

1. Get one free: `POST https://api.babyblueviper.com/register {"label":"your-app"}` (free trial calls
included, no funding needed to start)
2. Fund with Lightning sats, USDC (x402 on Base), or a card for continued paid use

### Environment Variables

```
INVINOVERITAS_API_KEY
```

Alternatively, configure the provider directly:

```typescript
import { invinoveritasActionProvider } from "@coinbase/agentkit";

const provider = invinoveritasActionProvider({
apiKey: "your_invinoveritas_api_key",
});
```

## Directory Structure

```
invinoveritas/
├── constants.ts # API base URL, timeout, error strings
├── invinoveritasActionProvider.test.ts # Tests for the provider
├── invinoveritasActionProvider.ts # Main provider: review + verify_proof
├── index.ts # Main exports
├── README.md # Documentation
├── schemas.ts # Action input schemas
├── types.ts # Type definitions
└── utils.ts # postJson fetch helper (shared timeout/error handling)
```

## Actions

- `review`: Get an independent verdict on a proposed action before executing it
- `artifactType='trade'` for a capital-scale-aware risk review (position size vs equity, drawdown, regime)
- `artifactType='onchain_action'` for deterministic checks (unlimited approvals, drainers, address poisoning, permit abuse, wrong-chain recipient)
- `artifactType='sanctions_screening'` for an overclaim-boundary check on a compliance verdict
- `sign=true` returns a portable signed proof any third party can recompute (`POST /verify-proof`), independent of trusting invinoveritas's pipeline
- Returns `{ verdict, confidence, summary, issues, alternative_approaches, onchain_risk, proof? }`
- Degrades to `{ verdict: "review_unavailable", reason }` on missing key, timeout, or a non-2xx response — never throws

- `verify_proof`: Verify a signed invinoveritas proof another agent handed you (free, no auth)
- Recomputes the Nostr event id and checks the BIP-340 schnorr signature against the published key
- Pass the signed `event` object, or a stored `proofId`
- Returns `{ valid: true|false, checks: {...} }`

## Network Support

`invinoveritas` is API-only — it makes HTTP calls, not wallet operations — so `supportsNetwork` always
returns `true`. No wallet provider is required to use this action provider.

## Notes

Unlike most action providers in this repo, the constructor does **not** throw when no API key is
configured. `verify_proof` is a free action that works with zero configuration, and `review` is
designed to degrade to an advisory `review_unavailable` result rather than block agent construction
for a user who hasn't set up a key yet.

## Adding New Actions

To add new invinoveritas actions:

1. Define your schema in `schemas.ts`
2. Implement your action in `invinoveritasActionProvider.ts`
3. Add corresponding tests in `invinoveritasActionProvider.test.ts`

For more information on invinoveritas, visit [api.babyblueviper.com](https://api.babyblueviper.com).
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DEFAULT_BASE_URL = "https://api.babyblueviper.com";
export const DEFAULT_TIMEOUT_MS = 20000;
export const INVINOVERITAS_API_KEY_MISSING_ERROR =
'No invinoveritas API key. Set INVINOVERITAS_API_KEY or pass apiKey to invinoveritasActionProvider(). Get one free: POST https://api.babyblueviper.com/register {"label":"your-app"}.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./invinoveritasActionProvider";
export * from "./schemas";
export * from "./types";
export * from "./constants";
export * from "./utils";
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import {
invinoveritasActionProvider,
InvinoveritasActionProvider,
} from "./invinoveritasActionProvider";

const MOCK_API_KEY = "invinoveritas-test-key";

const MOCK_REVIEW_RESPONSE = {
verdict: "approve",
confidence: 0.9,
summary: "Looks fine.",
issues: [],
};

const MOCK_VERIFY_PROOF_RESPONSE = {
valid: true,
checks: {
id_integrity: true,
signature_valid: true,
issued_by_invinoveritas: true,
is_proof_event: true,
},
};

describe("InvinoveritasActionProvider", () => {
let provider: InvinoveritasActionProvider;

beforeEach(() => {
delete process.env.INVINOVERITAS_API_KEY;
provider = invinoveritasActionProvider({ apiKey: MOCK_API_KEY });
jest.restoreAllMocks();
});

afterEach(() => {
jest.clearAllMocks();
delete process.env.INVINOVERITAS_API_KEY;
});

describe("constructor", () => {
it("should initialize with API key from constructor", () => {
const customProvider = invinoveritasActionProvider({ apiKey: "custom-key" });
expect(customProvider["apiKey"]).toBe("custom-key");
});

it("should initialize with API key from environment variable", () => {
process.env.INVINOVERITAS_API_KEY = "env-key";
const envProvider = invinoveritasActionProvider();
expect(envProvider["apiKey"]).toBe("env-key");
});

it("should NOT throw when no API key is provided — verify_proof needs none, and review must degrade, not block construction", () => {
expect(() => invinoveritasActionProvider()).not.toThrow();
});

it("should default baseUrl and timeoutMs when not provided", () => {
const defaultProvider = invinoveritasActionProvider({ apiKey: "k" });
expect(defaultProvider["baseUrl"]).toBe("https://api.babyblueviper.com");
expect(defaultProvider["timeoutMs"]).toBe(20000);
});
});

describe("review", () => {
it("should return review_unavailable (not throw) when no API key is configured", async () => {
const noKeyProvider = invinoveritasActionProvider();
const response = await noKeyProvider.review({ artifact: "swap 1 ETH for USDC" });
const parsed = JSON.parse(response);
expect(parsed.verdict).toBe("review_unavailable");
});

it("should successfully fetch a review verdict", async () => {
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_REVIEW_RESPONSE,
} as Response);

const response = await provider.review({
artifact: "swap 1 ETH for USDC",
artifactType: "onchain_action",
sign: true,
});
const parsed = JSON.parse(response);

expect(fetchMock).toHaveBeenCalled();
const [url, options] = fetchMock.mock.calls[0];
expect(url).toBe("https://api.babyblueviper.com/review");
expect(options).toEqual(
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Content-Type": "application/json",
Authorization: `Bearer ${MOCK_API_KEY}`,
}),
}),
);
expect(JSON.parse(options!.body as string)).toEqual({
artifact: "swap 1 ETH for USDC",
artifact_type: "onchain_action",
context: undefined,
sign: true,
});
expect(parsed.verdict).toBe("approve");
expect(parsed.confidence).toBe(0.9);
});

it("should default artifact_type to 'general' when not provided", async () => {
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_REVIEW_RESPONSE,
} as Response);

await provider.review({ artifact: "rename a local variable" });

const [, options] = fetchMock.mock.calls[0];
expect(JSON.parse(options!.body as string).artifact_type).toBe("general");
});

it("should degrade to review_unavailable on a non-ok HTTP response, not throw", async () => {
jest.spyOn(global, "fetch").mockResolvedValue({
ok: false,
status: 402,
json: async () => ({ detail: "Payment Required" }),
} as Response);

const response = await provider.review({ artifact: "swap 1 ETH for USDC" });
const parsed = JSON.parse(response);
expect(parsed.verdict).toBe("review_unavailable");
expect(parsed.reason).toContain("402");
});

it("should degrade to review_unavailable on a network error, not throw", async () => {
jest.spyOn(global, "fetch").mockRejectedValue(new Error("network down"));

const response = await provider.review({ artifact: "swap 1 ETH for USDC" });
const parsed = JSON.parse(response);
expect(parsed.verdict).toBe("review_unavailable");
});

it("should surface the proof and recompute_proof_at only when sign=true and a proof is returned", async () => {
jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => ({ ...MOCK_REVIEW_RESPONSE, proof: { event: { id: "abc" } } }),
} as Response);

const response = await provider.review({ artifact: "swap 1 ETH for USDC", sign: true });
const parsed = JSON.parse(response);
expect(parsed.proof).toEqual({ event: { id: "abc" } });
expect(parsed.recompute_proof_at).toBe("https://api.babyblueviper.com/verify-proof");
});
});

describe("verifyProof", () => {
it("should require either event or proofId", async () => {
const response = await provider.verifyProof({});
const parsed = JSON.parse(response);
expect(parsed.valid).toBe(false);
expect(parsed.error).toContain("Provide");
});

it("should work with NO API key configured — free, no-auth action", async () => {
const noKeyProvider = invinoveritasActionProvider();
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_VERIFY_PROOF_RESPONSE,
} as Response);

const response = await noKeyProvider.verifyProof({ event: { id: "abc" } });
const parsed = JSON.parse(response);

const [, options] = fetchMock.mock.calls[0];
expect((options!.headers as Record<string, string>)["Authorization"]).toBeUndefined();
expect(parsed.valid).toBe(true);
});

it("should verify a proof event and return the result unmodified", async () => {
jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_VERIFY_PROOF_RESPONSE,
} as Response);

const response = await provider.verifyProof({ event: { id: "abc" } });
expect(JSON.parse(response)).toEqual(MOCK_VERIFY_PROOF_RESPONSE);
});

it("should verify by proofId when event is not provided", async () => {
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({
ok: true,
json: async () => MOCK_VERIFY_PROOF_RESPONSE,
} as Response);

await provider.verifyProof({ proofId: "proof-123" });

const [, options] = fetchMock.mock.calls[0];
expect(JSON.parse(options!.body as string)).toEqual({
event: undefined,
proof_id: "proof-123",
});
});

it("should degrade to valid:false on a network error, not throw", async () => {
jest.spyOn(global, "fetch").mockRejectedValue(new Error("network down"));

const response = await provider.verifyProof({ event: { id: "abc" } });
const parsed = JSON.parse(response);
expect(parsed.valid).toBe(false);
});
});

describe("supportsNetwork", () => {
it("should return true for any network — invinoveritas is API-only, no wallet operations", () => {
expect(provider.supportsNetwork({ protocolFamily: "evm" } as never)).toBe(true);
expect(provider.supportsNetwork({ protocolFamily: "svm" } as never)).toBe(true);
});
});
});
Loading
Loading