Skip to main content
Glama

hotmart_subscriber_360_app

Access a subscriber's complete profile and purchase history, including LTV, quantity, ticket, and detailed purchase records. Ideal for reviewing subscriber data.

Instructions

Visão 360 de um assinante — perfil + histórico de compras.

Cards de LTV/qty/ticket + DataTable com purchases. Use pra 'ver dados do assinante X', 'histórico completo do código Y'.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
subscriber_codeYesSubscriber code (Hotmart alphanumeric, ex: 'VRWIQQRG').

Implementation Reference

  • Main handler function for hotmart_subscriber_360_app. Calls Hotmart API to fetch subscriber purchases, computes LTV/total/qty/average ticket, and builds a PrefabApp UI dashboard with Cards (LTV, purchases, average ticket) and a DataTable of purchase history.
    async def hotmart_subscriber_360_app(subscriber_code: str) -> PrefabApp:
        """Visão 360 de um assinante — perfil + histórico de compras.
    
        Cards de LTV/qty/ticket + DataTable com purchases. Use pra 'ver
        dados do assinante X', 'histórico completo do código Y'.
    
        Args:
            subscriber_code: Subscriber code (Hotmart alphanumeric, ex: 'VRWIQQRG').
        """
        client = get_client()
        raw = await client.get(
            f"/payments/api/v1/subscriptions/{subscriber_code}/purchases"
        )
        purchases: list[SubscriberPurchase] = parse_items(raw, SubscriberPurchase)
    
        def _value(p: SubscriberPurchase) -> float:
            if p.price and p.price.value is not None:
                return p.price.value
            return 0.0
    
        total_spent = sum(_value(p) for p in purchases)
        qty = len(purchases)
        sanity_warning = qty > 0 and total_spent == 0
    
        rows = []
        for p in purchases[:100]:
            rows.append({
                "transaction": p.transaction or "?",
                "date": epoch_ms_to_date(p.approved_date),
                "recurrence": str(p.recurrency_number) if p.recurrency_number else "—",
                "value": format_brl(_value(p)),
                "status": (p.status if p.status else None) or "?",
                "payment": (p.payment_type if p.payment_type else None) or "—",
            })
    
        with PrefabApp() as app:
            with Column(gap=4, css_class="p-6"):
                Heading(content=f"Assinante {subscriber_code}")
    
                if sanity_warning:
                    Heading(content="⚠️ LTV zerado com compras no histórico — possível schema drift", level=3)
    
                with Grid(columns=[1, 1, 1], gap=4):
                    with Card():
                        with CardHeader():
                            CardTitle(content="Total gasto (LTV)")
                        with CardContent():
                            Metric(label="LTV", value=format_brl(total_spent))
                    with Card():
                        with CardHeader():
                            CardTitle(content="Total de compras")
                        with CardContent():
                            Metric(label="Compras", value=str(qty))
                    with Card():
                        with CardHeader():
                            CardTitle(content="Ticket médio")
                        with CardContent():
                            Metric(
                                label="Médio",
                                value=format_brl(total_spent / qty) if qty else "—",
                            )
    
                with Card():
                    with CardHeader():
                        CardTitle(content="Histórico de compras")
                    with CardContent():
                        DataTable(
                            columns=[
                                DataTableColumn(key="transaction", header="Transação"),
                                DataTableColumn(key="date", header="Data", sortable=True),
                                DataTableColumn(key="recurrence", header="Recorrência"),
                                DataTableColumn(key="value", header="Valor"),
                                DataTableColumn(key="status", header="Status", sortable=True),
                                DataTableColumn(key="payment", header="Pagamento"),
                            ],
                            rows=rows,
                            search=True,
                        )
        return app
  • Module-level __all__ export that includes 'hotmart_subscriber_360_app' for auto-discovery by the server.
    __all__ = [
        "hotmart_subscriptions_health_app",
        "hotmart_churn_analyzer_app",
        "hotmart_subscriber_360_app",
    ]
  • Server registration: _discover_and_register_apps() iterates modules under hotmart_mcp.apps and registers any async function ending in '_app' as an MCP tool with app=True.
    def _discover_and_register_apps() -> int:
        """Import all modules under hotmart_mcp.apps and register apps (app=True)."""
        registered = 0
        for module_info in pkgutil.iter_modules(apps_pkg.__path__, prefix=f"{apps_pkg.__name__}."):
            if module_info.name.endswith("__init__"):
                continue
            module = importlib.import_module(module_info.name)
            for name, obj in inspect.getmembers(module, iscoroutinefunction):
                if name.startswith("_") or not name.endswith("_app"):
                    continue
                mcp.tool(app=True)(obj)
                registered += 1
        return registered
  • Imports for the handler: uses SubscriberPurchase model for type validation and _helpers (format_brl, epoch_ms_to_date, parse_items) for data formatting.
    """Subscriptions dashboards — Prefab UI apps (schema-validated)."""
    from __future__ import annotations
    
    from collections import Counter, defaultdict
    from typing import Optional
    
    from prefab_ui import PrefabApp
    from prefab_ui.components import (
        Card, CardContent, CardHeader, CardTitle, Column, DataTable,
        DataTableColumn, Grid, Heading, Metric,
    )
    from prefab_ui.components.charts import ChartSeries, LineChart, PieChart
    
    from hotmart_mcp._shared import get_client
    from hotmart_mcp.models import Subscription, SubscriberPurchase
    from hotmart_mcp.apps._helpers import (
        epoch_ms_to_date, format_brl, parse_items,
    )
  • Helper utilities used by the 360 app: format_brl (currency formatting), epoch_ms_to_date (timestamp conversion), parse_items (API response normalization), and safe_sum.
    """Shared helpers for Prefab UI apps — schema-validated."""
    from __future__ import annotations
    
    from datetime import datetime
    from typing import Any, TypeVar
    
    from pydantic import BaseModel, TypeAdapter
    
    T = TypeVar("T", bound=BaseModel)
    
    
    def format_brl(value: float | int | None) -> str:
        v = value or 0
        return f"R$ {v:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
    
    
    def epoch_ms_to_date(ms: int | None) -> str:
        if not ms:
            return ""
        return datetime.fromtimestamp(ms / 1000).strftime("%Y-%m-%d")
    
    
    def parse_items(raw: Any, model: type[T]) -> list[T]:
        """Validate an API response into a list of typed models.
    
        Hotmart's wrappers vary by endpoint — sometimes `{"items": [...]}`,
        sometimes a raw `[...]`. This normalises both into `list[model]`.
    
        Pydantic raises ValidationError if the response shape no longer
        matches the spec — surfaces schema drift loud instead of silently
        returning empty/zeroed data.
        """
        if isinstance(raw, dict):
            raw_items = raw.get("items", [])
        elif isinstance(raw, list):
            raw_items = raw
        else:
            raw_items = []
        return TypeAdapter(list[model]).validate_python(raw_items)
    
    
    def safe_sum(items, getter, *, label: str = "value") -> float:
        """Sum a numeric attribute from typed items, asserting non-zero
        when count > 0. Returns 0 for empty input. If items exist but
        every value is None/0, returns 0 — caller can render a warning."""
        total = 0.0
        for it in items:
            v = getter(it)
            if v is not None:
                total += v
        return total
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description bears full responsibility. It only states the tool shows a 360 view but does not disclose any behavioral traits such as data freshness, required permissions, or side effects. For a read-only data display, this is insufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is very concise, consisting of two sentences that front-load the purpose and provide usage examples. Every word contributes value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one parameter, no output schema), the description is mostly complete. It explains the purpose and gives example use cases. However, it could be more explicit about the exact fields returned (like LTV, qty, ticket) since they are mentioned in the UI context but not as structured output.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage for the single parameter subscriber_code, which is well-defined. The description adds no additional meaning beyond the schema, meeting the baseline for high coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it provides a '360 view of a subscriber' with profile and purchase history, and lists specific UI elements like LTV/qty/ticket cards and a DataTable. This distinguishes it from siblings like hotmart_subscriber_purchases_list which only lists purchases.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides example phrases like 'view data of subscriber X' and 'complete history of code Y', giving clear context for when to use the tool. However, it does not explicitly state when not to use it or mention alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/thaleslaray/hotmart-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server