Skip to main content

Overview

DebtStack is designed for AI agents. The API returns structured JSON that’s easy for LLMs to parse, supports field selection to minimize context usage, and provides atomic primitives that compose naturally.
New to DebtStack? Start with our interactive demo scenarios to see the API in action before building agents.

Design Principles

1. Atomic Primitives

Each endpoint does one thing well. Agents can compose primitives to answer complex questions:
"Which MAG7 company has the highest leverage?"
    → search.companies(ticker=MAG7, sort=-net_leverage)

"Who guarantees RIG's 8% 2027 notes?"
    → resolve.bond(q="RIG 8% 2027")
    → traverse.entities(bond_id, relationship=guarantees)

2. Field Selection

Request only what you need to minimize token usage:
# Good: Specific fields
requests.get("/v1/companies?ticker=AAPL&fields=ticker,name,net_leverage_ratio")

# Avoid: All fields (wastes tokens)
requests.get("/v1/companies?ticker=AAPL")

3. Structured Errors

Errors include actionable suggestions:
{
  "error": {
    "code": "INVALID_TICKER",
    "message": "Company 'APPL' not found",
    "details": {
      "suggestion": "Did you mean 'AAPL'?"
    }
  }
}

Agent Architecture

ReAct Pattern

DebtStack works well with ReAct (Reasoning + Acting) agents:
TOOLS = """
You have access to these DebtStack tools:

1. search_companies(filters, fields) - Find companies by leverage, sector, debt metrics
2. search_bonds(filters, fields) - Find bonds by yield, seniority, maturity
3. resolve_bond(query) - Map bond description to CUSIP
4. get_pricing(cusips) - Get YTM, spread, last price
5. traverse_entities(start, relationship) - Find guarantors, subsidiaries
6. search_documents(query, section_type) - Search SEC filings
"""

PROMPT = f"""
{TOOLS}

When answering credit analysis questions:
1. Think about what data you need
2. Use the appropriate tool(s)
3. Analyze the results
4. Provide a clear answer

Question: {{question}}
"""

Tool Definitions

def search_companies_tool(ticker=None, sector=None, min_leverage=None, fields=None):
    """
    Search for companies in the DebtStack database.

    Args:
        ticker: Comma-separated tickers (e.g., "AAPL,MSFT")
        sector: Filter by sector (e.g., "Technology")
        min_leverage: Minimum leverage ratio
        fields: Fields to return (e.g., "ticker,name,net_leverage_ratio")

    Returns:
        List of companies matching the criteria
    """
    params = {}
    if ticker:
        params["ticker"] = ticker
    if sector:
        params["sector"] = sector
    if min_leverage:
        params["min_leverage"] = min_leverage
    if fields:
        params["fields"] = fields

    response = requests.get(f"{BASE_URL}/companies", params=params, headers=HEADERS)
    return response.json()["data"]


def traverse_entities_tool(bond_cusip=None, company_ticker=None, relationship="guarantees"):
    """
    Traverse entity relationships to find guarantors, subsidiaries, etc.

    Args:
        bond_cusip: CUSIP of bond to find guarantors for
        company_ticker: Company to traverse from
        relationship: "guarantees", "subsidiaries", "parents", "debt"

    Returns:
        List of related entities
    """
    if bond_cusip:
        start = {"type": "bond", "id": bond_cusip}
    else:
        start = {"type": "company", "id": company_ticker}

    response = requests.post(
        f"{BASE_URL}/entities/traverse",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={
            "start": start,
            "relationships": [relationship],
            "direction": "inbound" if relationship == "guarantees" else "outbound",
            "fields": ["name", "entity_type", "jurisdiction", "is_guarantor"]
        }
    )
    return response.json()["data"]["traversal"]["entities"]

Example: Credit Analysis Agent

from openai import OpenAI

