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
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Example .env file for the API server
# If you want to add new variables, please do it in the format below
# <Your Variable Name>=<An explanation of the variable>
# <Your Variable Name>=<An explanation of the variable>
# On the next iteration of pnpm run dev, the new variables picked up by the validate-env script will be added to the .env file
# You can add hints to the ENV_HINTS object in the validate-env script to help the user with the new variables

Expand Down Expand Up @@ -30,6 +30,8 @@ TWITTER_API_KEY=<Your Twitter API Key>
TWITTER_API_SECRET_KEY=<Your Twitter API Secret Key>
TWITTER_ACCESS_TOKEN=<Your Twitter Access Token>
TWITTER_ACCESS_TOKEN_SECRET=<Your Twitter Access Token Secret>
EMBER_API_KEY=<Your Ember API Key>
EMBER_ENDPOINT_URL=<Your Ember Endpoint>

# GATED DATA (optional). Server will start without these and not use the feature
ORBIS_CONTEXT_ID=<Orbis server context ID>
Expand Down
3 changes: 3 additions & 0 deletions server/bin/validate-env
Comment thread
dvidsilva marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const ENV_HINTS = {
"[Press enter/return to skip] (optional - gated data) https://ceramic-orbisdb-mainnet-direct.hirenodes.io/",
USE_OPENAI_EMBEDDING:
"[Press enter/return to skip] (optional - gated data) must be TRUE to use gated data functionality",
EMBER_API_KEY: "Enter your Ember API key",
EMBER_ENDPOINT_URL:
"Enter your Ember endpoint URL with port, usually grpc.api.emberai.xyz:50051",
};

const rl = readline.createInterface({
Expand Down
8 changes: 6 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
"@ai16z/adapter-sqlite": "0.1.5-alpha.3",
"@ai16z/eliza": "0.1.5-alpha.3",
"@ai16z/plugin-bootstrap": "0.1.5-alpha.3",
"@emberai/sdk-typescript": "0.2.0",
"@grammyjs/conversations": "^1.2.0",
"@grpc/grpc-js": "^1.12.5",
"@grpc/proto-loader": "^0.7.13",
"@ngrok/ngrok": "^1.4.1",
"@solana/web3.js": "^1.98.0",
"agent-twitter-client": "^0.0.18",
"@useorbis/db-sdk": "0.0.60-alpha",
"agent-twitter-client": "^0.0.18",
"axios": "^1.7.7",
"better-sqlite3": "^11.6.0",
"cookie-parser": "^1.4.7",
Expand All @@ -35,7 +38,8 @@
"jsonc-parser": "^3.3.1",
"node-cache": "^5.1.2",
"prettier": "^3.4.2",
"tsc-watch": "^6.2.1"
"tsc-watch": "^6.2.1",
"viem": "^2.22.17"
},
"devDependencies": {
"@typechain/ethers-v6": "^0.5.1",
Expand Down
4 changes: 4 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import twitterRouter from "./routes/twitter.js";
import discordRouter from "./routes/discord.js";
import cookieParser from "cookie-parser";
import githubRouter from "./routes/github.js";
import emberRouter from "./routes/ember.js";
import { AnyType } from "./utils.js";
import { isHttpError } from "http-errors";

Expand Down Expand Up @@ -44,6 +45,9 @@ app.use("/hello", helloRouter);
// Initialize Telegram bot service
const telegramService = TelegramService.getInstance();

// Mount Ember AGI TypeScript SDK routes
app.use("/ember", emberRouter);

// Mount Telegram webhook endpoint
app.use("/telegram/webhook", telegramService.getWebhookCallback());

Expand Down
107 changes: 107 additions & 0 deletions server/src/routes/ember.ts
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • EmberClient needs to always be instantiated with a URL and optional API key.
  • There is no getClient method. It's instantiated directly.
const client = new EmberClient({
  endpoint: process.env.EMBER_ENDPOINT_URL, // grpc.api.emberai.xyz:50051
  apiKey: process.env.EMBER_API_KEY,
})
  • The /agent/swap endpoint is incorrectly instantiating the EmberClient. See above example.
  • There are only two options active for swap type right now.
    1. MARKET_BUY - buys the base token with the quote token
    2. MARKET_SELL - sells the base token for the quote token

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xTomDaniel changed the order Type, this method is a wrapper included in the ember.service, to follow convention - beihnd the scenes is the same

https://github.com/collabland/AI-Agent-Starter-Kit/pull/20/files/ed98278a160e7556ed79e3c3cca1260ae4eca847

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Sorry I completely overlooked that!

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Router, Request, Response } from "express";
import {
EmberService,
type AgentSwapAction,
} from "../services/ember.service.js";

const router = Router();

/**
* Initiates the Ember AGI TypeScript SDK
* https://github.com/EmberAGI/ember-sdk-typescript
*/
router.get("/init", async (req: Request, res: Response) => {
try {
const ember = await EmberService.getClient();

console.log("[Ember Init] initialized ember client:", { query: req.query });
res.json({ ok: true, ember });
} catch (error) {
console.error("[Ember Init] Error:", error);
res.status(500).json({ error: "Ember initialization failed" });
}
});

/**
* Fetches chains from the Ember AGI TypeScript SDK
*/
router.get("/chains", async (req: Request, res: Response) => {
try {
const ember = await EmberService.getClient();
const pageSize = parseInt(req.query.pageSize as string, 10) || 10;

const chains = await ember.getChains({
pageSize,
filter: "", // Optional filter string
pageToken: "", // Optional pagination token
});

console.log("[Ember Chains] fetched chains:");
res.status(200).json({ ok: true, chains });
} catch (error) {
console.error("[Ember Chains] Error:", error);
res.status(500).json({ error: "Failed to fetch chains" });
}
});

/**
* Fetches tokens information available to the Ember AGI TypeScript SDK
*/
router.get("/tokens", async (req: Request, res: Response) => {
try {
const ember = await EmberService.getClient();
const pageSize = parseInt(req.query.pageSize as string, 10) || 10;

const tokens = await ember.getTokens({
pageSize,
filter: "", // Optional filter string
pageToken: "", // Optional pagination token
chainId: (req.query.chainId as string) || "1", // Ethereum chain ID
});

console.log("[Ember Chains] fetched chains:");
res.status(200).json({ ok: true, tokens });
} catch (error) {
console.error("[Ember Chains] Error:", error);
res.status(500).json({ error: "Failed to fetch chains" });
}
});

/**
* We're using the ember client to find chains, find a ethereum chain, and perform a swap between two tokens
* Swaps are used to compensate agents in their network and decentralized applications
*/

router.post("/agent/swap", async (req: Request, res: Response) => {
try {
const { baseToken, quoteToken, amount, recipient } = req.body;
const ember = new EmberService();

const action: AgentSwapAction = {
type: "MARKET_BUY",
params: {
baseToken,
quoteToken,
amount,
recipient,
},
status: "PENDING",
};

const result = await ember.executeSwap(action);

res.json({
success: true,
action,
result,
});
} catch (error) {
console.error("[Agent Swap] Error:", error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Agent swap failed",
});
}
});

