In this tutorial, we will build a cryptocurrency address portfolio tracker, as a Shinkai Tool.
This tool can be a core component of a crypto AI Agent capable of discussing real-time blockchain data.
The tool presented here interacts with the TapTools API to analyze and visualize portfolio data for a specified Cardano wallet address.
It retrieves positions for fungible tokens, NFTs, and liquidity provider positions. The tool calculates their values in Ada and a specified quote currency and generates visualizations based on user-defined parameters.
You can find this tool in the Shinkai AI Store. Its name is : Taptools Cardano Portfolio Tracker. You can watch a usage example of this tool here.
While we use a Cardano example, the same tool structure can be applied to any blockchain for which an API provides such data.
local indexer/node with data fetching capabilities,
blockchain data provider services,
decentralized blockchain data protocols,
crypto wide analytics and market platforms.
Here, we’ll use the Taptools API. Taptools is the dominant platform for data about Cardano tokens and NFTs trading as well as wallet tracking. A quick look at the documentation of the API shows that they offer endpoints with practical aggregated data for wallet portfolio analysis.
Shinkai allows effortless building of tools and AI Agents thanks to its integrated AI assistance. You can build the tool by just prompting a LLM. You don’t have to wrestle with library dependencies or handle manual deployments, everything is automated and user-friendly.
To create the portfolio tracker tool, navigate to the Shinkai Tools tab, select a programming language, and use the AI ‘shinkai_code_gen’, because :
it can access documentation online,
it thinks about the requirements and asks for feedback before generating the tool.
You can then prompt the AI to build your tool.
Use a prompt similar to the one below, with clear instructions :
Prompt
Copy
Ask AI
<task>create a cardano wallet portfolio tracker tool using the taptools api. Include an option to generate a line graph of the overall wallet value over time, as well as bar graphs per type of positions showing their current values.</task><API documentation>The documentation for the api to use is at https://openapi.taptools.io/.</API documentation><graphs>Make graphs in well readable dark mode.Make graphs optional.Include in configuration some minimum values per position type, in order to exclude from the text output and from the graphs the dust positions.</graphs><modularity>Make the tool modular, with options according to the API documentation.</modularity><value calculations>Fetch the Ada price in the desired quote currency, and use that price to also calculate the value of all positions in quote currency.Each value in the output should be followed by its unit.</value calculations><explicit naming>Make sure every single config, input, graph, and output element has a distinct explicit names fully easily understandable.Do not use abbreviations, use full words, so that it's extra clear.</explicit naming><errors> Handle potential errors gracefully: if the API returns an error, capture the status code and message, then include them in the output for troubleshooting.</errors>
Below is an example of thinking process you might get from Shinkai code generator AI. Review it and follow along.
Codegen thinking process
Copy
Ask AI
Code Generationid: @@localhost.sep-shinkai-jobid_427f8faf-302e-444f-85c8-d3bca6dbe894Library AnalysisThinking about: Searching for Required LibrariesAI took 3[s] to process Searching for Required LibrariesReading https://openapi.taptools.io/Thinking about: Fetching DocumentationAI took 5[s] to process Fetching DocumentationReading documentation...Program RequirementsThinking about: Analyzing RequirementsArchitecting the perfect answer... 5sHarmonizing with your question... 10sCalculating the ideal response... 15sExecuting thought protocols... 20sDiving into the depths of knowledge... 25sAligning neural pathways... 30sAnalyzing countless possibilities... 35sSolving the knowledge puzzle... 40sAI took 44[s] to process Analyzing RequirementsRequirementsFetch wallet portfolio positions from TapTools APIFetch ADA price in specified quote currency from TapTools APICalculate values of all positions in both ADA and quote currencyFilter out dust positions based on configurable minimum valuesGenerate line graph of overall wallet value over time (optional)Generate bar graphs per position type showing current values (optional)Implement dark mode styling for all graphsHandle API errors gracefully and include error details in outputMake all configuration options explicit and clearly namedModular design allowing different API endpoints to be usedStandard Librariesjson : For handling JSON data datetime : For working with timestamps math : For calculations typing : For type hintsInternal Librariesshinkai_llm_prompt_processor : For generating graph visualizations get_access_token : For API authentication if requiredExample Input and OutputInput: { "wallet_address": "stake1u8rphunzxm9lr4m688peqmnthmap35yt38rgvaqgsk5jcrqdr2vuc", "quote_currency": "USD", "generate_line_graph": true, "generate_bar_graphs": true, "dark_mode": true }Output: { "portfolio_summary": { "total_value_ada": 10000, "total_value_quote": 6100, "currency": "USD", "positions_count": 15, "filtered_positions_count": 3 }, "positions": [ { "type": "FT", "name": "AGIX", "amount": 200, "value_ada": 10000, "value_quote": 6100 } ], "graphs": { "line_graph_path": "/home/user/wallet_value_trend.png", "bar_graph_path": "/home/user/position_values.png" }, "errors": [] }Configtaptools_api_key: "your-api-key-here" minimum_ada_value_for_display: 10 minimum_quote_value_for_display: 5 dark_mode_primary_color: "#2E3440" dark_mode_secondary_color: "#3B4252" dark_mode_text_color: "#E5E9F0"Please provide feedback on the requirements or just say OK to confirm
Also, in this video a working prototype of the Cardano portfolio tracker’s data retrieval feature is built in just a few minutes by simply providing instructions and API documentation snippets.
The ‘shinkai_code_gen’ AI should generate a functional tool based on your instructions. However, you may want to refine it further.
You can for example prompt the AI to :
rename configs, inputs or outputs keys exactly with precise names,
improve some number formatting in the output (percentages, rounding in meaningful ways, etc.),
remove data from the output that comes from the API call but is not of interest,
modify the layouts of the graphs,
improve some error messaging,
etc.
For reference, here is an example of fully annotated code for a Cardano wallet portfolio tracker tool, obtained after prompting a few times for such improvements.
Fully annotated code
Copy
Ask AI
# /// script# requires-python = ">=3.10,<3.12"# dependencies = [# "requests",# "matplotlib",# ]# ///# Import necessary modules and typesfrom typing import Any, Optional, Dict, List # Type hints for better code clarityfrom requests import get # HTTP GET requestsimport matplotlib.pyplot as plt # Plotting libraryfrom shinkai_local_support import get_home_path # Utility to get user's home directory# Configuration class for API and value thresholdsclass CONFIG: taptools_api_key: str # API key for TapTools min_quote_value_ft: str = "1" # Min value for fungible tokens in quote currency min_quote_value_nft: str = "1" # Min value for NFTs in quote currency min_quote_value_lp: str = "1" # Min value for liquidity positions in quote currency# Input class for user-provided parametersclass INPUTS: address: str # Wallet address to query quote_currency: str = "USD" # Currency for value quotes (default: USD) generate_positions_plots: str = "yes" # Option to generate position plots ("yes"/"no") generate_portfolio_trended_value_graphs: str = "yes" # Option to generate trend graphs ("yes"/"no") portfolio_trended_value_timeframe: str = "30d" # Timeframe for trend graphs (24h, 7d, 30d, 90d, 180d, 1y, all)# Output class for storing resultsclass OUTPUT: portfolio_positions: Dict[str, Any] # Portfolio data (positions, values) ada_price_in_quote: float # ADA price in quote currency error: Optional[str] = None # Error message if request fails# Main async function to fetch and process portfolio dataasync def run(config: CONFIG, inputs: INPUTS) -> OUTPUT: api_base_url = "https://openapi.taptools.io/api/v1" # Base URL for TapTools API portfolio_positions_url = f"{api_base_url}/wallet/portfolio/positions?address={inputs.address}" # URL for portfolio positions token_quote_url = f"{api_base_url}/token/quote?quote={inputs.quote_currency}" # URL for ADA price in quote currency headers = { "x-api-key": config.taptools_api_key # API key header for authentication } # Fetch ADA price in quote currency quote_response = get(token_quote_url, headers=headers) if quote_response.status_code != 200: # Check if request failed output = OUTPUT() try: error_details = quote_response.json() # Parse error details error_message = error_details.get("message", "An error occurred") # Extract error message except json.JSONDecodeError: # Handle non-JSON response error_message = quote_response.text output.error = f"Error fetching ADA price: {error_message} (Status Code: {quote_response.status_code})" # Set error return output ada_price_in_quote = quote_response.json().get("price", 0) # Extract ADA price ada_price_attr_name = f"ada_price_in_{inputs.quote_currency}" # Dynamic attribute name for ADA price # Fetch portfolio positions response = get(portfolio_positions_url, headers=headers) output = OUTPUT() # Initialize output object if response.status_code == 200: # Check if request succeeded data = response.json() # Parse response JSON # Rename keys for clarity data["Number_of_Fungible_Tokens"] = data.pop("numFTs", 0) # Number of fungible tokens data["Number_of_NFT_collections"] = data.pop("numNFTs", 0) # Number of NFT collections data["Fungible_Tokens_positions"] = data.pop("positionsFt", []) # Fungible token positions data["NFTs_positions"] = data.pop("positionsNft", []) # NFT positions data["Liquidity_Provider_Positions"] = data.pop("positionsLp", []) # Liquidity provider positions # Process fungible token positions for position in data["Fungible_Tokens_positions"]: for timeframe in ["24h", "7d", "30d"]: # Process price changes for timeframes if timeframe in position: change_value = position[timeframe] # Get change value sign = "+" if change_value >= 0 else "" # Add sign for positive/negative position[timeframe] = f"{sign}{round(change_value * 100, 2)} %" # Format as percentage # Round and rename values position["Value_in_Ada"] = round(position.pop("adaValue", 0), 2) # Value in ADA position["Liquid_Value_in_Ada"] = round(position.pop("liquidValue", 0), 2) # Liquid value in ADA position["Value_in_" + inputs.quote_currency] = round(position["Value_in_Ada"] * ada_price_in_quote, 2) # Value in quote currency position["Liquid_Value_in_" + inputs.quote_currency] = round(position["Liquid_Value_in_Ada"] * ada_price_in_quote, 2) # Liquid value in quote currency position["Liquid_Balance_in_Ada"] = position.pop("liquidBalance", 0) # Liquid balance in ADA position.pop("unit", None) # Remove unused unit field position.pop("fingerprint", None) # Remove unused fingerprint field # Process NFT positions for position in data["NFTs_positions"]: for timeframe in ["24h", "7d", "30d"]: # Process price changes for timeframes if timeframe in position: change_value = position[timeframe] # Get change value sign = "+" if change_value >= 0 else "" # Add sign for positive/negative position[timeframe] = f"{sign}{round(change_value * 100, 2)} %" # Format as percentage # Round and rename NFT values position["Value_in_Ada"] = round(position.pop("adaValue", 0), 2) # Value in ADA position["Value_in_" + inputs.quote_currency] = round(position["Value_in_Ada"] * ada_price_in_quote, 2) # Value in quote currency position["Floor_Price_in_Ada"] = position.pop("floorPrice", 0) # Floor price in ADA position["Floor_Price_in_" + inputs.quote_currency] = round(position["Floor_Price_in_Ada"] * ada_price_in_quote, 2) # Floor price in quote currency # Process liquidity provider positions for position in data["Liquidity_Provider_Positions"]: position["Value_in_Ada"] = round(position.pop("adaValue", 0), 2) # Value in ADA position["Value_in_" + inputs.quote_currency] = round(position["Value_in_Ada"] * ada_price_in_quote, 2) # Value in quote currency # Calculate total portfolio value total_value_in_ada = data.pop("adaValue", 0) # Total portfolio value in ADA data["Total_Portfolio_Value_in_Ada"] = round(total_value_in_ada, 2) # Rounded total in ADA data["Total_Portfolio_Value_in_" + inputs.quote_currency] = round(total_value_in_ada * ada_price_in_quote, 2) # Total in quote currency setattr(output, ada_price_attr_name, ada_price_in_quote) # Set ADA price in output output.portfolio_positions = data # Store portfolio data in output # Generate position plots if requested if inputs.generate_positions_plots.lower() == "yes": home_path = await get_home_path() # Get home directory for saving plots plt.style.use('dark_background') # Set dark theme for plots # Convert min values to float for comparison min_value_ft = float(config.min_quote_value_ft) # Min value for fungible tokens min_value_nft = float(config.min_quote_value_nft) # Min value for NFTs min_value_lp = float(config.min_quote_value_lp) # Min value for liquidity positions # Plot fungible tokens ft_names = [p.get("ticker", "") for p in data["Fungible_Tokens_positions"] if p.get("Value_in_" + inputs.quote_currency, 0) >= min_value_ft] # Token names ft_values = [p.get("Value_in_" + inputs.quote_currency, 0) for p in data["Fungible_Tokens_positions"] if p.get("Value_in_" + inputs.quote_currency, 0) >= min_value_ft] # Token values sorted_ft = sorted(zip(ft_values, ft_names), reverse=True) # Sort by value descending plt.figure(figsize=(10, 6)) # Set plot size plt.bar([name for _, name in sorted_ft], [value for value, _ in sorted_ft]) # Create bar plot plt.title("Fungible Tokens Value") # Set title plt.xlabel("Token") # Set x-axis label plt.ylabel(f"Value in {inputs.quote_currency}") # Set y-axis label plt.xticks(rotation=45, ha="right") # Rotate x-axis labels plt.tight_layout() # Adjust layout plt.savefig(f"{home_path}/fungible_tokens_value.png") # Save plot plt.close() # Close plot # Plot NFTs nft_names = [p.get("name", "") for p in data["NFTs_positions"] if p.get("Value_in_" + inputs.quote_currency, 0) >= min_value_nft] # NFT names nft_values = [p.get("Value_in_" + inputs.quote_currency, 0) for p in data["NFTs_positions"] if p.get("Value_in_" + inputs.quote_currency, 0) >= min_value_nft] # NFT values sorted_nft = sorted(zip(nft_values, nft_names), reverse=True) # Sort by value descending plt.figure(figsize=(10, 6)) # Set plot size plt.bar([name for _, name in sorted_nft], [value for value, _ in sorted_nft]) # Create bar plot plt.title("NFTs Value") # Set title plt.xlabel("NFT") # Set x-axis label plt.ylabel(f"Value in {inputs.quote_currency}") # Set y-axis label plt.xticks(rotation=45, ha="right") # Rotate x-axis labels plt.tight_layout() # Adjust layout plt.savefig(f"{home_path}/nfts_value.png") # Save plot plt.close() # Close plot # Plot liquidity provider positions lp_names = [p.get("ticker", "") for p in data["Liquidity_Provider_Positions"] if p.get("Value_in_" + inputs.quote_currency, 0) >= min_value_lp] # Position names lp_values = [p.get("Value_in_" + inputs.quote_currency, 0) for p in data["Liquidity_Provider_Positions"] if p.get("Value_in_" + inputs.quote_currency, 0) >= min_value_lp] # Position values sorted_lp = sorted(zip(lp_values, lp_names), reverse=True) # Sort by value descending plt.figure(figsize=(10, 6)) # Set plot size plt.bar([name for _, name in sorted_lp], [value for value, _ in sorted_lp]) # Create bar plot plt.title("Liquidity Provider Positions Value") # Set title plt.xlabel("Position") # Set x-axis label plt.ylabel(f"Value in {inputs.quote_currency}") # Set y-axis label plt.xticks(rotation=45, ha="right") # Rotate x-axis labels plt.tight_layout() # Adjust layout plt.savefig(f"{home_path}/liquidity_provider_positions_value.png") # Save plot plt.close() # Close plot # Generate trend graphs if requested if inputs.generate_portfolio_trended_value_graphs.lower() == "yes": trend_base_url = f"{api_base_url}/wallet/value/trended" # URL for portfolio trend data # Fetch trend data in ADA trend_response_ada = get(f"{trend_base_url}?address={inputs.address}&timeframe={inputs.portfolio_trended_value_timeframe}"e=ADA", headers=headers) if trend_response_ada.status_code == 200: # Check if request succeeded trend_data_ada = trend_response_ada.json() # Parse response JSON times_ada = [entry["time"] for entry in trend_data_ada] # Extract time points values_ada = [entry["value"] for entry in trend_data_ada] # Extract values plt.figure(figsize=(10, 6)) # Set plot size plt.plot(times_ada, values_ada, marker='o') # Create line plot title_ada = "Portfolio Value Trend in ADA (all time)" if inputs.portfolio_trended_value_timeframe == "all" else f"Portfolio Value Trend in ADA over {inputs.portfolio_trended_value_timeframe}" # Set dynamic title plt.title(title_ada) # Set title plt.ylabel("Value in ADA") # Set y-axis label plt.xticks([]) # Remove x-axis labels plt.tight_layout() # Adjust layout plt.savefig(f"{home_path}/portfolio_trend_ada.png") # Save plot plt.close() # Close plot # Fetch trend data in quote currency trend_response_quote = get(f"{trend_base_url}?address={inputs.address}&timeframe={inputs.portfolio_trended_value_timeframe}"e={inputs.quote_currency}", headers=headers) if trend_response_quote.status_code == 200: # Check if request succeeded trend_data_quote = trend_response_quote.json() # Parse response JSON times_quote = [entry["time"] for entry in trend_data_quote] # Extract time points values_quote = [entry["value"] for entry in trend_data_quote] # Extract values plt.figure(figsize=(10, 6)) # Set plot size plt.plot(times_quote, values_quote, marker='o') # Create line plot title_quote = f"Portfolio Value Trend in {inputs.quote_currency} (all time)" if inputs.portfolio_trended_value_timeframe == "all" else f"Portfolio Value Trend in {inputs.quote_currency} over {inputs.portfolio_trended_value_timeframe}" # Set dynamic title plt.title(title_quote) # Set title plt.ylabel(f"Value in {inputs.quote_currency}") # Set y-axis label plt.xticks([]) # Remove x-axis labels plt.tight_layout() # Adjust layout plt.savefig(f"{home_path}/portfolio_trend_{inputs.quote_currency.lower()}.png") # Save plot plt.close() # Close plot else: # Handle failed portfolio request try: error_details = response.json() # Parse error details error_message = error_details.get("message", "An error occurred") # Extract error message except json.JSONDecodeError: # Handle non-JSON response error_message = response.text output.error = f"Error: {error_message} (Status Code: {response.status_code})" # Set error return output # Return output object
Feel free to extend this tool further by adding additional features or integrating with other APIs !
Shinkai automates tool metadata generation, but you can enhance it.
Good tool metadata should include :
an explicit tool title,
a thorough description (features, options, requirements, extra information),
explicit descriptions for configurations and inputs,
adequate usable keywords to trigger the tool.
Go to the metadata section, and improve the above. Below is a good metadata for the tool.
Metada json
Copy
Ask AI
{ "homePage": "", "configurations": { "type": "object", "properties": { "min_quote_value_ft": { "description": "Minimum value in quote currency for fungible tokens to be included in the graph. This is used to filter out dust tokens from the graph. Default is 1 (1 USD or 1 EUR).", "type": "string" }, "min_quote_value_lp": { "description": "Minimum value in quote currency for liquidity positions to be included in the graph. This is used to filter out eventual residual positions. Default is 1 (1 USD or 1 EUR).", "type": "string" }, "min_quote_value_nft": { "description": "Minimum value in quote currency value for NFT collections to be include in the graph. Default is 1 (1 USD or 1 EUR).", "type": "string" }, "taptools_api_key": { "description": "The API key for TapTools", "type": "string" } }, "required": [ "taptools_api_key" ] }, "parameters": { "type": "object", "properties": { "address": { "description": "Cardano wallet address to scan.", "type": "string" }, "generate_portfolio_trended_value_graphs": { "description": "Whether to generate portfolio trended value graphs. yes or no. Default is yes.", "type": "string" }, "generate_positions_plots": { "description": "Whether to generate plots for portfolio positions. yes or no. Default is yes.", "type": "string" }, "portfolio_trended_value_timeframe": { "description": "The timeframe to use for portfolio trended value graphs. Options are 24h, 7d, 30d, 90d, 180d, 1y, all. Default is 30d.", "type": "string" }, "quote_currency": { "description": "The quote currency to use. USD or EUR. Default is USD.", "type": "string" } }, "required": [ "address" ] }, "result": { "type": "object", "properties": { "ada_price_in_quote": { "description": "The price of ADA in the quote currency", "type": "number" }, "error": { "description": "Error message if any", "nullable": true, "type": "string" }, "portfolio_positions": { "description": "The portfolio positions data", "type": "object" } }, "required": [ "portfolio_positions", "ada_price_in_quote" ] }, "sqlTables": [], "sqlQueries": [], "oauth": [], "runner": "any", "operating_system": [ "linux", "macos", "windows" ], "tool_set": "", "keywords": [ "portfolio", "taptools", "cardano", "address", "tracker" ], "version": "1.0.0"}