Metadata-Version: 2.5
Name: chitmark
Version: 0.6.3
Summary: Official Python SDK for Chitmark by Open Agent Ledger: trust decisions on agent-mediated actions (verify, feedback, challenge)
Project-URL: Homepage, https://chitmark.com
Project-URL: Documentation, https://chitmark.com/docs
Author-email: Open Agent Ledger <dev@chitmark.com>
License: Proprietary: no open-source grant. Copyright (c) 2026 Open Agent Ledger. All rights reserved.
License-File: LICENSE
Keywords: agent-actions,allow-challenge-deny,anti-abuse,chitmark,credit-abuse,free-tier-abuse,free-trial-abuse,open-agent-ledger,signup-abuse,trial-farming,trust-decisions
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: cryptography>=43.0.0; extra == 'dev'
Requires-Dist: mypy>=1.14.0; extra == 'dev'
Requires-Dist: pyjwt>=2.9.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Requires-Dist: respx>=0.22.0; extra == 'dev'
Requires-Dist: ruff>=0.9.0; extra == 'dev'
Provides-Extra: verdict
Requires-Dist: cryptography>=43.0.0; extra == 'verdict'
Requires-Dist: pyjwt>=2.9.0; extra == 'verdict'
Description-Content-Type: text/markdown

# chitmark (Python)

Official Python SDK for Chitmark: trust decisions on agent-mediated actions, tuned by business outcomes.

AI agents and multi-account farms drain free tiers, trial credits, and API allowances while looking exactly like your best customers. Chitmark scores each action in under 50 ms and returns `allow`, `challenge`, or `deny`. Then your outcomes (conversion, credit burn, chargeback) come back through `feedback` and tune the next decision.

