zeam-pass (Python)
ZEAM :: Pass for your own server. Define your tools once; agents pay per call over MCP and x402 HTTP on your domain. The Pass engine runs in your process: keys, channel records, gate usage and settlement stay on your server. ZEAM runs the relay (gas for transactions that cover their own gas) and the credit service (gate checks). You keep 90.01% of every sale, paid to your own split; the relay pays the gas.
Install
Published: pip install zeam-pass (Python ≥3.9).
Dependencies: coincurve (libsecp256k1, signing) and pycryptodome (Keccak, key sealing).
Start
from zeam_pass import Pass
agents = Pass(name="acme", payout="0xYourWalletAddress", price="0.01")
@agents.tool(description="A price for a symbol.",
input_schema={"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]})
def quote(symbol):
return {"symbol": symbol, "price": lookup(symbol)}
FastAPI / Starlette:
app.mount("/agents", agents.asgi())
Flask (werkzeug):
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/agents": agents.wsgi()})
Routes: /agents/mcp (MCP; payment in the tool call's _meta), POST /agents/v1/<tool> (x402 headers),
/agents/openapi.json and /agents/refund, with CORS. Django mounts agents.wsgi() or agents.asgi() the same
way. examples/server.py is a whole server on the standard library.
The agent side: zeampass.com/docs/agents.
Modes
mode |
What an agent does |
|---|---|
paywall (default) |
pays price USD per call with x402 batch-settlement (USDC on Base) |
gate |
signs each call with its key as a zero-value x402 exact payment: nothing is paid, the key needs no funds; admit=[...] lists the admitted key addresses; a grant (x-grant) from one of them admits one other key |
both |
its key must be admitted; it pays per call |
name (2 to 32 of a-z, 0-9 and -, starting with a letter) and payout (the wallet address earnings go to) fix your
split's address: keep both once you have sold.
Other keyword arguments to Pass(...):
| Argument | Default | |
|---|---|---|
price |
USD per call, up to 6 decimals ("0.02"); the default for tools with no price of their own |
|
prices |
per tool: {"tool": "0.05"} or {"tool": {"price": "0.05", "unit": "0.0001"}}, or a callable (tool, args) -> spec; a paywall needs price or prices |
|
free |
[] |
tool names served with no payment and no admission |
time |
none | line time: {"block": "0.00025", "blockMs": 250, "idleMs": 0, "maxBlocks": 14400}; paywall and both only; adds buy_time, line and POST <base>/line |
free_limit |
none | N or {"perHour": N}: N free calls per tool per client address per clock hour, kept in state_dir/free.json (a restart keeps the hour); no client address: one shared count; over it: 429 free_limit, retry-after |
site |
the request's origin | your public origin, used in the 402's resource and refund URL |
admit |
[] |
admitted key addresses |
on_empty |
"refuse" |
gate credit at 0: "refuse", or "allow" and warn |
contact |
none | an https:// URL or a mailto: address; goes in every 402, in the 403 refused answer (how and contact) and in openapi.json info.contact |
state_dir |
$PASS_STATE_DIR or ~/.zeam-pass/<name> |
keys, channel records, gate usage |
rpc |
https://mainnet.base.org |
a Base RPC URL |
relay |
https://api.zeampass.com/relay |
ZEAM's relay |
credits |
https://api.zeampass.com/credits |
ZEAM's gate credit service |
refund_url |
the refund URL the 402 names | |
tick_seconds |
60 |
seconds between background runs; 0 turns the thread off |
server_name, version |
the MCP handshake's server info | |
fee_recipient, credit_issuer |
$PASS_FEE_RECIPIENT, $PASS_CREDIT_ISSUER |
development only |
payout_is_fee_recipient |
False |
True only when payout is the fee address: the split pays it 100% |
settings |
engine tuning; gateFreePerMonth and gateCreditBlock set the gate's free checks and credit block, for tests |
The Python plugin does not read pass.json.
@agents.tool(...) keyword arguments:
| Argument | Default | |
|---|---|---|
name, description, input_schema |
the function's name and docstring, {"type": "object"} |
|
price |
prices[name], then price |
USD per call; an invalid price raises at registration |
unit |
none | USD per unit: the call reserves price; the tool calls zeam_pass.units(n); charge = min(price, n × unit) |
free |
False |
True: no payment, no admission; arguments still validated |
meter |
none | "time": on a line (x-line) the call burns the line's time; without one it is a paid call bounded to floor(price × blockMs / block) ms (past it: 402 out_of_time, not charged); needs time, never free |
Order: the tool's price/unit, then prices, then price. The 402 carries this call's amount in accepts,
pricing for this call and prices for every tool; tools/list entries carry _meta["zeam-pass/price"]; openapi.json
operations carry x-price. A price the callable cannot give: 500 price_invalid. gate mode never prices.
The gate
import os
from zeam_pass import Pass, sign_grant
agents = Pass(name="acme", payout="0xYourWalletAddress", mode="gate", admit=["0xAgentKeyAddress"])
grant = sign_grant(os.environ["ADMITTED_KEY"], "0xTheirKeyAddress", "2026-12-31T00:00:00Z", scope="quote")
An agent signs each call with its own key; nothing is paid, nothing goes on chain. sign_grant(key, delegate, until, scope="*"): key is the private key of an address in admit, delegate the address of the key you admit, until
an ISO time or a timezone-aware datetime, scope one tool name or "*". It returns the x-grant header value for
the agent that holds delegate. A call signed by any other key is refused bad_grant; so is one past until.
Removing the signing key from admit ends every grant it signed.
Agent side, in Python: new_key() makes a key; key_address(key) is the address the seller lists;
gate_proof(key, payment_required) signs the zero-value row of a gate's 402 (the body, or its PAYMENT-REQUIRED
header) and returns the PAYMENT-SIGNATURE value. Pass a gate.
Keys
On first start the plugin writes its keys to state_dir: the settle key (signs your claims and refunds) and, for
gate and both, a credit wallet, which holds the gate's USDC; agents.status() shows its address. Gate mode: send
it USDC on Base; it needs no ETH. Credit is 2,000 checks at a time for $1 USDC ($0.50 per 1,000), bought after the
month's 10,000 free checks are used and again below 200 remaining. Keep $1 USDC on Base in the credit wallet. both
mode needs no USDC in the credit wallet: paid calls are not gate checks.
keys.json has mode 0600 in a 0700 directory. PASS_KEY_SECRET seals the keys with AES-256-GCM (key from scrypt);
unsealed keys are sealed on the next start. Secret missing or wrong: paid calls answer 503 and nothing is served.
Put state_dir on storage that survives a redeploy, and back it up: it holds the keys and the channel records your
earnings are claimed from. agents.status() shows the settle key and credit wallet addresses, unclaimed and unpaid
earnings, and gate usage. Fields: Status.
A call
- Arguments are checked against your
input_schema(types,required,enum,minimum,maximum,minLength,maxLength, and the same for an object argument's properties, 1 level deep). Bad arguments, and bodies withNaN,Infinityor numbers too large for a float: 400 (MCP: an error result); nothing held. Properties the schema does not name are allowed, as in Node: your function gets the ones its signature takes (all of them with**kwargs). - The payment is verified and held.
- Your tool runs. With a
unit, it reports whole units withzeam_pass.units(n)(outside a tool run:RuntimeError). - The payment settles only on success. With a
unit: no report, or a bad one (negative, not whole, bool, str), is a failure: released, nothing charged. The settle response'sextracarrieschargedAmountandreservedAmount. The tool raised, returned a dict with"isError": True, or returned a value that is not valid JSON (a set, a non-finite float, a non-string dict key): the hold is released, nothing is charged; the last answers 500tool_failedwith the message every engine gives: "the result is not valid JSON (a non-finite number or a non-JSON value); nothing was charged".
The payment does not settle: the result is not delivered. Chain unreadable: nothing is served. Request bodies are capped at 1 MB.
A voucher settles in your process with no transaction. A deposit (the first call on a channel, and every top-up) goes through the relay and waits for its receipt: that call takes a few seconds.
Behind a proxy or on localhost, pass site="https://your.domain": the 402's resource and refund URL use that origin.
Flask routes you already have: @agents.paid holds, runs and settles the same way; an exception or a status of 400
or more releases the hold. Add the refund route:
@app.post("/quote")
@agents.paid
def quote():
...
app.add_url_rule("/refund", view_func=agents.refund_view(), methods=["POST"])
Line time
With time set, an agent buys time and spends it on a line with no payment per call:
buy_time {"blocks": n}(1 tomaxBlocks): a paid call of n ×block; the n ×blockMsms are credited to the paying channel once the payment settles, once per payment.POST <base>/line {"op": "open", "channelId"}(or the freelinetool): a message to sign with the payer key.{"op": "prove", "channelId", "nonce", "signature"}: the credential; the meter is on. A nonce is one attempt, 300 s.- Calls to a
meter="time"tool with the credential inx-line(MCP:_meta["zeam-pass/line"]or the request'sx-line) run until the time runs out; answers carryx-pass-ms-remainingandx-pass-ms-elapsed(MCP:_meta["zeam-pass/meter"]). Time burns while a call runs, once for calls at the same time, plusidleMsafter each. Refusals: 403line_unknown, 402meter_off,out_of_time,channel_leaving. {"op": "off"},"on","status","close"withcredentialorx-line.
zeam_pass.deadline_ms() inside a time tool: the epoch ms its call must end by (None elsewhere). The call runs in a
worker thread; past the deadline the caller gets out_of_time and a late result is discarded, so stop by then.
Claims and payouts take what burned; a refund switches the meter off, returns the unburned time with the balance
(timeReturnedMs) and closes the channel's lines. State: <state_dir>/meter.
Refunds
The buyer posts {"channelId", "issued", "signature"}, signed by the channel's payer over
ZEAM Pass refund\nchannel: <id>\nissued: <ISO time> within 5 minutes, plus channelConfig for a channel with no
calls. The balance goes back through the relay:
- The channel's spend times the fee address's share of your split (9.99%; 100% with
payout_is_fee_recipient), in whole micro-dollars, ≥ its deposit and refund gas × 1.15: ZEAM pays the gas."op": "refunded",gasMicroUSD: 0. - Otherwise: 409
"op": "refund_quote"(gas_payment_needed), an EIP-3009TransferWithAuthorizationof the quoted gas (15% margin) for the buyer to sign. The relay prices it; relay unreachable: the engine prices fromGAS(quotedBy: "engine"). Public check:https://api.zeampass.com/relay/quote. The buyer posts again with"gasPayment"; the relay sends the refund, then the payment, in one transaction. "selfSend": true: the buyer gets a signed refund of the full balance and sends it with ETH on Base for gas.- 1 refund per channel per hour; sooner:
refund_too_soonwithretry_after_seconds.
Every field and error: Errors and Refunds.
Background work
A daemon thread runs agents.tick() every tick_seconds (60): it stamps channels whose payer started a withdrawal,
claims earnings once the fee covers the gas and pays them out in the same relay request, and, in gate mode, buys
credit. Several workers can share one state_dir; overlapping ticks skip. tick_seconds=0 and agents.tick() from
your cron schedules it yourself. agents.payout_now() pays out now, or returns the transaction to send from your
wallet with ETH on Base for gas.
A paid call waits on the chain and, for a deposit, on the relay: serve with threads or workers (gunicorn, uvicorn, or
wsgiref with socketserver.ThreadingMixIn as in examples/server.py).
Tests
python -m unittest from this directory runs the plugin tests and the 5 fixture sets every engine shares
(wordpress/tests/vectors.json, settlement/fixtures.json, gate/fixtures.json, pricing/fixtures.json,
meter/fixtures.json).
Metadata
Release files for zeam-pass 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zeam_pass-1.0.0.tar.gz | 98.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zeam_pass-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 205.8 kB
Release files / zeam_pass-1.0.0.tar.gz
| Download URL | zeam_pass-1.0.0.tar.gz |
|---|---|
| Size | 98.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4d9748208e8b67aa2c4251261f3a1027aa6737798fcf485ee5e96b0d7ae0499
|
|
BLAKE2b-256 checksum How to use checksums |
22e1a59378e0fce2ecf9f6a1dd613986cb2799f300d666ca0849c3e08928f728
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / zeam_pass-1.0.0-py3-none-any.whl
| Download URL | zeam_pass-1.0.0-py3-none-any.whl |
|---|---|
| Size | 107.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3f867ec8d9de35ca3cda5e37892ee5df5555cb6022fe8f93c6d0bd3e8fe02847
|
|
BLAKE2b-256 checksum How to use checksums |
eae11156c985d10e1463b788ae092cb0e7d5496f6a71bce9b234d2ab7eafa8c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|