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

Add MainStreet action provider: `check_reputation` returns an onchain-verifiable SAFE/CAUTION/BLOCK reputation verdict + 0–100 score for a Base counterparty before payment, so an agent can refuse to pay BLOCK-rated or unscored addresses.
9 changes: 9 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,15 @@ const agent = createAgent({
</table>
</details>
<details>
<summary><strong>MainStreet</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>check_reputation</code></td>
<td width="768">Returns an onchain-verifiable SAFE/CAUTION/BLOCK reputation verdict + 0-100 score for a Base wallet, agent, or token before payment, so an agent can refuse to pay BLOCK-rated or unscored counterparties.</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 @@ -18,6 +18,7 @@ export * from "./erc721";
export * from "./erc8004";
export * from "./farcaster";
export * from "./jupiter";
export * from "./mainstreet";
export * from "./messari";
export * from "./pyth";
export * from "./moonwell";
Expand Down
26 changes: 26 additions & 0 deletions typescript/agentkit/src/action-providers/mainstreet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# MainStreet Action Provider

This directory contains the **MainStreetActionProvider** — an onchain reputation check for a Base
counterparty before an agent transacts or pays it (e.g. via x402).

[MainStreet](https://avisradar-production.up.railway.app/mainstreet.html) is an onchain reputation
oracle for agent-to-agent payments on Base. For any wallet, agent, or token it returns a
SAFE / CAUTION / BLOCK verdict + a 0–100 score as an **EIP-712-signed attestation, verifiable onchain**
against the MainStreetVerifier contract (`0x7397adb9713934c36d22aa54b4dbbcd70263592b`) — the signal is
checkable, not "trust us". Free, no signup (100 checks/day/IP).

## Actions

- `check_reputation`: Returns the reputation verdict + score for a Base address, with a pay / don't-pay
recommendation and a link to verify the signed attestation. Use it as a pre-payment / pre-routing
trust gate — refuse to settle to a BLOCK-rated or unscored counterparty.

## Usage

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

const provider = mainstreetActionProvider();
```

No configuration required (public read API). Pairs naturally with the `x402` and `erc8004` providers.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./mainstreetActionProvider";
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { mainstreetActionProvider } from "./mainstreetActionProvider";

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

const provider = mainstreetActionProvider();
const ADDR = "0x325bdf6f7efab24a2210c48c1b64cab2eae1d430";

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

describe("checkReputation", () => {
it("recommends proceeding for a SAFE counterparty with a real score", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ score: 76, verdict: "SAFE" }),
});

const result = JSON.parse(await provider.checkReputation({ address: ADDR }));
expect(result.score).toBe(76);
expect(result.verdict).toBe("SAFE");
expect(result.recommendation).toContain("OK to proceed");
});

it("refuses to pay a BLOCK-rated counterparty", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ score: 10, verdict: "BLOCK" }),
});

const result = JSON.parse(await provider.checkReputation({ address: ADDR }));
expect(result.recommendation).toContain("DO NOT PAY");
});

it("treats an unknown / unscored counterparty as CAUTION (never auto-OK)", async () => {
fetchMock.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ score: null, verdict: null }),
});

const result = JSON.parse(await provider.checkReputation({ address: ADDR }));
expect(result.recommendation).toContain("CAUTION");
});

it("handles HTTP errors gracefully", async () => {
fetchMock.mockResolvedValue({ ok: false, status: 404 });

const result = await provider.checkReputation({ address: ADDR });
expect(result).toContain("MainStreet error");
expect(result).toContain("404");
});

it("handles network errors gracefully", async () => {
fetchMock.mockRejectedValue(new Error("Network error"));

const result = await provider.checkReputation({ address: ADDR });
expect(result).toContain("MainStreet error");
expect(result).toContain("Network error");
});
});

describe("supportsNetwork", () => {
it("supports any network (network-agnostic API call)", () => {
expect(provider.supportsNetwork()).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { z } from "zod";
import { ActionProvider } from "../actionProvider";
import { CreateAction } from "../actionDecorator";
import { CheckReputationSchema } from "./schemas";

const MAINSTREET_BASE_URL = "https://avisradar-production.up.railway.app";

/**
* MainStreetActionProvider lets an agent check the onchain reputation of a Base
* counterparty BEFORE it transacts or pays via x402.
*
* MainStreet is an onchain reputation oracle for agent-to-agent payments on Base:
* for any wallet, agent, or token it returns a SAFE / CAUTION / BLOCK verdict + a
* 0-100 score as an EIP-712-signed attestation, verifiable onchain against the
* MainStreetVerifier contract on Base (0x7397adb9713934c36d22aa54b4dbbcd70263592b) —
* so the trust signal is checkable, not "trust us". Network-agnostic API call.
*/
export class MainStreetActionProvider extends ActionProvider {
/**
* Constructor for the MainStreetActionProvider class.
*/
constructor() {
super("mainstreet", []);
}

/**
* Checks the onchain reputation of a Base wallet / agent / token deployer.
*
* @param args - The address to check.
* @returns A JSON string with the score, verdict, a pay/don't-pay recommendation, and a verify link.
*/
@CreateAction({
name: "check_reputation",
description: `Check the onchain reputation of a Base wallet, agent, or token deployer BEFORE paying or transacting with it.
Returns a SAFE / CAUTION / BLOCK verdict + a 0-100 score from MainStreet, an onchain reputation oracle for agent-to-agent payments on Base.
Use this as a pre-payment / pre-routing trust gate: refuse to settle to a BLOCK-rated or low-scoring counterparty.
Takes the following input:
- address: the 0x Base address to check.
Notes:
- Free, no signup (100 checks/day/IP).
- The verdict is backed by settlement history, endpoint health, ERC-8004 feedback and identity proofs, and is EIP-712-signed + verifiable onchain against the MainStreetVerifier contract.`,
schema: CheckReputationSchema,
})
async checkReputation(args: z.infer<typeof CheckReputationSchema>): Promise<string> {
try {
const url = `${MAINSTREET_BASE_URL}/api/agent/score/${args.address}`;
const response = await fetch(url, { headers: { accept: "application/json" } });

if (!response.ok) {
return `MainStreet error: HTTP ${response.status} checking ${args.address}`;
}

const data = (await response.json()) as {
score?: number | null;
verdict?: string | null;
trustLevel?: string | null;
};
const score = data.score ?? null;
const verdict = data.verdict ?? data.trustLevel ?? "UNKNOWN";

// Safe default: only green-light a known SAFE counterparty with a real score.
// An unknown / unscored address (no reputation history — e.g. a fresh wallet)
// must NOT default to "OK", or the gate is useless against new scam wallets.
const recommendation =
verdict === "BLOCK" || (typeof score === "number" && score < 30)
? "DO NOT PAY — BLOCK-rated or below a safe threshold."
: verdict === "SAFE" && typeof score === "number" && score >= 30
? "OK to proceed — counterparty looks trustworthy."
: "CAUTION — no / insufficient reputation history; verify before paying.";

return JSON.stringify({
address: args.address,
score,
verdict,
recommendation,
verify: `${MAINSTREET_BASE_URL}/api/agent/attestation/${args.address}`,
source:
"MainStreet — onchain reputation oracle on Base (EIP-712 signed, verifiable against MainStreetVerifier 0x7397adb9713934c36d22aa54b4dbbcd70263592b).",
});
} catch (error) {
return `MainStreet error: ${error instanceof Error ? error.message : String(error)}`;
}
}

/**
* Checks if the MainStreet action provider supports the given network.
* MainStreet scores Base addresses but the lookup is a network-agnostic API call.
*
* @returns True — MainStreet actions are callable from any network context.
*/
supportsNetwork(): boolean {
return true;
}
}

/**
* Creates a new instance of the MainStreet action provider.
*
* @returns A new MainStreetActionProvider instance.
*/
export const mainstreetActionProvider = () => new MainStreetActionProvider();
13 changes: 13 additions & 0 deletions typescript/agentkit/src/action-providers/mainstreet/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";

/**
* Input schema for checking the MainStreet onchain reputation of a Base address.
*/
export const CheckReputationSchema = z
.object({
address: z
.string()
.regex(/^0x[a-fA-F0-9]{40}$/, "must be a 0x-prefixed 40-hex Base address")
.describe("The Base wallet, agent, or token-deployer address to check (0x...)."),
})
.strict();
Loading