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
2 changes: 2 additions & 0 deletions typescript/agentkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Patch Changes

- Added `offrampActionProvider` wrapping Galleon / USDCtoFiat `@usdctofiat/offramp` cashout fast|best for Base mainnet. Fast stays Fast (0% TOFIAT). Best is delegated 10 bps.

- [#966](https://github.com/coinbase/agentkit/pull/966) [`b211701`](https://github.com/coinbase/agentkit/commit/b21170143825cb1892daaa8e52c68e9c8c446ae1) Thanks [@phdargen](https://github.com/phdargen)! - Bumped x402 packages and fix missing readContract interface

- [#982](https://github.com/coinbase/agentkit/pull/982) [`c3dbef6`](https://github.com/coinbase/agentkit/commit/c3dbef60d1613effc9d9805816bec15f5510fdca) Thanks [@fffilimonov](https://github.com/fffilimonov)! - Added dTelecom action provider for decentralized voice services (WebRTC, STT, TTS) with x402 micropayments, and a voice agent example.
Expand Down
3 changes: 2 additions & 1 deletion typescript/agentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
"sushi": "6.2.1",
"twitter-api-v2": "^1.18.2",
"viem": "2.47.4",
"zod": "^4.3.6"
"zod": "^4.3.6",
"@usdctofiat/offramp": "7.0.1"
},
"devDependencies": {
"@types/jest": "^29.5.14",
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 @@ -34,6 +34,7 @@ export * from "./wow";
export * from "./allora";
export * from "./flaunch";
export * from "./onramp";
export * from "./offramp";
export * from "./vaultsfyi";
export * from "./x402";
export * from "./yelay";
Expand Down
28 changes: 28 additions & 0 deletions typescript/agentkit/src/action-providers/offramp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Offramp Action Provider

Native AgentKit wrapper around [`@usdctofiat/offramp`](https://www.npmjs.com/package/@usdctofiat/offramp) from [Galleon / USDCtoFiat](https://usdctofiat.xyz).

This provider does not republish the SDK. It calls `cashout({ mode, signer, amount, currency, platform, payee })`.

## Modes

- **fast** — Peer Cash at the live market rate, 0% spread. Galleon earns the locked `TOFIAT` Curator referral. Do not force Fast onto Delegate.
- **best** — deposit is delegated to the Delegate strategy. Galleon earns 10 bps on fill.

Attribution (`peer-ref-TOFIAT` and `galleonlabs`) is locked by the SDK and cannot be replaced.

## Network

Base mainnet only.

## Usage

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

const agentkit = await Agentkit.from({
actionProviders: [offrampActionProvider()],
});
```

Product docs: https://usdctofiat.xyz/developers
8 changes: 8 additions & 0 deletions typescript/agentkit/src/action-providers/offramp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Exports for offramp action provider (Galleon / USDCtoFiat).
*
* @module offramp
*/

export * from "./offrampActionProvider";
export * from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { OfframpActionProvider } from "./offrampActionProvider";
import { Network } from "../../network";
import { CashoutActionSchema } from "./schemas";
import { EvmWalletProvider } from "../../wallet-providers";

const mockCashout = jest.fn();

jest.mock("@usdctofiat/offramp", () => ({
cashout: (...args: unknown[]) => mockCashout(...args),
}));

jest.mock("viem", () => {
const actual = jest.requireActual("viem");
return {
...actual,
createWalletClient: jest.fn(() => ({ mocked: true })),
};
});

describe("OfframpActionProvider", () => {
const provider = new OfframpActionProvider();
let mockWalletProvider: jest.Mocked<EvmWalletProvider>;

beforeEach(() => {
mockCashout.mockReset();
mockCashout.mockResolvedValue({
mode: "fast",
depositId: "1",
});
mockWalletProvider = {
getAddress: jest.fn().mockReturnValue("0x123"),
getNetwork: jest.fn().mockReturnValue({
protocolFamily: "evm",
networkId: "base-mainnet",
chainId: "8453",
}),
toSigner: jest.fn().mockReturnValue({ address: "0x123" }),
toEip1193Provider: jest.fn().mockReturnValue({ request: jest.fn() }),
} as unknown as jest.Mocked<EvmWalletProvider>;
});

describe("network support", () => {
it("should support Base mainnet", () => {
expect(
provider.supportsNetwork({
networkId: "base-mainnet",
protocolFamily: "evm",
}),
).toBe(true);
});

it("should not support Base testnet", () => {
expect(
provider.supportsNetwork({
networkId: "base-sepolia",
protocolFamily: "evm",
}),
).toBe(false);
});

it("should not support other protocol families", () => {
expect(
provider.supportsNetwork({
protocolFamily: "other-protocol-family",
}),
).toBe(false);
});

it("should handle invalid network objects", () => {
expect(provider.supportsNetwork({} as Network)).toBe(false);
});
});

describe("action validation", () => {
it("should accept fast and best cashout input", () => {
const base = {
amount: "100",
currency: "EUR",
platform: "revolut",
payee: "alice",
};
expect(CashoutActionSchema.safeParse({ ...base, mode: "fast" }).success).toBe(true);
expect(CashoutActionSchema.safeParse({ ...base, mode: "best" }).success).toBe(true);
});

it("should reject a missing mode", () => {
const parseResult = CashoutActionSchema.safeParse({
amount: "100",
currency: "EUR",
platform: "revolut",
payee: "alice",
});
expect(parseResult.success).toBe(false);
});
});

describe("cashout", () => {
it("should wrap cashout in fast mode", async () => {
const result = await provider.cashout(mockWalletProvider, {
mode: "fast",
amount: "100",
currency: "EUR",
platform: "revolut",
payee: "alice",
});

expect(mockCashout).toHaveBeenCalledWith(
expect.objectContaining({
mode: "fast",
amount: "100",
currency: "EUR",
platform: "revolut",
payee: "alice",
}),
);
expect(JSON.parse(result)).toEqual({ mode: "fast", depositId: "1" });
});

it("should wrap cashout in best mode without forcing Delegate on fast", async () => {
mockCashout.mockResolvedValue({ mode: "best", depositId: "2" });
const result = await provider.cashout(mockWalletProvider, {
mode: "best",
amount: "50",
currency: "USD",
platform: "venmo",
payee: "bob",
});
expect(mockCashout).toHaveBeenCalledWith(expect.objectContaining({ mode: "best" }));
expect(JSON.parse(result).mode).toBe("best");
});

it("should throw when network ID is not set", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: undefined,
});
await expect(
provider.cashout(mockWalletProvider, {
mode: "fast",
amount: "100",
currency: "EUR",
platform: "revolut",
payee: "alice",
}),
).rejects.toThrow("Network ID is not set");
expect(mockCashout).not.toHaveBeenCalled();
});

it("should throw for unsupported networks", async () => {
mockWalletProvider.getNetwork.mockReturnValue({
protocolFamily: "evm",
networkId: "ethereum-mainnet",
});
await expect(
provider.cashout(mockWalletProvider, {
mode: "fast",
amount: "100",
currency: "EUR",
platform: "revolut",
payee: "alice",
}),
).rejects.toThrow("Base mainnet only");
expect(mockCashout).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Offramp Action Provider
*
* Native AgentKit wrapper around @usdctofiat/offramp (Galleon / USDCtoFiat).
*
* @module offramp
*/

import { createWalletClient, custom } from "viem";
import { base } from "viem/chains";
import { z } from "zod";
import { cashout } from "@usdctofiat/offramp";
import { ActionProvider } from "../actionProvider";
import { Network } from "../../network";
import { CreateAction } from "../actionDecorator";
import { EvmWalletProvider } from "../../wallet-providers";
import { CashoutActionSchema } from "./schemas";

const BASE_MAINNET = "base-mainnet";

function toViemWalletClient(walletProvider: EvmWalletProvider) {
return createWalletClient({
account: walletProvider.toSigner(),
chain: base,
transport: custom(walletProvider.toEip1193Provider()),
});
}

/**
* OfframpActionProvider exposes USDCtoFiat cash-out (Fast and Best) via @usdctofiat/offramp.
*/
export class OfframpActionProvider extends ActionProvider<EvmWalletProvider> {
constructor() {
super("offramp", []);
}

/**
* Sell Base USDC for fiat through Galleon / USDCtoFiat (@usdctofiat/offramp).
*/
@CreateAction({
name: "cashout",
description: `
Sell Base USDC for fiat using Galleon USDCtoFiat (@usdctofiat/offramp on https://usdctofiat.xyz).
This is a native wrapper around cashout({ mode, signer, amount, currency, platform, payee }).
Attribution (peer-ref-TOFIAT and galleonlabs) is locked by the SDK and cannot be replaced.

mode "fast": Peer Cash at the live market rate, 0% spread. Galleon earns the TOFIAT referral. Do not force this onto Delegate.
mode "best": deposit is delegated to the Delegate strategy; Galleon earns 10 bps on fill.

Use this when the user wants to cash out USDC to an eligible payment rail (for example Revolut or Venmo).
Do not use this to buy crypto (use get_onramp_buy_url). Do not invent a sandbox.
`,
schema: CashoutActionSchema,
})
async cashout(
walletProvider: EvmWalletProvider,
args: z.infer<typeof CashoutActionSchema>,
): Promise<string> {
const networkId = walletProvider.getNetwork().networkId;
if (!networkId) {
throw new Error("Network ID is not set");
}
if (networkId !== BASE_MAINNET) {
throw new Error(
"USDCtoFiat cashout is Base mainnet only. Switch the wallet to base-mainnet.",
);
}

const result = await cashout({
mode: args.mode,
signer: toViemWalletClient(walletProvider),
amount: args.amount,
currency: args.currency,
platform: args.platform,
payee: args.payee,
} as Parameters<typeof cashout>[0]);

return JSON.stringify(result);
}

/**
* Base mainnet only. USDCtoFiat cash-out is Base USDC.
*/
supportsNetwork(network: Network): boolean {
return network.protocolFamily === "evm" && network.networkId === BASE_MAINNET;
}
}

/**
* Factory for OfframpActionProvider.
*/
export const offrampActionProvider = () => new OfframpActionProvider();
29 changes: 29 additions & 0 deletions typescript/agentkit/src/action-providers/offramp/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from "zod";

/**
* Action schemas for the USDCtoFiat / Galleon offramp action provider.
*/

export const CashoutActionSchema = z.object({
mode: z
.enum(["fast", "best"])
.describe(
"fast: live market pricing with 0% spread. best: Delegate-managed pricing with a 10 bps fee.",
),
amount: z
.string()
.min(1)
.describe("Human USDC amount to sell, for example \"100\"."),
currency: z
.string()
.min(1)
.describe("Fiat currency code, for example USD, EUR, or GBP."),
platform: z
.string()
.min(1)
.describe("Payment platform id from the SDK, for example revolut or venmo."),
payee: z
.string()
.min(1)
.describe("Payout identifier on that platform, for example a Revolut username."),
});
Loading
Loading