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

Add PayCrow action provider for trust-informed escrow payments
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 @@ -21,6 +21,7 @@ export * from "./pyth";
export * from "./moonwell";
export * from "./morpho";
export * from "./opensea";
export * from "./paycrow";
export * from "./spl";
export * from "./superfluid";
export * from "./sushi";
Expand Down
42 changes: 42 additions & 0 deletions typescript/agentkit/src/action-providers/paycrow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PayCrow Action Provider

The PayCrow action provider integrates [PayCrow](https://github.com/michu5696/paycrow) trust-informed escrow payments into AgentKit. PayCrow is a trust layer for agent-to-agent payments that protects buyers by checking seller reputation before paying and holding funds in escrow until services are delivered.

## Actions

### trust_gate

Check an agent or seller's trust score before making a payment. Returns a decision (`proceed`, `caution`, or `block`), a recommended timelock duration, and a maximum recommended payment amount.

### safe_pay

Make a trust-informed escrow payment. Combines trust checking and escrow creation into a single action. The escrow protects the buyer by holding funds until the service is delivered.

### escrow_create

Create a USDC escrow with a configurable timelock. Funds are held until service delivery is confirmed or the timelock expires.

### rate_service

Rate a completed escrow (1-5 stars). Ratings contribute to the seller's on-chain trust score, which affects future `trust_gate` decisions.

## Usage

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

const agent = new AgentKit({
actionProviders: [paycrowActionProvider()],
});
```

## Network Support

PayCrow's trust API is network-agnostic and works across all networks.

## Links

- [PayCrow GitHub](https://github.com/michu5696/paycrow)
- [PayCrow npm](https://www.npmjs.com/package/paycrow)
- [Trust API](https://paycrow-app.fly.dev)
2 changes: 2 additions & 0 deletions typescript/agentkit/src/action-providers/paycrow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./paycrowActionProvider";
export * from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { PayCrowActionProvider } from "./paycrowActionProvider";

// Mock global fetch
const mockFetch = jest.fn();
global.fetch = mockFetch;

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

beforeEach(() => {
provider = new PayCrowActionProvider();
mockFetch.mockReset();
});

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

describe("supportsNetwork", () => {
it("should return true for any network", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(provider.supportsNetwork({} as any)).toBe(true);
});
});

describe("trustGate", () => {
it("should return trust score data on success", async () => {
const mockResponse = {
decision: "proceed",
recommended_timelock: 30,
max_amount: 100,
score: 0.85,
};

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});

const result = await provider.trustGate({ address: "0x1234" });
const parsed = JSON.parse(result);

expect(parsed.success).toBe(true);
expect(parsed.address).toBe("0x1234");
expect(parsed.decision).toBe("proceed");
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/api/trust?address=0x1234"),
);
});

it("should include intendedAmount in query params when provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ decision: "caution" }),
});

await provider.trustGate({ address: "0x1234", intendedAmount: 50 });

expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("amount=50"),
);
});

it("should handle API errors gracefully", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
});

const result = await provider.trustGate({ address: "0x1234" });
const parsed = JSON.parse(result);

expect(parsed.success).toBe(false);
expect(parsed.error).toContain("HTTP 500");
});

it("should handle network errors gracefully", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network error"));

const result = await provider.trustGate({ address: "0x1234" });
const parsed = JSON.parse(result);

expect(parsed.success).toBe(false);
expect(parsed.error).toContain("Network error");
});
});

describe("safePay", () => {
it("should execute a safe payment on success", async () => {
const mockResponse = {
status: "released",
escrowId: "esc_123",
txHash: "0xabc",
};

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});

const result = await provider.safePay({
url: "https://api.example.com/service",
sellerAddress: "0x5678",
amountUsdc: 10,
});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(true);
expect(parsed.status).toBe("released");
expect(parsed.escrowId).toBe("esc_123");
});

it("should handle API errors gracefully", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: async () => "Invalid seller address",
});

const result = await provider.safePay({
url: "https://api.example.com/service",
sellerAddress: "invalid",
amountUsdc: 10,
});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(false);
expect(parsed.error).toContain("Invalid seller address");
});
});

describe("escrowCreate", () => {
it("should create an escrow on success", async () => {
const mockResponse = {
escrowId: "esc_456",
txHash: "0xdef",
};

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});

const result = await provider.escrowCreate({
seller: "0x5678",
amountUsdc: 25,
timelockMinutes: 120,
serviceUrl: "https://api.example.com/data",
});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(true);
expect(parsed.escrowId).toBe("esc_456");
expect(parsed.txHash).toBe("0xdef");

const fetchBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(fetchBody.seller).toBe("0x5678");
expect(fetchBody.amount_usdc).toBe(25);
expect(fetchBody.timelock_minutes).toBe(120);
expect(fetchBody.service_url).toBe("https://api.example.com/data");
});

it("should handle API errors gracefully", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => "Internal server error",
});

const result = await provider.escrowCreate({
seller: "0x5678",
amountUsdc: 25,
timelockMinutes: 60,
serviceUrl: "https://api.example.com/data",
});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(false);
expect(parsed.error).toContain("HTTP 500");
});
});

describe("rateService", () => {
it("should submit a rating on success", async () => {
const mockResponse = {
rated: true,
newScore: 0.9,
};

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});

const result = await provider.rateService({
escrowId: "esc_123",
stars: 5,
});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(true);
expect(parsed.rated).toBe(true);

const fetchBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(fetchBody.escrow_id).toBe("esc_123");
expect(fetchBody.stars).toBe(5);
});

it("should handle API errors gracefully", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: async () => "Escrow not found",
});

const result = await provider.rateService({
escrowId: "esc_invalid",
stars: 3,
});
const parsed = JSON.parse(result);

expect(parsed.success).toBe(false);
expect(parsed.error).toContain("Escrow not found");
});
});
});
Loading
Loading