export default router;
125 changes: 125 additions & 0 deletions server/src/services/ember.service.ts
Copy link
Copy Markdown

@0xTomDaniel 0xTomDaniel Feb 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct in understanding that this is just a proof-of-concept that will be refactored?

  • Sorry if I'm pointing out the obvious, but I just want to note that the execute swap route definitely shouldn't be hard coded to always swap the same thing
  • Also, the ideal use of Ember is that methods should be packaged as Tools (Actions) for the agent to call
  • The agent signs the transaction returned by Ember with its own agent wallet

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xTomDaniel yes, this would't be useful released as this

waiting for some agent documentation, or someone to refactor into the workflow. but they wanted to check how things are working

I was traveling but we'll be around for a few days if we have instructions to execute

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xTomDaniel if you understand the flow better, and have that info, we can develop that together. we could find more in the chat and improve the documentation in this repo with some examples and diagrams

Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { BaseService } from "./base.service.js";
import EmberClient from "@emberai/sdk-typescript";
import { OrderType, SwapTokensResponse } from "@emberai/sdk-typescript";
import { privateKeyToAccount } from "viem/accounts";

export interface AgentSwapAction {
type: "MARKET_BUY" | "MARKET_SELL";
params: {
baseToken: string;
quoteToken: string;
amount: string;
recipient: string;
};
status: "PENDING" | "EXECUTED" | "FAILED";
}

/**
* EmberService class - https://www.emberai.xyz/
* Bridging the gap between Agents and dApps
*/
export class EmberService extends BaseService {
private static client: EmberClient;

public constructor() {
super();
}

public static getClient(): EmberClient {
if (!EmberService.client) {
EmberService.client = new EmberClient({
endpoint:
process.env.EMBER_ENDPOINT_URL || "grpc.api.emberai.xyz:50051",
apiKey: process.env.EMBER_API_KEY || "",
});
}
return EmberService.client;
}

/**
* executeSwap - Executes a swap between two tokens
* @param action
* @returns
*/
public async executeSwap(
action: AgentSwapAction
): Promise<SwapTokensResponse | void> {
this.start();

try {
const client = EmberService.getClient();

const { chains } = await client.getChains({
pageSize: 10,
filter: "",
pageToken: "",
});

const ethereum = chains.find((c) => c.name.toLowerCase() === "ethereum");
if (!ethereum) throw new Error("Chain not found");

// Get tokens on Ethereum
const { tokens } = await client.getTokens({
chainId: ethereum.chainId,
pageSize: 100,
filter: "",
pageToken: "",
});

// Find USDC and WETH tokens
const baseToken = tokens.find(
(token) => token.symbol === action.params.baseToken
);
const quoteToken = tokens.find(
(token) => token.symbol === action.params.quoteToken
);

if (!baseToken || !quoteToken) {
throw new Error("Required tokens not found");
}

const account = privateKeyToAccount(
action.params.recipient as `0x${string}`
);

const swap = await client.swapTokens({
orderType: OrderType.MARKET_BUY,
baseToken: {
chainId: ethereum.chainId,
address: baseToken?.tokenId as string,
},
quoteToken: {
chainId: ethereum.chainId,
address: quoteToken?.tokenId as string,
},
amount: action.params.amount,
recipient: account.address,
});

action.status = "EXECUTED";
// this.actions.push(action);

return swap;
} catch (error) {
action.status = "FAILED";
throw error;
}
}

public async start(): Promise<void> {
console.log("[EmberService] Starting service...");

try {
if (!EmberService.client) {
EmberService.client = EmberService.getClient();
}
} catch (error) {
console.error("[EmberService] Error:", error);
throw new Error("Ember service failed to start.");
}
}

public async stop(): Promise<void> {
console.log("[EmberService] Stopping service...");
}
}