client = OpenAI()
API_KEY = "ds_xxxxx"
HEADERS = {"X-API-Key": API_KEY}
BASE_URL = "https://api.debtstack.ai/v1"

# Define tools
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_companies",
            "description": "Search for companies by leverage, sector, or debt metrics",
            "parameters": {
                "type": "object",
                "properties": {
                    "ticker": {"type": "string", "description": "Comma-separated tickers"},
                    "sector": {"type": "string", "description": "Business sector"},
                    "min_leverage": {"type": "number", "description": "Minimum leverage ratio"},
                    "max_leverage": {"type": "number", "description": "Maximum leverage ratio"},
                    "fields": {"type": "string", "description": "Fields to return"}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_bonds",
            "description": "Search for bonds by yield, seniority, or maturity",
            "parameters": {
                "type": "object",
                "properties": {
                    "ticker": {"type": "string", "description": "Company ticker"},
                    "min_ytm": {"type": "number", "description": "Minimum yield to maturity"},
                    "seniority": {"type": "string", "description": "senior_secured, senior_unsecured, subordinated"},
                    "fields": {"type": "string", "description": "Fields to return"}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_guarantors",
            "description": "Find which entities guarantee a specific bond",
            "parameters": {
                "type": "object",
                "properties": {
                    "cusip": {"type": "string", "description": "Bond CUSIP"},
                },
                "required": ["cusip"]
            }
        }
    }
]


def execute_tool(name, args):
    """Execute a tool and return results."""
    if name == "search_companies":
        params = {k: v for k, v in args.items() if v}
        response = requests.get(f"{BASE_URL}/companies", params=params, headers=HEADERS)
        return response.json()["data"]

    elif name == "search_bonds":
        params = {k: v for k, v in args.items() if v}
        if "min_ytm" in params:
            params["has_pricing"] = True
        response = requests.get(f"{BASE_URL}/bonds", params=params, headers=HEADERS)
        return response.json()["data"]

    elif name == "get_guarantors":
        response = requests.post(
            f"{BASE_URL}/entities/traverse",
            headers={**HEADERS, "Content-Type": "application/json"},
            json={
                "start": {"type": "bond", "id": args["cusip"]},
                "relationships": ["guarantees"],
                "direction": "inbound",
                "fields": ["name", "entity_type", "is_guarantor"]
            }
        )
        return response.json()["data"]["traversal"]["entities"]


def chat(question):
    """Run the agent."""
    messages = [
        {"role": "system", "content": "You are a credit analyst assistant. Use the tools to answer questions about corporate debt."},
        {"role": "user", "content": question}
    ]

    while True:
        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=tools
        )

        message = response.choices[0].message

        if message.tool_calls:
            messages.append(message)
            for tool_call in message.tool_calls:
                result = execute_tool(
                    tool_call.function.name,
                    json.loads(tool_call.function.arguments)
                )
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(result)
                })
        else:
            return message.content


# Example usage
answer = chat("Which of the MAG7 companies has the highest leverage?")
print(answer)

Best Practices

Minimize Token Usage

  1. Use field selection - Only request fields you need
  2. Use batch operations - Combine multiple calls
  3. Limit results - Use limit parameter

Handle Errors Gracefully

def safe_search(query):
    try:
        response = requests.get(f"{BASE_URL}/bonds/resolve", params={"q": query}, headers=HEADERS)
        data = response.json()

        if "error" in data:
            return f"Could not find bond: {data['error']['details'].get('suggestion', query)}"

        return data["data"]["matches"]
    except Exception as e:
        return f"Error: {str(e)}"

Cache Frequently Used Data

from functools import lru_cache

@lru_cache(maxsize=100)
def get_company_info(ticker):
    response = requests.get(
        f"{BASE_URL}/companies",
        params={"ticker": ticker, "fields": "ticker,name,sector,total_debt,net_leverage_ratio"},
        headers=HEADERS
    )
    return response.json()["data"][0] if response.json()["data"] else None

Next Steps