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
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 "./m2mSentinel";
export * from "./messari";
export * from "./pyth";
export * from "./moonwell";
Expand Down
26 changes: 26 additions & 0 deletions typescript/agentkit/src/action-providers/m2mSentinel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# M2M Sentinel Action Provider

The M2M Sentinel Action Provider equips AgentKit agents with smart contract bytecode analysis, proxy detection, and token capability observations on Base Mainnet.

## Capabilities

- **`audit_contract`**: Inspect target contract bytecode capability observations, proxy implementation slots (EIP-1967/UUPS/Beacon), and known limitation heuristics in <35ms.
- **`get_gas_metrics`**: Get real-time Base network gas metrics and fee recommendations.
- **`get_token_price`**: Observe real-time Base DEX token prices for slippage checks and preflight valuation.
- **`get_service_status`**: Check operational status and trust quorum of M2M Sentinel verification rails.

## Usage

```typescript
import { AgentKit } from "@coinbase/agentkit";
import { m2mSentinelActionProvider } from "@coinbase/agentkit/action-providers/m2mSentinel";

const agentKit = await AgentKit.from({
walletProvider,
actionProviders: [
m2mSentinelActionProvider({
apiKey: process.env.M2M_SENTINEL_API_KEY, // Optional, free tier available at m2msentinel.vercel.app
}),
],
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const SUPPORTED_NETWORKS = ["base-mainnet", "base-sepolia"];

export const DEFAULT_BASE_URL = "https://m2msentinel.vercel.app";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./m2mSentinelActionProvider";
export type { M2MSentinelConfig } from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { m2mSentinelActionProvider, M2MSentinelActionProvider } from "./m2mSentinelActionProvider";
import { EvmWalletProvider } from "../../wallet-providers";
import { Network } from "../../network";

describe("M2MSentinelActionProvider", () => {
let provider: M2MSentinelActionProvider;
let mockWallet: EvmWalletProvider;

beforeEach(() => {
provider = m2mSentinelActionProvider();
mockWallet = {} as unknown as EvmWalletProvider;
vi.restoreAllMocks();
});

it("should have the correct provider name", () => {
expect(provider.name).toBe("m2m_sentinel");
});

it("should support Base networks", () => {
const baseMainnet: Network = { protocolFamily: "evm", chainId: "8453", networkId: "base-mainnet" };
const baseSepolia: Network = { protocolFamily: "evm", chainId: "84532", networkId: "base-sepolia" };
const solana: Network = { protocolFamily: "svm", chainId: "solana", networkId: "solana" };

expect(provider.supportsNetwork(baseMainnet)).toBe(true);
expect(provider.supportsNetwork(baseSepolia)).toBe(true);
expect(provider.supportsNetwork(solana)).toBe(false);
});

it("should expose all 4 actions", () => {
const actions = provider.getActions(mockWallet);
expect(actions).toHaveLength(4);

const actionNames = actions.map((a) => a.name);
expect(actionNames).toContain("m2m_audit_contract");
expect(actionNames).toContain("m2m_get_gas_metrics");
expect(actionNames).toContain("m2m_get_token_price");
expect(actionNames).toContain("m2m_get_service_status");
});

it("should handle auditContract successfully", async () => {
const mockAudit = {
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
proxyResolution: { isProxy: true },
provenance: { trustLevel: "HIGH_TRUST_PRIMARY" },
};

global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockAudit,
} as Response);

const result = await provider.auditContract(mockWallet, {
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
});

const parsed = JSON.parse(result);
expect(parsed.status).toBe("SUCCESS");
expect(parsed.notASafetyGuarantee).toBe(true);
expect(parsed.data.address).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { z } from "zod";
import { ActionProvider } from "../actionProvider";
import { EvmWalletProvider } from "../../wallet-providers";
import { CreateAction } from "../actionDecorator";
import { Network } from "../../network";
import {
AuditContractSchema,
GetGasMetricsSchema,
GetTokenPriceSchema,
GetServiceStatusSchema,
M2MSentinelConfig,
} from "./schemas";
import { SUPPORTED_NETWORKS, DEFAULT_BASE_URL } from "./constants";

/**
* M2MSentinelActionProvider enables AI agents on Base to perform preflight bytecode
* inspection, EIP-1967/UUPS proxy detection, token capability observations, and network metrics.
*/
export class M2MSentinelActionProvider extends ActionProvider<EvmWalletProvider> {
private readonly baseUrl: string;
private readonly apiKey?: string;

/**
* Creates an instance of M2MSentinelActionProvider.
*
* @param config - Optional configuration object
*/
constructor(config?: M2MSentinelConfig) {
super("m2m_sentinel", []);
this.baseUrl = config?.baseUrl || DEFAULT_BASE_URL;
this.apiKey = config?.apiKey || process.env.M2M_SENTINEL_API_KEY;
}

/**
* Checks if the target network is supported by M2M Sentinel.
*
* @param network - The network instance to validate
* @returns True if network is Base mainnet or Base Sepolia
*/
supportsNetwork(network: Network): boolean {
const chainId = String(network.chainId || network.networkId || "");
const protocolFamily = String(network.protocolFamily || "evm").toLowerCase();
return (
protocolFamily === "evm" &&
(SUPPORTED_NETWORKS.includes(network.networkId || "") ||
chainId === "8453" ||
chainId === "84532")
);
}

private async fetchApi(path: string, init?: RequestInit): Promise<Response> {
const headers: Record<string, string> = {
Accept: "application/json",
"User-Agent": "CoinbaseAgentKit-M2MSentinel/1.1.0",
...(init?.headers as Record<string, string>),
};
if (this.apiKey && !headers["x-api-key"]) {
headers["x-api-key"] = this.apiKey;
}
return fetch(`${this.baseUrl}${path}`, {
...init,
headers,
});
}

/**
* Inspects a Base target contract for bytecode capability observations, proxy implementation slots, and limitations.
*
* @param _wallet - The wallet provider instance (not consumed for read operations)
* @param args - Arguments containing target contract address
* @returns Factual audit observations and proxy resolution
*/
@CreateAction({
name: "audit_contract",
description:
"Inspect Base smart contract bytecode capability observations, proxy implementation slots, and limitations before executing transactions. Returns factual evidence, not a safety guarantee.",
schema: AuditContractSchema,
})
async auditContract(
_wallet: EvmWalletProvider,
args: z.infer<typeof AuditContractSchema>,
): Promise<string> {
try {
const res = await this.fetchApi(`/v1/audit/${encodeURIComponent(args.address)}`);
if (res.status === 402) {
return JSON.stringify({
status: "PAYMENT_REQUIRED",
message: "Payment required. Authenticate with an M2M Sentinel API key or settle the x402 challenge.",
notASafetyGuarantee: true,
});
}
if (!res.ok) {
const errBody = await res.json().catch(() => ({}));
return JSON.stringify({
status: "ERROR",
statusCode: res.status,
message: errBody.message || `Audit request failed with HTTP ${res.status}`,
notASafetyGuarantee: true,
});
}
const data = await res.json();
return JSON.stringify({
status: "SUCCESS",
data,
notASafetyGuarantee: true,
});
} catch (err) {
return JSON.stringify({
status: "ERROR",
message: err instanceof Error ? err.message : String(err),
notASafetyGuarantee: true,
});
}
}

/**
* Retrieves real-time Base network gas execution metrics.
*
* @param _wallet - The wallet provider instance
* @param _args - Empty arguments
* @returns Base network gas recommendations
*/
@CreateAction({
name: "get_gas_metrics",
description: "Get real-time Base network gas execution metrics and fee recommendations before submitting transactions.",
schema: GetGasMetricsSchema,
})
async getGasMetrics(
_wallet: EvmWalletProvider,
_args: z.infer<typeof GetGasMetricsSchema>,
): Promise<string> {
try {
const res = await this.fetchApi("/v1/gas/fees");
if (!res.ok) {
return JSON.stringify({ status: "ERROR", statusCode: res.status, message: "Failed to retrieve gas fees" });
}
return JSON.stringify(await res.json());
} catch (err) {
return JSON.stringify({ status: "ERROR", message: err instanceof Error ? err.message : String(err) });
}
}

/**
* Observes real-time Base DEX token price for slippage check and preflight valuation.
*
* @param _wallet - The wallet provider instance
* @param args - Token symbol argument
* @returns Token price observation
*/
@CreateAction({
name: "get_token_price",
description: "Observe real-time Base DEX token price for slippage verification and valuation.",
schema: GetTokenPriceSchema,
})
async getTokenPrice(
_wallet: EvmWalletProvider,
args: z.infer<typeof GetTokenPriceSchema>,
): Promise<string> {
try {
const res = await this.fetchApi(`/v1/token/price/${encodeURIComponent(args.symbol.toUpperCase())}`);
if (!res.ok) {
return JSON.stringify({ status: "ERROR", statusCode: res.status, message: `Failed to retrieve price for ${args.symbol}` });
}
return JSON.stringify(await res.json());
} catch (err) {
return JSON.stringify({ status: "ERROR", message: err instanceof Error ? err.message : String(err) });
}
}

/**
* Checks operational status of M2M Sentinel verification rails.
*
* @param _wallet - The wallet provider instance
* @param _args - Empty arguments
* @returns Service status response
*/
@CreateAction({
name: "get_service_status",
description: "Check operational status of M2M Sentinel upstream verification rails and quorum.",
schema: GetServiceStatusSchema,
})
async getServiceStatus(
_wallet: EvmWalletProvider,
_args: z.infer<typeof GetServiceStatusSchema>,
): Promise<string> {
try {
const res = await this.fetchApi("/v1/status");
if (!res.ok) {
return JSON.stringify({ status: "UNAVAILABLE", statusCode: res.status });
}
return JSON.stringify(await res.json());
} catch (err) {
return JSON.stringify({ status: "UNAVAILABLE", message: err instanceof Error ? err.message : String(err) });
}
}
}

export const m2mSentinelActionProvider = (config?: M2MSentinelConfig) =>
new M2MSentinelActionProvider(config);
55 changes: 55 additions & 0 deletions typescript/agentkit/src/action-providers/m2mSentinel/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { z } from "zod";

/**
* Input schema for auditing a smart contract on Base.
*/
export const AuditContractSchema = z
.object({
address: z
.string()
.regex(/^0x[a-fA-F0-9]{40}$/, "Invalid contract address format (must be 0x followed by 40 hex characters)")
.describe("Target Base mainnet smart contract address to inspect"),
})
.strip()
.describe("Instructions for auditing a contract on Base");

/**
* Input schema for getting real-time Base network gas execution metrics.
*/
export const GetGasMetricsSchema = z
.object({})
.strip()
.describe("Instructions for getting Base network gas metrics");

/**
* Input schema for retrieving Base DEX token prices.
*/
export const GetTokenPriceSchema = z
.object({
symbol: z
.string()
.min(1, "Token symbol is required")
.describe("Token symbol on Base (e.g. USDC, WETH, cbBTC)"),
})
.strip()
.describe("Instructions for retrieving Base token prices");

/**
* Input schema for retrieving M2M Sentinel service status.
*/
export const GetServiceStatusSchema = z
.object({})
.strip()
.describe("Instructions for retrieving M2M Sentinel service operational status");

/**
* Configuration options for M2MSentinelActionProvider.
*/
export const M2MSentinelConfigSchema = z
.object({
baseUrl: z.string().url().optional().describe("Custom M2M Sentinel base URL"),
apiKey: z.string().optional().describe("Optional M2M Sentinel API key for authenticated quota"),
})
.optional();

export type M2MSentinelConfig = z.infer<typeof M2MSentinelConfigSchema>;
Loading