Metadata-Version: 2.4
Name: metrify-sdk
Version: 0.2.4
Summary: Billing SDK for MCP servers
License: MIT
Project-URL: Homepage, https://github.com/mangelico/metrify-sdk
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-mock>=3.12; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"

# metrify-sdk

> Billing infrastructure for MCP servers.
> Add one decorator. Start charging. Get paid in USDT.

---

## Installation

```bash
pip install metrify-sdk
```

Requires Python 3.8+

---

## Quickstart (Provider)

**Step 1** — Register at [metrify.dev](https://metrify.dev) and get your `pk_live_...` provider key.

**Step 2** — Install the SDK:

```bash
pip install metrify-sdk
```

**Step 3** — Write two functions: an inner one that does the billed work, and an outer one that the MCP framework actually exposes:

```python
from metrify import Metrify
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-mcp-server")
m = Metrify(provider_key="your_provider_key")

# Inner — billing-wrapped. Metrify needs to know WHICH consumer to check the
# balance of and charge, so consumer_api_key must be the first parameter.
@m.tool(price=0.01, description="Search the web for a query")
def search(consumer_api_key: str, query: str) -> str:
    # Your logic here
    return f"Results for: {query}"

# Outer — the tool an agent actually calls over MCP. Its job is to figure
# out WHICH consumer is calling (from the incoming request's Bearer token)
# and pass that into the inner, billed function above.
@mcp.tool()
def search_mcp(query: str) -> str:
    consumer_api_key = resolve_consumer_api_key()  # see below
    return search(consumer_api_key=consumer_api_key, query=query)

if __name__ == "__main__":
    mcp.run(transport="streamable-http")
```

**This two-layer split is required, not optional.** `@m.tool()` bills a
specific consumer, so it needs their `api_key` — but it doesn't see the raw
HTTP request, so it can't extract that itself. Today, resolving
`consumer_api_key` from the incoming `Authorization: Bearer <JWT>` header
is **on you, the provider** — the SDK doesn't do it for you yet. In
practice that means validating the JWT (RS256, verified against Metrify's
published JWKS) and reading its `sub` claim.

You don't have to build that from scratch: [examples/auth_middleware_example.py](examples/auth_middleware_example.py)
is a complete, copyable JWT-validation middleware — adapted from the exact
code metrify-demo runs in production — that does this for you and exposes
the resolved key via a `ContextVar` your outer tool function can read. It's
a standalone example file (not an importable part of this package yet), so
copy it into your project and wire it into your ASGI app as shown at the
bottom of that file.

---

## How billing works

When a consumer calls your tool, Metrify verifies their balance before execution.
If sufficient, the tool runs. On success, the consumer wallet is debited atomically:
95% goes to you, 5% to Metrify. If your tool raises an exception, the consumer is
not charged.

```
Consumer wallet → [Metrify] → Provider wallet
    $0.01000           ↓          $0.00950
                  fee: $0.00050
```

---

## @m.tool() parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `price` | float | ✓ | Cost per call in USDT |
| `description` | str | ✓ | Tool description shown to agents |

---

## Policy: no charge on upstream error

If your tool raises any exception, the consumer is not charged.
Raise `UpstreamError` explicitly when an external API you depend on fails:

```python
from metrify import Metrify, UpstreamError

m = Metrify(provider_key="your_provider_key")

@m.tool(price=0.05, description="Summarize text using an LLM")
async def summarize(consumer_api_key: str, text: str) -> str:
    try:
        result = await llm_client.complete(text)
    except Exception as e:
        raise UpstreamError(f"LLM unavailable: {e}")
    return result
```

The consumer receives your error message and is not charged.
Idempotency keys are implemented from day one — duplicate charges are impossible.

---

## Provider registration

[metrify.dev](https://metrify.dev) — currently invite-only waitlist.

---

## Links

- [metrify.dev](https://metrify.dev)
- [metrify-mcp](https://github.com/mangelico/metrify-mcp) — consumer wallet management MCP server
- [Changelog](https://github.com/mangelico/metrify-sdk/releases)

---

## License

MIT
