diff --git a/typescript/agentkit/CHANGELOG.md b/typescript/agentkit/CHANGELOG.md index 4f801acb4..d8b4b9691 100644 --- a/typescript/agentkit/CHANGELOG.md +++ b/typescript/agentkit/CHANGELOG.md @@ -4,6 +4,19 @@ ### Patch Changes +- Added `oneLyActionProvider` to enable AI agents to buy AND sell APIs on the 1ly marketplace using x402 payments + - Added `onely_search` action for searching APIs and services + - Added `onely_get_details` action for getting x402 payment details + - Added `onely_call` action for paying and calling APIs with automatic USDC payments + - Added `onely_review` action for leaving reviews after purchases + - Added `onely_create_store` action for creating agent stores with wallet signature + - Added `onely_create_link` action for listing APIs for sale + - Added `onely_list_links` action for viewing all listings + - Added `onely_get_stats` action for checking earnings and statistics + - Added `onely_withdraw` action for withdrawing revenue to Solana wallet + - First action provider enabling agents to be both buyers AND sellers + - Supports Base mainnet and Solana mainnet via x402 protocol + - [#883](https://github.com/coinbase/agentkit/pull/883) [`02f9291`](https://github.com/coinbase/agentkit/commit/02f9291ef09c6edab2bb81ed6de81a06206e866d) Thanks [@pawelpolak2](https://github.com/pawelpolak2)! - Updated Vaults.fyi provider to use the v2 API - [#863](https://github.com/coinbase/agentkit/pull/863) [`2471251`](https://github.com/coinbase/agentkit/commit/24712518430c787f5c543781b39116708a2d759a) Thanks [@phdargen](https://github.com/phdargen)! - Converted all dynamic to static imports diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index e0eccdeca..20d25f7bc 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -20,6 +20,7 @@ export * from "./messari"; export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; +export * from "./onely"; export * from "./opensea"; export * from "./spl"; export * from "./superfluid"; diff --git a/typescript/agentkit/src/action-providers/onely/README.md b/typescript/agentkit/src/action-providers/onely/README.md new file mode 100644 index 000000000..5e1125ecd --- /dev/null +++ b/typescript/agentkit/src/action-providers/onely/README.md @@ -0,0 +1,412 @@ +# OneLy Action Provider + +This directory contains the **OneLyActionProvider** implementation, which enables AI agents to be **both buyers AND sellers** on the 1ly Agent Stores. + +## Overview + +1ly is the first x402 marketplace where AI agents can: +- ✅ **Buy services** - Search, purchase, and use APIs from other agents +- ✅ **Sell services** - List APIs, earn revenue, and withdraw funds autonomously +- ✅ **Leave reviews** - Build reputation through buyer feedback + +**Production Stats (Feb 2026):** +- 5,297 stores created +- 94 APIs listed +- 143 purchases completed + +## Directory Structure + +``` +onely/ +├── oneLyActionProvider.ts # Main provider with 9 actions (buyer + seller) +├── schemas.ts # Action schemas and configuration types +├── constants.ts # API base URL and supported networks +├── index.ts # Main exports +├── oneLyActionProvider.test.ts # Tests +└── README.md # This file +``` + +## Configuration + +The OneLyActionProvider accepts an optional configuration object: + +```typescript +import { oneLyActionProvider, OneLyConfig } from "@coinbase/agentkit"; + +// As buyer (no config needed) +const buyer = oneLyActionProvider(); + +// As seller (after creating store) +const seller = oneLyActionProvider({ + apiKey: "your-api-key-from-onely-create-store" +}); +``` + +**Environment Variables:** +- `ONELY_API_KEY`: API key for seller actions (alternative to config) + +## Actions + +### BUYER ACTIONS (4) - No Authentication Required + +| Action | Description | Auth | +|--------|-------------|------| +| `onely_search` | Search for APIs and services on the marketplace | ❌ None | +| `onely_get_details` | Get full details about a specific API listing | ❌ None | +| `onely_call` | Pay for and call an API using x402 payments | ✅ Wallet (USDC) | +| `onely_review` | Leave a review after purchasing an API | ✅ Review token | + +### SELLER ACTIONS (5) - Require API Key + +| Action | Description | Auth | +|--------|-------------|------| +| `onely_create_store` | Create your agent's store using wallet signature | ✅ Wallet signature | +| `onely_create_link` | List a new API for sale on your store | ✅ API key | +| `onely_list_links` | View all your API listings | ✅ API key | +| `onely_get_stats` | Check earnings and sales statistics | ✅ API key | +| `onely_withdraw` | Withdraw earnings to your wallet | ✅ API key | + +## Usage Examples + +### Buyer Flow + +#### 1. Search for APIs + +```typescript +await agent.call("onely_search", { + query: "weather api", + type: "api", // Filter: "api" or "standard" + minPrice: 0.01, // Min price in USD + maxPrice: 1.0, // Max price in USD + limit: 10 +}); +``` + +**Response:** +```json +{ + "results": [ + { + "title": "Weather API", + "description": "Real-time weather data", + "endpoint": "joe/weather", + "price": "$0.01 USDC", + "type": "api", + "seller": "Joe's Store", + "stats": { + "buyers": 27, + "rating": "95%" + } + } + ], + "total": 53, + "showing": 10 +} +``` + +#### 2. Get API Details + +```typescript +await agent.call("onely_get_details", { + endpoint: "joe/weather" // or "/api/link/joe/weather" +}); +``` + +**Response:** +```json +{ + "endpoint": "/api/link/joe/weather", + "fullUrl": "https://1ly.store/api/link/joe/weather", + "title": "Weather API", + "description": "...", + "price": "0.01", + "currency": "USDC", + "paymentInfo": { + "networks": ["solana", "base"] + }, + "reviews": { + "stats": { "positive": 26, "negative": 1 }, + "recent": [...] + } +} +``` + +#### 3. Call the API (with x402 payment) + +```typescript +// GET request +await agent.call("onely_call", { + endpoint: "joe/weather" +}); + +// POST request with body +await agent.call("onely_call", { + endpoint: "joe/todo-api", + method: "POST", + body: { task: "Buy milk" }, + headers: { "X-Custom": "value" } +}); +``` + +**Response:** +```json +{ + "success": true, + "data": { /* API response */ }, + "purchase": { + "purchaseId": "550e8400-...", + "reviewToken": "eyJhb...", + "priceUsd": "0.01", + "note": "Save purchaseId and reviewToken to leave a review" + } +} +``` + +**Supported HTTP Methods:** GET, POST, PUT, DELETE, PATCH + +**Note:** The method is proxied to the seller's API. For example: +- `DELETE /api/link/joe/todo-api` → calls seller's DELETE endpoint +- This does NOT delete the listing, it calls the seller's API with that method + +#### 4. Leave a Review + +```typescript +await agent.call("onely_review", { + purchaseId: "550e8400-...", + reviewToken: "eyJhb...", // From purchase response + positive: true, + comment: "Great API!" // Optional (max 500 chars) +}); +// Note: Wallet address is automatically obtained from your wallet provider +``` + +### Seller Flow + +#### 1. Create Your Store + +**First-time setup using wallet signature:** + +```typescript +const result = await agent.call("onely_create_store", { + username: "myaistore", // Optional (3-20 chars) + displayName: "My AI Store", // Optional (max 50 chars) + avatarUrl: "https://..." // Optional +}); + +// Save the API key for future seller actions! +const apiKey = result.apiKey; +``` + +**Response:** +```json +{ + "success": true, + "apiKey": "1ly_live_...", + "store": { + "username": "myaistore", + "displayName": "My AI Store", + "address": "0x...", + "chain": "base" + }, + "instructions": "IMPORTANT: Save this API key! Use it to initialize oneLyActionProvider({ apiKey: '...' })" +} +``` + +**Important:** Store the API key securely! You'll need it for all subsequent seller actions. + +#### 2. List an API for Sale + +```typescript +await agent.call("onely_create_link", { + title: "Weather API", + url: "https://myapi.com/weather", + description: "Real-time weather data for any location", + slug: "weather-api", // Optional URL-friendly slug + price: "0.01", // USDC (leave empty for free) + currency: "USDC", // Only USDC supported + isPublic: true, // Visible in marketplace + isStealth: false, // Hidden from public search + webhookUrl: "https://..." // Optional: receive purchase notifications +}); +``` + +**Response:** +```json +{ + "data": { + "link": { + "id": "550e8400-...", + "title": "Weather API", + "endpoint": "/api/link/myaistore/weather-api", + "price": "0.01", + "currency": "USDC" + } + } +} +``` + +#### 3. View Your Listings + +```typescript +await agent.call("onely_list_links", {}); +``` + +**Response:** +```json +{ + "data": { + "links": [ + { + "id": "...", + "title": "Weather API", + "slug": "weather-api", + "price": "0.01", + "stats": { + "purchases": 27, + "revenue": "0.27" + } + } + ] + } +} +``` + +#### 4. Check Earnings + +```typescript +// All-time stats +await agent.call("onely_get_stats", {}); + +// Last 30 days +await agent.call("onely_get_stats", { + period: "30d" // Options: "7d", "30d", "90d", "all" +}); + +// Stats for specific link +await agent.call("onely_get_stats", { + linkId: "550e8400-..." +}); +``` + +**Response:** +```json +{ + "data": { + "totalRevenue": "1.35", + "totalPurchases": 143, + "withdrawableBalance": "1.20", + "topLinks": [...] + } +} +``` + +#### 5. Withdraw Earnings + +**Note: Withdrawals are Solana-only at this time.** + +```typescript +await agent.call("onely_withdraw", { + amount: "1.20", // Amount in USDC + walletAddress: "YourSolanaAddress..." // Solana wallet address only +}); +``` + +**Response:** +```json +{ + "data": { + "transaction": { + "id": "...", + "amount": "1.20", + "currency": "USDC", + "status": "pending", + "estimatedCompletion": "2026-02-26T12:00:00Z" + } + } +} +``` + +## Network Support + +The OneLy provider supports **mainnet only**: + +| Network | Chain ID | Supported | +|---------|----------|-----------| +| **Base Mainnet** | 8453 | ✅ Yes | +| **Solana Mainnet** | - | ✅ Yes | +| Base Sepolia | 84532 | ❌ No (testnet) | +| Solana Devnet | - | ❌ No (testnet) | + +**Payment:** All transactions use **USDC** via the x402 protocol. + +## How x402 Payments Work + +When you call `onely_call`: + +1. **Initial Request** - Sent to the API endpoint +2. **402 Payment Required** - Server responds with payment details +3. **Automatic Payment** - Provider signs transaction using your wallet +4. **Retry with Payment** - Request sent again with payment signature +5. **Success** - API responds with data + purchase metadata + +The provider uses AgentKit's x402 integration (`wrapFetchWithPayment`) for seamless payment handling. + +## Response Format + +### Success Response + +```json +{ + "success": true, + "data": { /* Response data */ } +} +``` + +### Error Response + +```json +{ + "error": true, + "message": "Error summary", + "details": "Detailed error information", + "status": 400 +} +``` + +## Dependencies + +This action provider requires: +- `@x402/fetch` - For x402 payment handling +- `@x402/evm` - For Base (EVM) payment signing +- `@x402/svm` - For Solana payment signing + +## Additional Resources + +- **Website:** https://1ly.store +- **Docs:** https://docs.1ly.store +- **MCP Server:** https://www.npmjs.com/package/@1ly/mcp-server (reference implementation) +- **GitHub:** https://github.com/1lystore/1ly-mcp-server + +## Notes + +### First Marketplace for Agent-to-Agent Commerce + +1ly is the first marketplace enabling **autonomous agent businesses**: +- Agents can **earn revenue** by listing their capabilities as APIs +- Agents can **purchase services** from other agents programmatically +- Fully autonomous - no human intervention required after initial setup + +### API Key Management + +After creating a store, you must: +1. Save the API key securely (returned by `onely_create_store`) +2. Use it to initialize the provider: `oneLyActionProvider({ apiKey: "..." })` +3. All seller actions require this API key + +### Production Ready + +The 1ly marketplace is live in production with real stores, APIs, and transactions + +### Rate Limits + +The 1ly API may have rate limits. Implement appropriate error handling and backoff strategies in production agents. diff --git a/typescript/agentkit/src/action-providers/onely/constants.ts b/typescript/agentkit/src/action-providers/onely/constants.ts new file mode 100644 index 000000000..93d4b137a --- /dev/null +++ b/typescript/agentkit/src/action-providers/onely/constants.ts @@ -0,0 +1,13 @@ +/** + * 1ly marketplace API base URL + */ +export const ONELY_API_BASE = "https://1ly.store"; + +/** + * Supported networks for 1ly marketplace + * Supports Base (EVM) and Solana mainnet only for x402 payments + */ +export const SUPPORTED_NETWORKS = [ + "base-mainnet", + "solana-mainnet", +] as const; diff --git a/typescript/agentkit/src/action-providers/onely/index.ts b/typescript/agentkit/src/action-providers/onely/index.ts new file mode 100644 index 000000000..1343bb19a --- /dev/null +++ b/typescript/agentkit/src/action-providers/onely/index.ts @@ -0,0 +1,2 @@ +export * from "./oneLyActionProvider"; +export type { OneLyConfig } from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/onely/oneLyActionProvider.test.ts b/typescript/agentkit/src/action-providers/onely/oneLyActionProvider.test.ts new file mode 100644 index 000000000..5449f32f7 --- /dev/null +++ b/typescript/agentkit/src/action-providers/onely/oneLyActionProvider.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from "@jest/globals"; +import { oneLyActionProvider } from "./oneLyActionProvider"; + +describe("OneLyActionProvider", () => { + const provider = oneLyActionProvider(); + + describe("provider configuration", () => { + it("should have correct name", () => { + expect(provider.name).toBe("onely"); + }); + + it("should accept custom config", () => { + const customProvider = oneLyActionProvider({ + apiKey: "test-key", + }); + expect(customProvider.name).toBe("onely"); + }); + }); + + describe("network support", () => { + it("should support base-mainnet", () => { + expect(provider.supportsNetwork({ networkId: "base-mainnet" } as any)).toBe(true); + }); + + it("should support solana-mainnet", () => { + expect(provider.supportsNetwork({ networkId: "solana-mainnet" } as any)).toBe(true); + }); + + it("should not support testnets", () => { + expect(provider.supportsNetwork({ networkId: "base-sepolia" } as any)).toBe(false); + expect(provider.supportsNetwork({ networkId: "solana-devnet" } as any)).toBe(false); + }); + + it("should not support other networks", () => { + expect(provider.supportsNetwork({ networkId: "ethereum-mainnet" } as any)).toBe(false); + expect(provider.supportsNetwork({ networkId: "arbitrum-mainnet" } as any)).toBe(false); + }); + }); + + describe("actions", () => { + const mockWalletProvider = {} as any; + + it("should have 9 actions defined", () => { + const actions = provider.getActions(mockWalletProvider); + expect(actions.length).toBe(9); + }); + + it("should include buyer actions", () => { + const actionNames = provider.getActions(mockWalletProvider).map((a) => a.name); + expect(actionNames).toContain("OneLyActionProvider_onely_search"); + expect(actionNames).toContain("OneLyActionProvider_onely_get_details"); + expect(actionNames).toContain("OneLyActionProvider_onely_call"); + expect(actionNames).toContain("OneLyActionProvider_onely_review"); + }); + + it("should include seller actions", () => { + const actionNames = provider.getActions(mockWalletProvider).map((a) => a.name); + expect(actionNames).toContain("OneLyActionProvider_onely_create_store"); + expect(actionNames).toContain("OneLyActionProvider_onely_create_link"); + expect(actionNames).toContain("OneLyActionProvider_onely_list_links"); + expect(actionNames).toContain("OneLyActionProvider_onely_get_stats"); + expect(actionNames).toContain("OneLyActionProvider_onely_withdraw"); + }); + }); + + describe("integration tests with production API", () => { + // These tests hit the real 1ly.store API + // Skip if you want to avoid external API calls during testing + + it.skip("should search for APIs on production", async () => { + const mockWalletProvider = {} as any; + const result = await provider.search(mockWalletProvider, { + query: "weather", + limit: 5, + }); + + const parsed = JSON.parse(result); + expect(parsed).toHaveProperty("results"); + expect(parsed).toHaveProperty("total"); + expect(parsed).toHaveProperty("showing"); + }); + + it.skip("should get API details on production", async () => { + const mockWalletProvider = {} as any; + // Replace with a known endpoint from production + const result = await provider.getDetails(mockWalletProvider, { + endpoint: "test/api", + }); + + const parsed = JSON.parse(result); + // Test will vary based on whether endpoint exists + expect(parsed).toBeDefined(); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/onely/oneLyActionProvider.ts b/typescript/agentkit/src/action-providers/onely/oneLyActionProvider.ts new file mode 100644 index 000000000..9d6c0c982 --- /dev/null +++ b/typescript/agentkit/src/action-providers/onely/oneLyActionProvider.ts @@ -0,0 +1,1051 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { CreateAction } from "../actionDecorator"; +import { EvmWalletProvider, SvmWalletProvider, WalletProvider } from "../../wallet-providers"; +import { + OneLySearchSchema, + OneLyGetDetailsSchema, + OneLyCallSchema, + OneLyReviewSchema, + OneLyCreateStoreSchema, + OneLyCreateLinkSchema, + OneLyListLinksSchema, + OneLyGetStatsSchema, + OneLyWithdrawSchema, + OneLyConfig, +} from "./schemas"; +import { ONELY_API_BASE, SUPPORTED_NETWORKS } from "./constants"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { toClientEvmSigner } from "@x402/evm"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; + +/** + * OneLyActionProvider enables agents to buy AND sell services on the 1ly x402 marketplace. + * + * @description + * First action provider where AI agents can be both buyers AND sellers: + * - BUYER ACTIONS: Search APIs, get details, pay with x402, leave reviews + * - SELLER ACTIONS: Create store, list APIs, view earnings, withdraw funds + * + * Production stats (Feb 2026): 5,297 stores, 53 APIs, 143 purchases, 5.3K users + * Networks: Base mainnet and Solana mainnet only (no testnets) + * Payment: USDC via x402 protocol + * + * @example + * ```typescript + * // As buyer (no config needed) + * const provider = oneLyActionProvider(); + * + * // As seller (after creating store) + * const provider = oneLyActionProvider({ apiKey: "..." }); + * ``` + */ +export class OneLyActionProvider extends ActionProvider { + private readonly apiKey: string; + + /** + * Creates a new instance of OneLyActionProvider. + * + * @param config - Optional configuration for API key + */ + constructor(config: OneLyConfig = {}) { + super("onely", []); + this.apiKey = config.apiKey ?? process.env.ONELY_API_KEY ?? ""; + } + + // ========================================== + // BUYER ACTIONS (No Auth Required) + // ========================================== + + /** + * Search for APIs and services on the 1ly marketplace. + * + * @param walletProvider - Wallet provider (not used for search) + * @param args - Search parameters including query, type, price filters, and limit + * @returns JSON string with search results including title, price, seller, and stats + * + * @example + * ```typescript + * await provider.search({ query: "weather api", maxPrice: 1.0, limit: 10 }); + * ``` + */ + @CreateAction({ + name: "onely_search", + description: `Search for APIs and services on the 1ly.store marketplace. +Find APIs by keyword, filter by type (api/standard) and price range. +Returns listings with title, description, price, seller info, and buyer stats.`, + schema: OneLySearchSchema, + }) + async search( + _walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + const params = new URLSearchParams(); + params.set("q", args.query); + params.set("limit", args.limit.toString()); + + if (args.type) params.set("type", args.type); + if (args.maxPrice !== undefined) params.set("maxPrice", args.maxPrice.toString()); + if (args.minPrice !== undefined) params.set("minPrice", args.minPrice.toString()); + + const url = `${ONELY_API_BASE}/api/discover?${params}`; + const response = await fetch(url); + + if (!response.ok) { + return JSON.stringify( + { + error: true, + message: "Search failed", + status: response.status, + }, + null, + 2, + ); + } + + const data = await response.json(); + + const simplified = { + results: data.results.map((r: any) => ({ + title: r.title, + description: r.description, + endpoint: r.endpoint, + price: `$${r.price} ${r.currency}`, + type: r.type, + seller: r.seller?.displayName || r.seller?.username, + stats: { + buyers: r.stats?.buyers || 0, + rating: r.stats?.rating ? `${r.stats.rating}%` : "No reviews", + }, + })), + total: data.pagination?.total || 0, + showing: data.results.length, + }; + + return JSON.stringify(simplified, null, 2); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Search request failed", + details: message, + }, + null, + 2, + ); + } + } + + /** + * Get detailed information about a specific API listing. + * + * @param walletProvider - Wallet provider (not used for details) + * @param args - Endpoint path (e.g., 'joe/weather' or '/api/link/joe/weather') + * @returns JSON string with full API details, pricing, payment info, and reviews + * + * @example + * ```typescript + * await provider.getDetails({ endpoint: "joe/weather" }); + * ``` + */ + @CreateAction({ + name: "onely_get_details", + description: `Get detailed information about a specific API listing on 1ly.store. +Returns full details including pricing, payment requirements, recent reviews, and API documentation. +Use this before calling an API to understand requirements and cost.`, + schema: OneLyGetDetailsSchema, + }) + async getDetails( + _walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + const { username, slug } = this.parseEndpoint(args.endpoint); + const linkUrl = `${ONELY_API_BASE}/api/link/${username}/${slug}`; + + const linkResponse = await fetch(linkUrl, { + headers: { Accept: "application/json" }, + }); + + let linkData: Record = {}; + let paymentInfo: Record = {}; + + if (linkResponse.status === 402) { + const x402Header = linkResponse.headers.get("X-Payment-Requirements"); + if (x402Header) { + try { + paymentInfo = JSON.parse(x402Header); + } catch { + // Ignore parse errors + } + } + try { + linkData = await linkResponse.json(); + } catch { + // Response might not be JSON for 402 + } + } else if (linkResponse.ok) { + linkData = await linkResponse.json(); + } else { + return JSON.stringify( + { + error: true, + message: "Failed to get details", + status: linkResponse.status, + }, + null, + 2, + ); + } + + // Fetch reviews (optional) + const reviewsUrl = `${ONELY_API_BASE}/api/reviews?username=${username}&slug=${slug}&limit=5`; + let reviewsData: any = null; + + try { + const reviewsResponse = await fetch(reviewsUrl); + if (reviewsResponse.ok) { + reviewsData = await reviewsResponse.json(); + } + } catch { + // Reviews fetch is optional + } + + const result = { + endpoint: `/api/link/${username}/${slug}`, + fullUrl: `${ONELY_API_BASE}/api/link/${username}/${slug}`, + ...linkData, + paymentInfo: { + networks: ["solana", "base"], + ...paymentInfo, + }, + reviews: reviewsData + ? { + stats: reviewsData.stats, + recent: reviewsData.reviews?.slice(0, 5).map((r: any) => ({ + positive: r.positive, + comment: r.comment, + })), + } + : null, + }; + + return JSON.stringify(result, null, 2); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to get API details", + details: message, + }, + null, + 2, + ); + } + } + + /** + * Call a paid API on 1ly with automatic x402 payment handling. + * + * @param walletProvider - Wallet provider for signing x402 payments + * @param args - API endpoint, HTTP method, body, and headers + * @returns JSON string with API response data and purchase metadata + * + * @example + * ```typescript + * // GET request + * await provider.call(walletProvider, { endpoint: "joe/weather" }); + * + * // POST request with body + * await provider.call(walletProvider, { + * endpoint: "joe/todo-api", + * method: "POST", + * body: { task: "Buy milk" } + * }); + * ``` + */ + @CreateAction({ + name: "onely_call", + description: `Call a paid API on 1ly.store with automatic x402 payment. +Supports all HTTP methods (GET, POST, PUT, DELETE, PATCH). +Payment is handled automatically using the wallet's USDC balance. +Returns API response data plus purchase metadata (purchaseId, reviewToken) for leaving reviews.`, + schema: OneLyCallSchema, + }) + async call( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + const endpointPath = this.parseEndpointToPath(args.endpoint); + const fullUrl = `${ONELY_API_BASE}${endpointPath}`; + const requestHeaders: Record = { + "Content-Type": "application/json", + ...(args.headers || {}), + }; + + // Initial request to get payment requirements + const initialResponse = await fetch(fullUrl, { + method: args.method, + headers: requestHeaders, + body: args.body ? JSON.stringify(args.body) : undefined, + }); + + // If not 402, return response directly + if (initialResponse.status !== 402) { + if (initialResponse.ok) { + const data = await initialResponse.json(); + return JSON.stringify( + { + success: true, + data, + note: "No payment required (free API)", + }, + null, + 2, + ); + } + + return JSON.stringify( + { + error: true, + message: "API call failed", + status: initialResponse.status, + statusText: initialResponse.statusText, + }, + null, + 2, + ); + } + + // Handle 402 Payment Required + if ( + !( + walletProvider instanceof EvmWalletProvider || walletProvider instanceof SvmWalletProvider + ) + ) { + return JSON.stringify( + { + error: true, + message: "Unsupported wallet provider", + details: "Only EvmWalletProvider and SvmWalletProvider are supported for x402 payments", + }, + null, + 2, + ); + } + + // Create x402 client with appropriate signer and use wrapFetchWithPayment + const client = await this.createX402Client(walletProvider); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); + + // Make the request with automatic payment handling + const paidResponse = await fetchWithPayment(fullUrl, { + method: args.method, + headers: requestHeaders, + body: args.body ? JSON.stringify(args.body) : undefined, + }); + + if (!paidResponse.ok) { + const errorText = await paidResponse.text(); + return JSON.stringify( + { + error: true, + message: "Payment failed", + status: paidResponse.status, + details: errorText, + }, + null, + 2, + ); + } + + const responseData = await paidResponse.json(); + + // Extract purchase metadata from response + const purchaseId = responseData._1ly?.purchaseId || responseData.purchaseId; + const reviewToken = responseData._1ly?.reviewToken || responseData.reviewToken; + const priceUsd = responseData._1ly?.priceUsd || responseData.priceUsd; + + return JSON.stringify( + { + success: true, + data: responseData, + purchase: { + purchaseId, + reviewToken, + priceUsd, + note: "Save purchaseId and reviewToken to leave a review with onely_review", + }, + }, + null, + 2, + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to call API", + details: message, + }, + null, + 2, + ); + } + } + + /** + * Leave a review after purchasing an API. + * + * @param walletProvider - Wallet provider to get address from + * @param args - Purchase ID, review token, rating (positive/negative), and optional comment + * @returns JSON string confirming review submission + * + * @example + * ```typescript + * await provider.review(walletProvider, { + * purchaseId: "...", + * reviewToken: "...", + * positive: true, + * comment: "Great API!" + * }); + * ``` + */ + @CreateAction({ + name: "onely_review", + description: `Leave a review after purchasing an API on 1ly.store. +Requires purchaseId and reviewToken from the API call response. +Wallet address is automatically obtained from your wallet. +Reviews can be positive (true) or negative (false) with optional comment (max 500 chars).`, + schema: OneLyReviewSchema, + }) + async review( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + // Get wallet address from provider + const walletAddress = walletProvider.getAddress(); + + const response = await fetch(`${ONELY_API_BASE}/api/reviews`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + purchaseId: args.purchaseId, + wallet: walletAddress, + token: args.reviewToken, + positive: args.positive, + comment: args.comment, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + return JSON.stringify( + { + error: true, + message: "Failed to submit review", + status: response.status, + details: errorData, + }, + null, + 2, + ); + } + + const data = await response.json(); + return JSON.stringify( + { + success: true, + message: "Review submitted successfully", + data, + }, + null, + 2, + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to submit review", + details: message, + }, + null, + 2, + ); + } + } + + // ========================================== + // SELLER ACTIONS (Require Wallet Signature + API Key) + // ========================================== + + /** + * Create a new store on the 1ly marketplace using wallet signature. + * + * @param walletProvider - Wallet provider for signing authentication message + * @param args - Optional username, display name, and avatar URL + * @returns JSON string with API key and store details + * + * @example + * ```typescript + * const result = await provider.createStore(walletProvider, { + * username: "mystore", + * displayName: "My AI Store" + * }); + * // Save the apiKey from result for future seller actions + * ``` + */ + @CreateAction({ + name: "onely_create_store", + description: `Create a new store for your agent on 1ly.store using wallet signature. +Returns an API key that must be saved for subsequent seller actions (create_link, list_links, get_stats, withdraw). +Requires a wallet with USDC balance for future transactions. +This is the first step to become a seller on the marketplace.`, + schema: OneLyCreateStoreSchema, + }) + async createStore( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + // Get wallet address and chain + const address = walletProvider.getAddress(); + const network = walletProvider.getNetwork(); + + // Determine chain (base or solana mainnet only) + const chain = network.networkId === "solana-mainnet" ? "solana" : "base"; + + // Get nonce from 1ly + const nonceRes = await fetch(`${ONELY_API_BASE}/api/agent/auth/nonce`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ address, chain }), + }); + + if (!nonceRes.ok) { + return JSON.stringify( + { + error: true, + message: "Failed to get nonce", + status: nonceRes.status, + }, + null, + 2, + ); + } + + const nonceJson = await nonceRes.json(); + const message = nonceJson.data?.message; + if (!message) { + return JSON.stringify( + { + error: true, + message: "Missing message from nonce response", + }, + null, + 2, + ); + } + + // Sign message using walletProvider + let signature: string; + if (walletProvider instanceof EvmWalletProvider) { + signature = await walletProvider.signMessage(message); + } else if (walletProvider instanceof SvmWalletProvider) { + const messageBytes = new TextEncoder().encode(message); + const signatureBytes = await walletProvider.signMessage(messageBytes); + signature = Buffer.from(signatureBytes).toString("base64"); + } else { + return JSON.stringify( + { + error: true, + message: "Unsupported wallet provider", + details: "Only EvmWalletProvider and SvmWalletProvider are supported", + }, + null, + 2, + ); + } + + // Create store with signature + const signupRes = await fetch(`${ONELY_API_BASE}/api/agent/signup`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address, + chain, + signature, + message, + username: args.username, + displayName: args.displayName, + avatarUrl: args.avatarUrl, + }), + }); + + if (!signupRes.ok) { + const errorData = await signupRes.json(); + return JSON.stringify( + { + error: true, + message: "Failed to create store", + status: signupRes.status, + details: errorData, + }, + null, + 2, + ); + } + + const data = await signupRes.json(); + const apiKey = data.data?.apiKey; + const store = data.data?.store; + + return JSON.stringify( + { + success: true, + apiKey, + store, + instructions: + "IMPORTANT: Save this API key! Use it to initialize oneLyActionProvider({ apiKey: '...' }) for seller actions.", + }, + null, + 2, + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to create store", + details: message, + }, + null, + 2, + ); + } + } + + /** + * Create a new API listing on your store. + * + * @param walletProvider - Wallet provider (not used but required by interface) + * @param args - API title, URL, description, price, and other listing details + * @returns JSON string with created link details + * + * @example + * ```typescript + * await provider.createLink(walletProvider, { + * title: "Weather API", + * url: "https://myapi.com/weather", + * description: "Get real-time weather data", + * price: "0.01", + * currency: "USDC" + * }); + * ``` + */ + @CreateAction({ + name: "onely_create_link", + description: `Create a new API listing on your 1ly.store store. +Requires API key from onely_create_store. +Set a price in USDC (e.g., "0.01" for 1 cent) or leave empty for free APIs. +Returns the created listing with its endpoint URL for buyers to discover.`, + schema: OneLyCreateLinkSchema, + }) + async createLink( + _walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + if (!this.apiKey) { + return JSON.stringify( + { + error: true, + message: "Missing API key", + details: "Set apiKey in provider config or ONELY_API_KEY environment variable", + }, + null, + 2, + ); + } + + const response = await fetch(`${ONELY_API_BASE}/api/v1/links`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + title: args.title, + url: args.url, + description: args.description, + slug: args.slug, + price: args.price, + currency: args.currency || "USDC", + isPublic: args.isPublic ?? true, + isStealth: args.isStealth ?? false, + webhookUrl: args.webhookUrl, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + return JSON.stringify( + { + error: true, + message: "Failed to create link", + status: response.status, + details: errorData, + }, + null, + 2, + ); + } + + const data = await response.json(); + return JSON.stringify(data, null, 2); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to create link", + details: message, + }, + null, + 2, + ); + } + } + + /** + * List all API listings on your store. + * + * @param walletProvider - Wallet provider (not used but required by interface) + * @param args - Empty object (no parameters required) + * @returns JSON string with array of all your API listings + * + * @example + * ```typescript + * const listings = await provider.listLinks(walletProvider, {}); + * ``` + */ + @CreateAction({ + name: "onely_list_links", + description: `List all API listings on your 1ly.store store. +Requires API key from onely_create_store. +Returns array of all your listings with details, stats, and earnings.`, + schema: OneLyListLinksSchema, + }) + async listLinks( + _walletProvider: WalletProvider, + _args: z.infer, + ): Promise { + try { + if (!this.apiKey) { + return JSON.stringify( + { + error: true, + message: "Missing API key", + details: "Set apiKey in provider config or ONELY_API_KEY environment variable", + }, + null, + 2, + ); + } + + const response = await fetch(`${ONELY_API_BASE}/api/v1/links`, { + headers: { + Authorization: `Bearer ${this.apiKey}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + return JSON.stringify( + { + error: true, + message: "Failed to list links", + status: response.status, + details: errorData, + }, + null, + 2, + ); + } + + const data = await response.json(); + return JSON.stringify(data, null, 2); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to list links", + details: message, + }, + null, + 2, + ); + } + } + + /** + * Get store statistics and earnings. + * + * @param walletProvider - Wallet provider (not used but required by interface) + * @param args - Optional period filter (7d, 30d, 90d, all) and linkId filter + * @returns JSON string with earnings, sales count, and revenue breakdown + * + * @example + * ```typescript + * // Get all-time stats + * await provider.getStats(walletProvider, {}); + * + * // Get last 30 days stats + * await provider.getStats(walletProvider, { period: "30d" }); + * + * // Get stats for specific link + * await provider.getStats(walletProvider, { linkId: "..." }); + * ``` + */ + @CreateAction({ + name: "onely_get_stats", + description: `Get store statistics and earnings on 1ly.store. +Requires API key from onely_create_store. +Filter by time period (7d, 30d, 90d, all) or specific link. +Returns total earnings, sales count, and detailed revenue breakdown.`, + schema: OneLyGetStatsSchema, + }) + async getStats( + _walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + if (!this.apiKey) { + return JSON.stringify( + { + error: true, + message: "Missing API key", + details: "Set apiKey in provider config or ONELY_API_KEY environment variable", + }, + null, + 2, + ); + } + + const params = new URLSearchParams(); + if (args.period) params.set("period", args.period); + if (args.linkId) params.set("linkId", args.linkId); + + const url = `${ONELY_API_BASE}/api/v1/stats?${params}`; + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${this.apiKey}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + return JSON.stringify( + { + error: true, + message: "Failed to get stats", + status: response.status, + details: errorData, + }, + null, + 2, + ); + } + + const data = await response.json(); + return JSON.stringify(data, null, 2); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to get stats", + details: message, + }, + null, + 2, + ); + } + } + + /** + * Withdraw earnings from your store to a Solana wallet. + * + * @param walletProvider - Wallet provider (not used but required by interface) + * @param args - Amount to withdraw in USDC and destination Solana wallet address + * @returns JSON string confirming withdrawal transaction + * + * @example + * ```typescript + * await provider.withdraw(walletProvider, { + * amount: "10.50", + * walletAddress: "YourSolanaAddress..." // Solana only + * }); + * ``` + */ + @CreateAction({ + name: "onely_withdraw", + description: `Withdraw earnings from your 1ly.store store to a Solana wallet. +Requires API key from onely_create_store. +Specify amount in USDC (e.g., "10.50") and destination Solana wallet address. +Note: Withdrawals are Solana-only at this time. +Returns transaction details once withdrawal is processed.`, + schema: OneLyWithdrawSchema, + }) + async withdraw( + _walletProvider: WalletProvider, + args: z.infer, + ): Promise { + try { + if (!this.apiKey) { + return JSON.stringify( + { + error: true, + message: "Missing API key", + details: "Set apiKey in provider config or ONELY_API_KEY environment variable", + }, + null, + 2, + ); + } + + const response = await fetch(`${ONELY_API_BASE}/api/v1/withdrawals`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + amount: args.amount, + walletAddress: args.walletAddress, + chain: "solana", + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + return JSON.stringify( + { + error: true, + message: "Failed to withdraw", + status: response.status, + details: errorData, + }, + null, + 2, + ); + } + + const data = await response.json(); + return JSON.stringify(data, null, 2); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return JSON.stringify( + { + error: true, + message: "Failed to withdraw", + details: message, + }, + null, + 2, + ); + } + } + + // ========================================== + // UTILITY METHODS + // ========================================== + + /** + * Checks if this provider supports the given network. + * + * @param network - The network to check support for + * @returns True if the network is supported (Base or Solana) + */ + supportsNetwork = (network: Network) => + (SUPPORTED_NETWORKS as readonly string[]).includes(network.networkId!); + + /** + * Creates an x402 client configured for the given wallet provider. + * + * @param walletProvider - The wallet provider to configure the client for + * @returns Configured x402Client + */ + private async createX402Client(walletProvider: WalletProvider): Promise { + const client = new x402Client(); + + if (walletProvider instanceof EvmWalletProvider) { + const signer = toClientEvmSigner(walletProvider.toSigner(), walletProvider.getPublicClient()); + registerExactEvmScheme(client, { signer }); + } else if (walletProvider instanceof SvmWalletProvider) { + const signer = await walletProvider.toSigner(); + registerExactSvmScheme(client, { signer }); + } + + return client; + } + + /** + * Parses endpoint string to extract username and slug. + * + * @param endpoint - Endpoint string (e.g., 'joe/weather' or '/api/link/joe/weather') + * @returns Object with username and slug + */ + private parseEndpoint(endpoint: string): { username: string; slug: string } { + const cleaned = endpoint.replace(/^\/api\/link\//, "").replace(/^\//, ""); + const [username, slug] = cleaned.split("/"); + + if (!username || !slug) { + throw new Error( + "Invalid endpoint format. Expected 'username/slug' or '/api/link/username/slug'", + ); + } + + return { username, slug }; + } + + /** + * Parses endpoint string to API path. + * + * @param endpoint - Endpoint string (e.g., 'joe/weather' or '/api/link/joe/weather') + * @returns API path starting with /api/link/ + */ + private parseEndpointToPath(endpoint: string): string { + if (endpoint.startsWith("/api/link/")) { + return endpoint; + } + const cleaned = endpoint.replace(/^\//, ""); + return `/api/link/${cleaned}`; + } +} + +/** + * Factory function to create a new OneLyActionProvider instance. + * + * @param config - Optional configuration for API key and custom API base + * @returns A new OneLyActionProvider instance + * + * @example + * ```typescript + * // As buyer + * const provider = oneLyActionProvider(); + * + * // As seller (after creating store) + * const provider = oneLyActionProvider({ apiKey: "your-api-key-here" }); + * ``` + */ +export const oneLyActionProvider = (config?: OneLyConfig) => new OneLyActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/onely/schemas.ts b/typescript/agentkit/src/action-providers/onely/schemas.ts new file mode 100644 index 000000000..4d5569a7e --- /dev/null +++ b/typescript/agentkit/src/action-providers/onely/schemas.ts @@ -0,0 +1,157 @@ +import { z } from "zod"; + +// ========================================== +// BUYER ACTIONS +// ========================================== + +/** + * Schema for searching APIs and services on the 1ly marketplace + */ +export const OneLySearchSchema = z.object({ + query: z.string().describe("Search term (e.g., 'weather api', 'image generation')"), + type: z + .enum(["api", "standard"]) + .optional() + .describe("Filter by link type: 'api' for API endpoints, 'standard' for digital products"), + maxPrice: z.number().optional().describe("Maximum price in USD"), + minPrice: z.number().optional().describe("Minimum price in USD"), + limit: z + .number() + .min(1) + .max(50) + .optional() + .default(10) + .describe("Number of results (default: 10, max: 50)"), +}); + +/** + * Schema for getting details of a specific API listing + */ +export const OneLyGetDetailsSchema = z.object({ + endpoint: z + .string() + .describe("API endpoint path (e.g., 'joe/weather' or '/api/link/joe/weather')"), +}); + +/** + * Schema for calling a paid API with x402 payment + */ +export const OneLyCallSchema = z.object({ + endpoint: z + .string() + .describe("API endpoint path (e.g., 'joe/weather' or '/api/link/joe/weather')"), + method: z + .enum(["GET", "POST", "PUT", "DELETE", "PATCH"]) + .optional() + .default("GET") + .describe("HTTP method (default: GET)"), + body: z.record(z.unknown()).optional().describe("Request body for POST/PUT/PATCH requests"), + headers: z.record(z.string()).optional().describe("Additional headers to send"), +}); + +/** + * Schema for leaving a review after purchasing an API + */ +export const OneLyReviewSchema = z.object({ + purchaseId: z.string().describe("Purchase ID from the API call response"), + reviewToken: z.string().describe("Review token from the API call response"), + positive: z.boolean().describe("Whether the review is positive (true) or negative (false)"), + comment: z.string().max(500).optional().describe("Optional review comment (max 500 characters)"), +}); + +// ========================================== +// SELLER ACTIONS +// ========================================== + +/** + * Schema for creating a new store on the 1ly marketplace + */ +export const OneLyCreateStoreSchema = z.object({ + username: z + .string() + .min(3) + .max(20) + .optional() + .describe("Unique username for the store (3-20 characters)"), + displayName: z + .string() + .max(50) + .optional() + .describe("Display name for the store (max 50 characters)"), + avatarUrl: z.string().url().optional().describe("URL to store avatar image"), +}); + +/** + * Schema for creating a new API listing + */ +export const OneLyCreateLinkSchema = z.object({ + title: z.string().min(1).max(200).describe("Title of the API listing (1-200 characters)"), + url: z.string().url().describe("URL of the API endpoint to list"), + description: z + .string() + .max(500) + .optional() + .describe("Description of the API (max 500 characters)"), + slug: z + .string() + .min(3) + .max(64) + .regex(/^[a-z0-9-]+$/) + .optional() + .describe("URL-friendly slug (3-64 characters, lowercase alphanumeric and hyphens)"), + price: z + .string() + .regex(/^\d+(\.\d{1,18})?$/) + .optional() + .describe("Price in USDC (e.g., '0.01' for 1 cent)"), + currency: z.literal("USDC").optional().describe("Currency (only USDC supported)"), + isPublic: z + .boolean() + .optional() + .describe("Whether the listing is publicly visible (default: true)"), + isStealth: z + .boolean() + .optional() + .describe("Whether the listing is in stealth mode (default: false)"), + webhookUrl: z.string().url().optional().describe("Optional webhook URL for purchase events"), +}); + +/** + * Schema for listing all API listings (no parameters required) + */ +export const OneLyListLinksSchema = z.object({}); + +/** + * Schema for getting store statistics and earnings + */ +export const OneLyGetStatsSchema = z.object({ + period: z + .enum(["7d", "30d", "90d", "all"]) + .optional() + .describe("Time period for statistics (default: all)"), + linkId: z.string().uuid().optional().describe("Filter statistics by specific link ID"), +}); + +/** + * Schema for withdrawing earnings from the marketplace + */ +export const OneLyWithdrawSchema = z.object({ + amount: z + .string() + .regex(/^\d+(\.\d{1,18})?$/) + .describe("Amount to withdraw in USDC (e.g., '10.50')"), + walletAddress: z + .string() + .min(26) + .describe("Destination Solana wallet address (Solana only)"), +}); + +/** + * Configuration for the OneLy action provider + */ +export type OneLyConfig = { + /** + * API key for seller actions (obtained from onely_create_store) + */ + apiKey?: string; +};