[![PyPI version](https://img.shields.io/pypi/v/chitmark.svg)](https://pypi.org/project/chitmark/)

Requires Python >= 3.10. Fully typed (`py.typed`), synchronous `httpx` under the hood.

Try it without a key: [run the playground](https://chitmark.com/playground). Live service health: [chitmark.com/status](https://chitmark.com/status).

## Install

```bash
pip install chitmark
# or
uv add chitmark
```

## Quick start

```python
from chitmark import Chitmark

with Chitmark(api_key="ck_live_...") as client:
    verdict = client.verify(
        action="signup",
        session="sess_9f3a",
        surface="app.acme.com/signup",
        subject={
            "email": "buyer@acmecorp.com",  # hashed client-side before the wire
            "ip": "203.0.113.7",  # truncated to /24 client-side
            "userAgent": "Mozilla/5.0 ...",
        },
    )

# Persist verdict["eventId"] on the account row: feedback joins only on that id.
```

## The three verbs

| Method                  | Endpoint             | Purpose  |
| :---------------------- | :------------------- | :------- |
| `client.verify(...)`    | `POST /v1/verify`    | Decide   |
| `client.feedback(...)`  | `POST /v1/feedback`  | Learn    |
| `client.challenge(...)` | `POST /v1/challenge` | Escalate |

Store-outage degradation on verify is HTTP 200 with a Verdict
(``degraded: True``); the client deserializes it like any other Verdict.
Verify never returns HTTP 503. Timeouts and transport errors synthesize a
local degraded challenge Verdict and never raise on ``verify``. HTTP 4xx /
429 Error responses raise ``ChitmarkApiError``. Set ``on_degraded`` to
``challenge``, never allow on degraded.

Use `is_eligible_allow(verdict)` before any protected action: only an explicit non-degraded `allow` is eligible. Unknown or malformed decisions fail closed.

### Report outcomes

Store the `eventId` from verify on the account row, then report outcomes against that same id. Never guess or derive the id.

```python
# At signup: persist the join key
verdict = client.verify(action="signup", subject={"email": "a@b.com"})
db.accounts.update(user_id, chitmark_event_id=verdict["eventId"])

# Later, when a label matures:
client.feedback(
    event_id=account.chitmark_event_id,  # the stored join key
    outcome="credit_burn",
    value=87.4,  # measured magnitude: unit rides along (default "usd"; also "credits" | "count")
    observed_at="2026-08-06T04:00:00Z",
)
```

Exact duplicate feedback bodies derive the same warehouse id (`eventId` + `outcome` + `value` + `unit` + `observedAt`), so retrying a connector with a stable business timestamp never double-counts a burned value. Prefer an idempotency key; omitting `observedAt` uses server time. `unit` ships only alongside `value`.

### Handle a challenge

When verify returns `challenge`, issue one, solve the proof locally, and complete it. Proof-of-work difficulty is server-issued (4 by default, up to 6 at higher risk tiers): about 65k hashes, milliseconds for one real user, costly at farm scale.

```python
import hashlib

issued = client.challenge(event_id=verdict["eventId"], session="sess_9f3a")
instructions = issued["instructions"]

if instructions["type"] == "pow":
    prefix = "0" * instructions["difficulty"]
    nonce = 0
    while True:
        digest = hashlib.sha256(
            f"{issued['challengeId']}:{instructions['seed']}:{nonce}".encode()
        ).hexdigest()
        if digest.startswith(prefix):
            break
        nonce += 1

    client.complete_challenge(
        event_id=verdict["eventId"],
        challenge_id=issued["challengeId"],
        session="sess_9f3a",
        proof={"type": "proof_of_work", "nonce": str(nonce)},
    )
    # Re-verify with context={"challengeId": ...}; completion alone does not authorize.
```

### Verify the receipt

Every production verdict ships a `verdictToken`: an ES256 JWT bound to session, origin, and event. Verify authenticity, then authorize on the decision (install the `verdict` extra for the crypto dependency):

```python
pip install "chitmark[verdict]"
```

```python
from chitmark.verify_token import verify_verdict_token

claims = verify_verdict_token(
    verdict["verdictToken"],
    session="sess_9f3a",
    tenant_id="org_acme",
)

if claims["degraded"] or claims["decision"] != "allow":
    raise ChallengeRequiredError()  # HTTP 428 / challenge response

# Only now trust the allow.
```

Rejects expired tokens, unknown keys, bad signatures, and session or origin mismatches with typed error codes. A verified token that is not a non-degraded `allow` must still fail closed.

Cache JWKS using HTTP headers: respect `Cache-Control` (`max-age=60`), and when `ETag` or `Last-Modified` are present use conditional requests (`If-None-Match` / `If-Modified-Since`) instead of a hard-coded refresh interval. Refresh immediately on unknown `kid`. Do not fetch on every verification. Every signed JWT requires `kid`. Keys are EC P-256 for ES256: require JWT `alg === "ES256"` before verifying (`wrong_algorithm` otherwise); never trust the token-requested alg. Rotation overlaps old and new keys; the old key stays until tokens under it expire (keep ≥ 5 minutes + 60 seconds). An empty JWKS (`keys: []`) means verification is impossible: fail closed; never treat an empty set as proof a token is valid (local/CI may also emit `unsigned.<eventId>`). Pass a cached `jwks=` mapping into `verify_verdict_token` to skip the network call.

### PII modes

| Mode               | Behavior                                                  |
| :----------------- | :-------------------------------------------------------- |
| `hashed` (default) | SHA-256 email, /24 IP truncation, allowlisted form fields |
| `none`             | Derived/header-shape signals only                         |
| `raw`              | Tenant opt-in only; higher compliance review              |

### Dependency injection and lifecycle

The client owns its `httpx.Client` by default and closes it on context exit. Pass your own for connection pooling or tests:

```python
import httpx
from chitmark import Chitmark

pool = httpx.Client(base_url="https://api.chitmark.com", timeout=0.8)
client = Chitmark(api_key="ck_live_...", http_client=pool)
```

## Develop

```bash
cd packages/sdk-python
uv sync --extra dev   # or: pip install -e ".[dev]"
pytest
ruff check .
```

## Agent integration

Using Cursor, Claude Code, Codex, or another coding agent? Point it at
[chitmark.com/SKILL.md](https://chitmark.com/SKILL.md), or paste this into your
prompt: `Integrate Chitmark into my app following https://chitmark.com/SKILL.md`.

## Resources

- [Agent integration skill](https://chitmark.com/SKILL.md)
- [API reference](https://chitmark.com/docs)
- [Quickstart](https://chitmark.com/docs/quickstart)
- [Playground](https://chitmark.com/playground)
- [Examples repository](https://github.com/nonameuserd/chitmark-examples)

## License

Proprietary: see [LICENSE](LICENSE).
