Skip to main content

zeroclick-sellers

Python helpers for ZeroClick seller integrations. Verify proxy signatures, check allowance before doing work, return the payment refusals ZeroClick expects, and report what was used.

The TypeScript sibling is @zeroclickai/sellers. Both implement the same wire format; this SDK pins it with the fixtures in tests/vectors/.

Install

pip install zeroclick-sellers

The API key needs both the usage:read and usage:write scopes.

Pick the right client

Your framework Client Guard
FastAPI, Starlette (ASGI) create_async_seller await zeroclick.guard(...)
Flask, Django (WSGI) create_seller zeroclick.guard(...)

Use the async client in ASGI apps. The blocking one would stall the event loop for the duration of every allowance check.

Quickstart (FastAPI)

import os
from fastapi import FastAPI, Request
from fastapi.responses import Response

from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_async_seller
from zeroclick_sellers.adapters import zc_request_from_asgi_scope

zeroclick = create_async_seller(
    signing_secrets={
        os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
            "ZEROCLICK_SIGNING_SECRET"
        ]
    },
    api_key=os.environ["ZEROCLICK_API_KEY"],
)
app = FastAPI()


def to_fastapi(response: ZcResponse) -> Response:
    return Response(
        content=response.body,
        status_code=response.status,
        headers=dict(response.headers),
    )


@app.post("/v1/product-watch")
async def product_watch(request: Request) -> Response:
    zc_request = zc_request_from_asgi_scope(request.scope, await request.body())

    decision = await zeroclick.guard(
        zc_request,
        service_slug="product-watch",
        usage=[UsageItem(meter_slug="requests", quantity=1)],
    )
    if decision.action == "deny":
        return to_fastapi(decision.response)

    result = do_the_work(owner=decision.context.zc_agent_id)

    return to_fastapi(
        zeroclick.with_usage(
            ZcResponse.json(result),
            [
                SyncUsageItem(
                    service_slug="product-watch", meter_slug="requests", quantity=1
                )
            ],
        )
    )

Flask and Django are the same shape with create_seller, no await, and zc_request_from_wsgi_environ(request.environ, request.get_data()). Complete runnable versions of all three are in examples/, and the end-to-end suite runs them as real servers.

Why the adapters exist

path_and_query must be the raw, percent-encoded request target. Every framework hands you a decoded one. Observed for GET /v1/items/a%2Fb%20c:

decoded (unusable) raw (correct)
ASGI / uvicorn scope["path"]/v1/items/a/b c scope["raw_path"]/v1/items/a%2Fb%20c
WSGI / werkzeug PATH_INFO/v1/items/a/b c RAW_URI/v1/items/a%2Fb%20c?…

Using the decoded path produces a different canonical string and fails verification. The adapters handle this, including the fact that ASGI's raw_path excludes the query string while WSGI's RAW_URI includes it.

[!NOTE] On WSGI, if the server sets neither RAW_URI nor REQUEST_URI, an encoded separator cannot be recovered — WSGI decodes %2F to / before the SDK is called and nothing can tell it from a literal /. gunicorn, werkzeug, uWSGI and nginx all set one of them.

Decisions, not exceptions

guard returns a decision. A bad signature is an expected event, not a programming error, so it does not raise:

  • action == "allow" carries the verified context and an allowance of "allowed", "unavailable", or "not_required".
  • action == "deny" carries a ready-to-return response: 401 for a bad signature, the exact seller 402 payment_required body for a business denial, 503 when allowance is unavailable under a fail-closed policy.

A verified request whose context.zc_agent_id is None is a signed anonymous probe — valid, not a failure.

Charging up to a maximum

When the price is not known until the work is done, declare the ceiling with max_quantity instead of quantity:

usage = [
    UsageItem(meter_slug="requests", quantity=1),
    UsageItem(meter_slug="output_tokens", max_quantity=100_000),
]

The buyer authorises up to that ceiling and settles at the actual amount you report, so a ceiling never overcharges. An item declaring both is rejected.

Free identity-scoped endpoints

