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.

If your server is TypeScript rather than Python, use @zeroclickai/sellers instead — same wire format, same concepts.

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()) in place of the ASGI adapter.

Scoped usage keys

api_key carries both the usage:read and usage:write scopes. To follow least privilege you can instead pass two scoped keys — usage_read_key for allowance checks (guard / check_allowance) and usage_write_key for usage reporting (report_usage), handy when a separate worker reports usage — and omit api_key. A scoped key falls back to api_key when it is not set, so either form works.

zeroclick = create_async_seller(
    signing_secrets={...},
    usage_read_key=os.environ["ZEROCLICK_USAGE_READ_KEY"],
    usage_write_key=os.environ["ZEROCLICK_USAGE_WRITE_KEY"],
)

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 without a buyer identity is a signed anonymous probe — valid, not a failure. Test it with truthiness, not is None: an absent zc-agent-id header gives None, but a present-but-empty one gives "", and both mean the same thing. guard_identity already handles both.

if not decision.context.zc_agent_id:
    ...  # anonymous

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.

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.1.1.tar.gz (102.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.1.1-py3-none-any.whl (24.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: zeroclick_sellers-0.1.1.tar.gz
  • Upload date:
  • Size: 102.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.1.1.tar.gz
Algorithm Hash digest
SHA256 174bcae125c6ad52b9d1731197c56f150c79d60d3e54f0f8b6fb3f37307699d6
MD5 6727251d41f985e398769ad79d92a9e3
BLAKE2b-256 24c1e7aacae3088401ccaada632421ec9fb01b0e1d728354e20859169055c58b

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeroclick_sellers-0.1.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.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for zeroclick_sellers-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b5f8478ee63cacf8254a8f61f78f0122bc723fde14de40e81444e655a907cac2
MD5 0a1d75865c96a298a5585078bc25b056
BLAKE2b-256 72d4ba822cc70996ccf903dcc3ec887da8ad1bdeadc0c8cb66f9965866f3c66c

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeroclick_sellers-0.1.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