For endpoints that cost nothing but must know who is calling, use guard_identity. It verifies the signature exactly like guard, makes no allowance call, and denies an unidentified buyer with the usage: [] body that ZeroClick answers with a free identity challenge.

Allowance outages

Configure the behaviour on create_seller:

  • "allow" (default) — allow with allowance == "unavailable".
  • "deny" — return the SDK's 503.
  • "throw" — raise ZCError for your application to handle.

Use on_allowance_unavailable for operational logging. The policy applies only to allowance-API failures after a signature verifies — it never applies to a missing or invalid signature.

Asynchronous usage

For work that finishes after the response, report it with a stable, seller-owned idempotency key:

result = zeroclick.report_usage(
    zc_agent_id="zcagent_example",
    idempotency_key="job_123_output_tokens",
    service_slug="research-api",
    meter_slug="output_tokens",
    quantity=4200,
)

report_usage does not generate idempotency keys and does not retry. A duplicate=True result means the key already landed — a success, not an error.

Encrypted bodies

If your services opt into body encryption, the request arrives as a Compact JWE and the reply goes back the same way. The signature covers the ciphertext, so guard runs first and unchanged:

from zeroclick_sellers import decrypt_request, encrypt_response

raw_body = await request.body()
zc_request = zc_request_from_asgi_scope(request.scope, raw_body)

decision = await zeroclick.guard(
    zc_request,
    service_slug="product-watch",
    usage=[UsageItem(meter_slug="requests", quantity=1)],
)
if decision.action == "deny":
    return to_fastapi(decision.response)

envelope = decrypt_request(raw_body, resolve_private_key=lookup_private_key)
payload = json.loads(envelope.plaintext)

...

return to_fastapi(encrypt_response(stamped_response, envelope))

resolve_private_key receives the kid from the JWE protected header and returns that key, or None if it is unknown. encrypt_response returns the response unchanged when the request carried no reply key, so the same handler serves encrypted and plaintext buyers.

The suite is fixed at ECDH-ES+A256KW / A256GCM; anything else is refused. A reply key arriving with private material is rejected outright rather than used.

Development

uv sync
uv run pytest

The suite imports the working tree, so packaging problems would not show up there. Check the built artifact separately before publishing:

uv build
python -m venv /tmp/check && /tmp/check/bin/pip install dist/*.whl
cd /tmp && /tmp/check/bin/python -c "import zeroclick_sellers"

The suite covers three layers: the wire vectors in tests/vectors/ — generated independently of this SDK, and in the JWE case taken straight from the TypeScript implementation — sync/async parity across every allowance scenario and policy, and an end-to-end suite that runs the FastAPI, Flask and Django examples as real servers over real HTTP.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zeroclick_sellers-0.0.1.tar.gz (98.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zeroclick_sellers-0.0.1-py3-none-any.whl (23.7 kB view details)

Uploaded Python 3

File details

Details for the file zeroclick_sellers-0.0.1.tar.gz.

File metadata

  • Download URL: zeroclick_sellers-0.0.1.tar.gz
  • Upload date:
  • Size: 98.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for zeroclick_sellers-0.0.1.tar.gz
Algorithm Hash digest
SHA256 b5a73d4fb9867136b5eff8eb480e5081280822a08ec8c0aae208c05b27db0983
MD5 4c338a8cb87fd0a726c17f125374be8c
BLAKE2b-256 2e93663eb173079eb0843fbe99c42a2aa5ae5715fa1bc154eafd61c78cbc6669

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeroclick_sellers-0.0.1.tar.gz:

Publisher: publish-python-sdk.yml on piedotorg/zeroclick

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zeroclick_sellers-0.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for zeroclick_sellers-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 aa53c4446c5d6c4e61d7f078e2f896afcad8cf6f9c96504eb34b80f2ff91b7d7
MD5 ca523e1b2b79276664ccef1abe1a5fb2
BLAKE2b-256 4039b7c68b81d33bdd9d914dd768dc229e7863f8a672ee3fa124ed1156c2c1c4

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeroclick_sellers-0.0.1-py3-none-any.whl:

Publisher: publish-python-sdk.yml on piedotorg/zeroclick

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page