ZEAM::Bridge

01 · Node

@zeambridge/bridge

Requires
Node 18 or later
Version
the latest on npm
Source
github.com/zeam-labs/pass

Install

npm install @zeambridge/bridge express

app.mjs

import express from 'express'
import { bridge } from '@zeambridge/bridge'

const agents = bridge({
  name: 'acme',
  site: 'https://acme.example',
  mode: 'paywall',
  price: '0.02',
  payout: '0xYourWalletAddress',
})
agents.tool({
  name: 'add',
  description: 'Adds two numbers.',
  inputSchema: {
    type: 'object',
    properties: { a: { type: 'number' }, b: { type: 'number' } },
    required: ['a', 'b'],
  },
  run: async ({ a, b }) => ({ sum: a + b }),
})

const app = express()
app.use(agents.express('/agents'))
app.listen(3000)

Replace 0xYourWalletAddress, and the name and site values, before you run it: payout is a wallet address on Base that you control, and your earnings are paid to it. bridge() refuses the placeholder.

node app.mjs

It prints nothing when it starts. To check it, send the call an agent sends before it pays:

curl -i -X POST http://localhost:3000/agents/v1/add -H 'content-type: application/json' -d '{"a":2,"b":3}'

It answers 402 with your price, in the body and in the PAYMENT-REQUIRED header. Your tools are at http://localhost:3000/agents/mcp.

Routes you already have: put the paywall in front of them.

app.use('/reports', agents.paywall.express())

01 · Python

zeambridge

Requires
Python 3.9 or later
Version
the latest on PyPI
Installs
coincurve, pycryptodome
Source
github.com/zeam-labs/pass

Install

pip install zeambridge fastapi uvicorn

app.py

from fastapi import FastAPI
from zeam_bridge import Bridge

agents = Bridge(name="acme", payout="0xYourWalletAddress", price="0.01")

@agents.tool(description="Multiplies two numbers.", input_schema={"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"]})
def multiply(a, b):
    return {"product": a * b}

app = FastAPI()
app.mount("/agents", agents.asgi())

Replace 0xYourWalletAddress, and the name value, before you run it: payout is a wallet address on Base that you control, and your earnings are paid to it. Bridge() refuses the placeholder.

uvicorn app:app --host 0.0.0.0 --port 3000

uvicorn prints its own start lines and one line for each request; Bridge prints nothing. To check it, send the call an agent sends before it pays:

curl -i -X POST http://localhost:3000/agents/v1/multiply -H 'content-type: application/json' -d '{"a":2,"b":3}'

It answers 402 with your price, in the body and in the PAYMENT-REQUIRED header. Your tools are at http://localhost:3000/agents/mcp.

Flask

With Flask instead of FastAPI, the whole file is:

pip install zeambridge flask
from flask import Flask
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from zeam_bridge import Bridge

agents = Bridge(name="acme", payout="0xYourWalletAddress", price="0.01")

@agents.tool(description="Multiplies two numbers.", input_schema={"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"]})
def multiply(a, b):
    return {"product": a * b}

app = Flask(__name__)
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/agents": agents.wsgi()})
flask --app app run --host 0.0.0.0 --port 3000

The payout rule and the check are the same as for FastAPI.

Flask routes you already have take @agents.paid:

@app.post("/quote")
@agents.paid
def quote():
    ...

01 · WordPress

ZEAM Bridge

Requires
WordPress 6.0 or later, tested to 7.1
PHP
7.4 or later, with GMP or BCMath, and sodium
Version
2.0.0

A gate, a paywall or both in front of your posts for AI agents. Your pages and logged-in editors are unaffected.

Download ZEAM Bridge 2.0.0

Your site must be reachable from the internet and use pretty permalinks.

Set up

  1. Download the plugin: https://zeampass.com/downloads/zeam-pass-2.0.0.zip (its SHA-256 is next to it, at the same address plus .sha256). Plugins → Add New → Upload Plugin, choose the zip, Install Now.
  2. Before you activate, add an encryption key of at least 32 random characters to wp-config.php and keep it: define('ZEAM_BRIDGE_KEY', '…');
  3. Activate, then Settings → ZEAM Bridge.
  4. Connect your payout wallet: your browser wallet, or paste the address.
  5. Name, under Advanced on the settings page: with your wallet it fixes your split's address and holds your gate credit. It defaults to one made from your site title; set it before you take payments. A change moves new earnings to a new split and leaves gate credit bought before it behind, and the settings page refuses it while the current split has earnings not yet claimed: pay out first.
  6. Choose gate, paywall or both. Paywall: set a price per call; optional, a price or Free per tool, free calls an hour per address, and a line-time price with the milliseconds it buys. Line time is sold only once you add a tool that runs on time, with 'meter' => 'time' in the zeam_bridge_tools filter; until then the price is saved and nothing sells time. Gate: list the addresses of the keys you admit. "Contact for access": a web address or a mailto: address where an agent asks to be admitted. Agents see it in every 402 and when a key is refused. Empty: your site's address; your admin email is never shown.
  7. Save. The page shows your split, earnings, gate usage and the credit wallet to fund.
  8. Gate, optional, after you save: the Grants box signs a grant with your browser wallet, when it holds a key on your saved list.

Agents get two tools, search_posts and read_post, over your posts.

WordPress tools

Add a tool of your own with the zeam_bridge_tools filter. A name is a-z, 0-9 and _. run gets the arguments and a meter and returns an array or a WP_Error:

add_filter('zeam_bridge_tools', function ($tools) { $tools['word_count'] = ['description' => 'Counts words.', 'inputSchema' => ['type' => 'object', 'properties' => ['text' => ['type' => 'string']], 'required' => ['text']], 'price' => '0.05', 'unit' => '0.0001', 'run' => function ($args, $meter) { $n = str_word_count($args['text']); $meter->units($n); return ['words' => $n]; }]; return $tools; });

A tool that runs on time:

add_filter('zeam_bridge_tools', function ($tools) { $tools['until_deadline'] = ['description' => 'Says when its time ends.', 'meter' => 'time', 'run' => function ($args, $meter) { return ['deadlineMs' => $meter->deadlineMs()]; }]; return $tools; });

Keys: description, inputSchema, outputSchema (the result's JSON Schema, optional; listed in the 402's x402 bazaar discovery extension), price (USD, optional), unit (USD per unit, optional), free (optional), meter ('time', optional), run. A 'meter' => 'time' tool needs line time, is never free, and gets its deadline from $meter->deadlineMs() (epoch ms): on a line it spends the line's time; without one it is a paid call that runs for the milliseconds its price buys at your line time rate, and past that it answers 402 out_of_time, not charged. The names buy_time and line are taken. A price that is not USD with up to 6 decimals, $0.000001 to $9,007,199,254.740991, drops the tool. That top is not a business limit: it is the largest amount a number counts exactly in micro-USD. zeam_bridge_prices filter, optional, for tools with no price of their own: ($price, $tool, $args), $price starting null, returns a USD string or ['price' => '0.05', 'unit' => '0.001'], or null or '' for the price per call; a return that is not a price answers 500 price_invalid.

02 · Settings

Settings.

mode

Paywall, gate or both.

Node
mode
Python
mode
WordPress
Mode
Default
paywall

price

USD per call, up to 6 decimals ("0.02"); the price of a tool with none of its own; required for paywall and both unless prices is set.

Node
price
Python
price
WordPress
Price per call (USD)
Default
Node and Python none; WordPress always needs a price per call for a paywall or both, with or without per-tool prices, from 0.000001 to 9007199254.740991, at most 6 decimals

payout

The wallet address earnings go to; a gate's zero-value proofs name it, and nothing is paid to it.

Node
payout
Python
payout
WordPress
Payout wallet
Default
none

name

2 to 32 of a-z, 0-9 and -, starting with a letter; fixes your split's address.

Node
name
Python
name
WordPress
Name
Default
Node and Python none: you give one (with no name option, Node reads it from ./bridge.json); WordPress one made from your site title, or site- and eight characters when the title does not fit the rule

Whatever the engine, set the name before you take payments: with your payout wallet it fixes your split’s address, and it holds your gate credit. A change moves new earnings to a new split and leaves gate credit bought before it behind, so pay out first.

More settings

admit

The addresses of the keys a gate admits (gate and both).

Node
admit
Python
admit
WordPress
Admitted keys
Default
[]

contact

An http(s):// URL or a mailto: address; goes in every 402, in the 403 refused answer (how and contact) and in openapi.json info.contact.

Node
contact
Python
contact
WordPress
Contact for access
Default
Node and Python none; WordPress your site's address

site

Your public origin, used in the 402's resource and refund URL.

Node
site
Python
site
WordPress
your site
Default
Node and Python the request's origin; WordPress your site's address

onEmpty

Gate credit at 0: refuse, or allow and warn.

Node
onEmpty
Python
on_empty
WordPress
When gate credit runs out
Default
refuse

state dir

Keys, channel records, gate usage, line time and free-call counts.

Node
stateDir
Python
state_dir
WordPress
its database
Default
Node BRIDGE_STATE_DIR if set, otherwise .pass in the folder you start it from; Python BRIDGE_STATE_DIR if set, otherwise ~/.zeam-pass/<name>; WordPress keeps its state in its database

Formerly ZEAM :: Pass: @zeam-labs/pass, zeam-pass and import zeam_pass keep working and point here; Node's pass() is bridge(), and ./pass.json is still read. Each BRIDGE_… setting is still read under its PASS_… name (on a machine that also runs ZEAM :: Pass, set the BRIDGE_… names: both read PASS_STATE_DIR and PASS_NAME); on WordPress the plugin's folder and download stay zeam-pass, and ZEAM_PASS_KEY, the zeam_pass_tools and zeam_pass_prices filters and /wp-json/zeam-pass/ keep working.

03 · Pricing

Per call, per tool, per unit, by time.

Node

Per call
price
Per tool
price on tool(), or prices: { tool: '0.05' } or (tool, args) => price
Per unit used
unit on tool(); meter.units(n) in run
By time
time: { usd, ms, idleMs, maxMs, maxCalls }: that many dollars buys that many milliseconds; meter: 'time' on tool()
Free
free: ['ping'], or free: true on tool()
Free limit
freeLimit: 60

Python

Per call
price
Per tool
@agents.tool(price=...), or prices={"tool": "0.05"} or lambda tool, args: price
Per unit used
@agents.tool(unit=...); units(n)
By time
time={"usd": "0.00025", "ms": 250, "idleMs": 0, "maxMs": 60000, "maxCalls": 1000}; @agents.tool(meter="time")
Free
free=[...], or @agents.tool(free=True)
Free limit
free_limit=60

WordPress

Per call
Price per call (USD): from 0.000001 to 9007199254.740991, at most 6 decimals
Per tool
Per tool: a price per tool, from 0.000001 to 9007199254.740991, at most 6 decimals; the zeam_bridge_prices filter
Per unit used
unit in the zeam_bridge_tools filter; $meter->units($n)
By time
Line time (USD), from 0.000001 to 9007199254.740991, at most 6 decimals, and the ms it buys, 1 to 3,600,000 (empty for 250); 'meter' => 'time' in the zeam_bridge_tools filter
Free
Free, per tool
Free limit
Free calls an hour per address, a whole number from 1 to 1,000,000, or empty

Pricing rules

Per call

One price for every tool with none of its own.

Default
none; paywall and both need price or prices (WordPress: always a price per call)

Per tool

A tool's own price, else prices[tool], else price.

Default
none

Per unit used

The call reserves the tool's price. The tool reports whole units. Charge: units × unit, at most the reserve. No units reported: nothing charged.

Default
none

By time

The agent buys milliseconds with buy_time, in the smallest step your price divides into whole micro-dollars (any number of milliseconds when a millisecond costs a whole number of micro-dollars, as in the example), opens a line, and calls meter: 'time' tools on it with no payment per call. Time burns while a call runs; on Node and Python, plus idleMs after each, and maxMs in the same time object limits one purchase, if you set it. maxCalls (1 to 1,000, default 1,000) caps how many calls run at once on one line; the next is refused 429 too_many_calls. WordPress has none of these settings: no idle time, no maximum on a purchase, and 1,000 calls at once. An agent that names no amount buys ms. Unburned time goes back with a refund. Paywall and both only. Adds buy_time, line and POST <base>/line; on WordPress, once one of your tools runs on time.

Default
off; ms 250, idleMs 0, no maximum on a purchase, maxCalls 1000

Free

No payment, no key. Arguments still checked.

Default
none

Free limit

Free calls per tool, per client address, per clock hour. Callers whose address the seller cannot see share one count. Over it: 429 free_limit.

Default
no limit

app.mjs

import express from 'express'
import { bridge } from '@zeambridge/bridge'

const run = async () => ({ ok: true })
const agents = bridge({
  name: 'acme', payout: '0xYourWalletAddress', price: '0.02', freeLimit: 60,
  time: { usd: '0.00025', ms: 250, idleMs: 0 },
})
agents.tool({ name: 'report', price: '0.05', run })
agents.tool({ name: 'ping', free: true, run })
agents.tool({
  name: 'words', price: '0.05', unit: '0.0001',
  inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
  run: async ({ text }, meter) => {
    meter.units(text.length)
    return { ok: true }
  },
})
agents.tool({ name: 'scan', meter: 'time', run: async (args, { signal, deadline }) => ({ until: deadline }) })

const app = express()
app.use(agents.express('/agents'))
app.listen(3000)

app.py

from fastapi import FastAPI
from zeam_bridge import Bridge, deadline_ms, units

agents = Bridge(name="acme", payout="0xYourWalletAddress", price="0.02", free_limit=60,
              time={"usd": "0.00025", "ms": 250, "idleMs": 0})


@agents.tool(price="0.05")
def report():
    return {"ok": True}


@agents.tool(free=True)
def ping():
    return {"ok": True}


@agents.tool(price="0.05", unit="0.0001", input_schema={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]})
def words(text):
    units(len(text))
    return {"ok": True}


@agents.tool(meter="time")
def scan():
    return {"until": deadline_ms()}


app = FastAPI()
app.mount("/agents", agents.asgi())

report: $0.05 per call. ping: free, 60 calls an hour per address. words: reserves $0.05, charges $0.0001 per unit. scan: burns line time; without a line, a paid call of $0.02 that runs up to 20 s, the time that price buys at the line rate.

The 402 carries this call’s amount, pricing for it, and prices for every tool. tools/list: _meta["zeam-pass/price"]. OpenAPI: x-price.

Reference

Overview

A gate, a paywall or both in front of your API, MCP server or WordPress site. No OAuth, no sign-up, no account with us. Agents get in with a key. Install the plugin; connect your payout wallet.

  • Gate: admits the keys you list. Each call is signed with the agent's key as a zero-value x402 payment: nothing is paid, the key needs no funds or ETH, nothing goes on chain. A listed key can sign a grant that admits one other key, for one tool or all, until a set time.
  • Paywall: agents pay per call in USDC, from a wallet or a pass. You keep 90.01% of every sale. A failed call is not charged; on a line, a call uses the time it runs.
  • Both: listed keys, paid per call.

The engine runs in your process with your keys. ZEAM holds none of your money or keys and makes no admission decisions.

What it creates

Settle key

Node and Python make it on first run; the WordPress plugin makes it on activation. It signs your claims and refunds and holds no money. Node and Python keep it in keys.json in the state dir, mode 0600; set BRIDGE_KEY_SECRET to seal it. WordPress keeps it encrypted in its database. Back it up: a lost settle key cannot claim what your channels owe you.

Credit wallet

Made the first time your Bridge runs in gate or both mode. In gate mode, send it USDC on Base; that wallet needs no ETH. In both mode it needs nothing. Its key is kept where the settle key is. Back it up too: a lost credit key loses the USDC in that wallet.

Split

Your earnings go to a split contract that nobody owns. It pays 90.01% to your payout wallet and is created by your first payout. Its address depends on your name and payout wallet.

Node, Python
agents.status(): receiver (split), settleKey, creditWallet
WordPress
Settings → ZEAM Bridge: Your split, Settle key, Credit wallet (shown in gate mode)

What agents see

MCP
/agents/mcp · /wp-json/zeam-pass/mcp
HTTP
POST /agents/v1/<tool> · POST /wp-json/zeam-pass/v1/<tool>
OpenAPI
/agents/openapi.json · /wp-json/zeam-pass/openapi.json
Refunds
POST /agents/refund · POST /wp-json/zeam-pass/refund

On WordPress the same routes answer under /wp-json/zeam-bridge/ too.

An unpaid call answers 402 with your price: accepts[].amount in micro-USDC, "20000" is $0.02. Over MCP, the tool result carries the same terms. The agent pays in USDC on Base, over x402, with an MPP charge when your 402 offers one, or from the deposit of an MPP session when your Node seller has sessions on, and gets the answer.

HTTP/1.1 402 Payment Required
content-type: application/json
payment-required: eyJ4NDAyVmVyc2lvbiI6Mi…
…

{"x402Version":2,…,"accepts":[{"scheme":"batch-settlement","network":"eip155:8453","amount":"20000",…},…],…,"pricing":"$0.02 per call. A failed call is not charged.",…}
HTTP/1.1 402 Payment Required
content-type: application/json
payment-required: eyJ4NDAyVmVyc2lvbiI6Mi…
…

{"x402Version": 2, …, "accepts": [{"scheme": "batch-settlement", "network": "eip155:8453", "amount": "10000", …}, …], …, "pricing": "$0.01 per call. A failed call is not charged.", …}

Arguments are checked before any charge; an unpaid HTTP call with no arguments, as x402 directories send to read the price, gets the 402 with your terms. The 402 carries the x402 bazaar discovery extension: how to call the tool, its input schema and its output schema, so x402 directories can list it. The plugin verifies the payment, holds it while your tool runs, and settles it only if the tool succeeds, whether or not the agent is still connected to receive the answer.

Bad arguments, or a tool that fails, cost the agent nothing per call; on a line, a call uses the time it runs. What an agent deposits and does not spend, it takes back at the refunds route.

An agent needs no ETH to pay on the USDC row without Permit2, or for a refund the seller sends through the ZEAM relay. To pay and to get money back, the agent needs ETH on Base only where it sends a transaction itself, in five cases: the Permit2 row's one-time approval, selfSend (from any wallet), initiateWithdraw and finalizeWithdraw (from the channel's payer), and, with an MPP session, an open or top-up it sends itself for a credential of type hash (the USDC approval for the session escrow, then its open or topUp) and requestClose and withdraw on the session escrow without the relay (each from the channel's payer, Agents guide, section 7).

A gate

A gate’s 402 asks for amount: "0". The agent signs it with its own key, as a zero-value x402 payment: nothing is paid, the key needs no funds and no ETH, and nothing goes on chain. You list the keys you admit in admit.

A key you admit lets one other key in with a grant, sent as the x-grant header. Sign one with signGrant in Node, sign_grant in Python, or the Grants box on the WordPress settings page; The gate shows both calls. The grant works only for the key it names.

Hosting

Node

On a fetch host, serve agents.handle('/agents') instead of Express: it answers a Request with a Response, or nothing when the path is not its own.

import { bridge } from '@zeambridge/bridge'

const agents = bridge({
  name: 'acme', payout: '0xYourWalletAddress', price: '0.02', tick: false,
})
const handle = agents.handle('/agents')
export default {
  fetch: async (request) => (await handle(request)) ?? new Response('not found', { status: 404 }),
  scheduled: () => agents.tick(),
}

A timer in your process claims what agents paid, pays you out, stamps withdrawing channels and, for a gate, buys gate credit, every minute. On a serverless host nothing runs between requests: set tick: false and call agents.tick() from your scheduler.

Keep the state directory on a disk that survives a redeploy: it holds your settle key, your credit wallet’s key if you run a gate, and your channel records. If you seal the key with BRIDGE_KEY_SECRET, keep that secret too. A host with no disk that lasts passes bridge() its own stores (channel records and gate usage) and its own keys (each a 32-byte private key as hex; a paywall needs settle only, a gate credit only, both needs both). Every store function is async. update reads the current value, calls fn with it and stores what fn returns, atomically per id: for channels, null deletes; for the gate, fn returns [next, result] and update returns result. get returns null for an id or key the store does not hold, and list returns every channel record the store holds as an array, [] when it holds none. Line time is kept in the state directory only, so a host with no disk that lasts should not sell time. Free-call counts are kept in the state directory too, not in your stores: on such a host they start over whenever the instance does.

import { bridge } from '@zeambridge/bridge'

const agentsOn = (db, { settle, credit }) => bridge({
  name: 'acme', payout: '0xYourWalletAddress', price: '0.02', tick: false,
  stores: {
    channels: {
      get: async (channelId) => db.channel(channelId),
      update: async (channelId, fn) => db.updateChannel(channelId, fn),
      list: async () => db.channels(),
    },
    gate: {
      get: async (key) => db.gate(key),
      update: async (key, fn) => db.updateGate(key, fn),
    },
  },
  keys: { settle, credit },
})

Python

A background thread claims what agents paid, pays you out, stamps withdrawing channels and, for a gate, buys gate credit, every minute. Where no process stays alive between requests that thread does not run: set tick_seconds=0 and call agents.tick() from your scheduler.

Keep the state directory on a disk that survives a redeploy: it holds your settle key, your credit wallet’s key if you run a gate, and your channel records. If you seal the key with BRIDGE_KEY_SECRET, keep that secret too. Python keeps its keys and its channel records on disk only: it needs a disk that lasts.

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 and request_queue_size = socket.SOMAXCONN, as below: the standard library's default queue of 5 connections drops a burst).

wsgiref serves a WSGI app, such as the Flask app.py; FastAPI is ASGI, so it stays on uvicorn.

import socket
from socketserver import ThreadingMixIn
from wsgiref.simple_server import WSGIServer, make_server

from app import app


class Threaded(ThreadingMixIn, WSGIServer):
    daemon_threads = True
    request_queue_size = socket.SOMAXCONN


make_server("", 3000, app, server_class=Threaded).serve_forever()

Your server must be reachable from the internet at the address agents call; behind a proxy, set site to that address.

Your Bridge reads Base through a public endpoint that is rate-limited; with real traffic, set your own Base node or a provider in rpc.

WordPress

New versions appear in Dashboard → Updates and on the Plugins page like any other plugin, with the WordPress auto-update setting, off unless you turn it on. They come from zeampass.com, signed by ZEAM Labs: the plugin installs a new version only when its release file carries a valid signature and the download matches the signed SHA-256. A download that does not match is refused and nothing changes.

Every minute the plugin claims agent payments, pays out once ZEAM's fee covers the gas, stamps withdrawing channels and buys gate credit. WordPress runs scheduled work on visits: on a low-traffic site, have a cron job request https://your.site/wp-cron.php every minute and set define('DISABLE_WP_CRON', true); in wp-config.php. "Run now" on the settings page runs it at once.

Before you activate, add an encryption key of at least 32 random characters to wp-config.php: define('ZEAM_BRIDGE_KEY', '…');. The plugin makes its settle key on activation, and its credit wallet the first time it runs in gate or both mode, and seals each with it. Without it the plugin derives one from your WordPress salts and says so on its settings page. Keep the value across every move and redeploy; if you set none, keep the WordPress salts instead. Without the one the keys were sealed with, they cannot be opened. A site that already has ZEAM_PASS_KEY needs no change: it is read when ZEAM_BRIDGE_KEY is not set. With both set, the keys are opened with either and sealed again with ZEAM_BRIDGE_KEY on next use; keep it from then on.

Your Bridge reads Base through a public endpoint that is rate-limited; with real traffic, set your own Base node or a provider under Advanced, Base RPC.

Money

You keep 90.01% of every sale.

Fee
9.99% of each sale goes to ZEAM
Gate
10,000 checks a month free, then 2,000 checks for $1 USDC
Both
paid calls are not gate checks
Payouts
the first waits for more sales: it also creates your split, which costs more gas
License
Apache-2.0 for Node and Python, GPL-2.0-only for WordPress

For a gate, keep $1 USDC on Base in the credit wallet.

You, the seller, need no ETH. The one exception is a payout you choose to send from your own wallet before the fee covers its gas; Status has the steps.

Node
agents.payout() hands you the transaction
Python
agents.payout_now() hands you the transaction
WordPress
Pay out now on the settings page
  • Gas. An agent needs no ETH to pay on the USDC row without Permit2, and you, the seller, need none to be paid. ZEAM's relay sends what your Bridge signed, to where it signed it (claims, payouts and refunds for your channels), and agents' deposits and MPP charges, which the agent signed. It pays the gas of every deposit it sends; of a claim or payout when ZEAM's 9.99% fee on it covers that gas; and of an MPP charge on the same rule (a charge whose fee does not cover it is not sent: the call is not delivered and nothing is charged). A refund has its own rule, under Refunds.
  • Deposits. An agent's first call on a channel deposits at least the deposit floor: your price per call, or the gas floor where that is higher (about $0.036 at a gas price of 0.0084 gwei and ETH at $2,370; it tracks gas, and your 402 states the live figure); later calls spend it, and a later top-up meets the same floor.
  • Refunds. The unspent balance goes back to the agent through the relay. ZEAM pays the gas when the channel's fees cover the gas of its deposit, its refund and any calls not yet settled, and of every further deposit and every earlier refund ZEAM paid for on that channel, plus 15%; otherwise the agent pays the quoted gas in USDC (15% margin, priced by the relay, public at https://api.zeampass.com/relay/quote). Nothing is claimed for you from it. selfSend: the agent gets a signed refund of the full balance and sends it from any wallet with ETH on Base for gas. Without you: the agent sends initiateWithdraw, then finalizeWithdraw after the withdrawal delay, each from the channel's payer wallet with ETH on Base for gas, for at most the channel's balance minus totalClaimed in the escrow. Detail: Agents guide, section 5. When a buyer paid with a pass and you refuse its refund, the pass service hands that buyer's funding wallet the same escrow withdrawal to broadcast itself; the pass service does not send it or pay its gas.
  • MPP clients. For a tool priced per call, your 402 also carries an MPP Payment challenge (the WWW-Authenticate: Payment header) when ZEAM's fee on that one call (9.99% of it; all of it when your payout is the fee address) covers the gas of one USDC transfer plus 15%; an MPP client pays that call with one USDC transfer to your split. Nothing to set up. Below that price your 402 offers x402 only (and, on Node, a session where you have turned sessions on: MPP sessions, below), and so does every tool priced by use and every purchase of line time. A time tool called without a line is a tool priced per call here, at its callUSD.
  • Stock x402 clients. Stock x402 clients deposit 5x your price. They work when your price is at least 1/5 of the deposit floor in your 402 deposit line. Example: floor $0.036, price $0.0072 and up. Line time follows the same rule: on a new channel a stock client's first purchase must cost at least 1/5 of the floor, so it buys that many milliseconds or more; a pass and ZEAM :: Pass deposit enough for a small purchase. Use ZEAM :: Pass 4.0.0 or later: earlier versions, formerly @zeam-labs/x402-mcp-bridge, read X402_… settings.

Sessions

Node only, for now: the Python and WordPress plugins offer no session. Off until you turn it on: sessions: { escrow: '0x5e09aD70b644414cD1cD7Df1d9a10fF7899D92B8', factory: '0xE18fCE1266056becA4a3B0edfc15F8634EA6901B' } in your Bridge's options, in mode paywall or both. Those are the EvmPaymentChannel escrow on Base and its payee factory, both deployed on 2026-10-13 and immutable.

The session contracts are not audited. Nobody outside their authors has reviewed them. A buyer's deposit sits in the escrow, and what a session earns you passes through your payee contract, below.

  • What a buyer sees. Your 402 for every tool priced per call carries a second MPP Payment challenge, intent="session" (draft-evm-session-00), at the same price as every other way to pay, whether or not it carries a charge. A tool priced by use and a purchase of line time get none.
  • How a buyer pays. One deposit into the escrow, which the relay sends from the buyer's signed authorization (for a pass, the pass service sends it), then one signed voucher per call. A call that fails is not charged.
  • Your payee. A session pays your SessionPayee contract, at the factory's addressOf(your settle key's address, your split). It signs for your split, which cannot sign, and forwards all it receives to it; your split and its 90.01% and 9.99% are as they were. Your 402 publishes the split, its salt and the settle key's address beside the challenge so a buyer can check the payee. The relay creates the payee with your first session payout.
  • Settlement. Each background run claims session channels that have been idle 20 seconds, once ZEAM's fee on what it claims covers the gas. The payout that follows sweeps the payee to your split, distributes the split and withdraws to your wallet, by the rule under Payouts below break-even. You need no ETH.
  • A buyer leaves. On a buyer's close you are paid what it spent and the rest of its deposit returns to it in the same transaction; the relay sends that free when the channel's fees cover the gas of its open, of every top-up and close request the relay sent for it, and of its close, and otherwise takes the gas in USDC from what returns to the buyer, who needs no ETH. A buyer can also leave without you: it asks the escrow to close and withdraws after 15 minutes, and a background run in that time claims what it spent without waiting for the channel to be idle. A buyer you have taken off your access list can still close.

What a buyer signs and checks: Agents guide, section 7.

The gate

  • What an agent sends. A zero-value x402 exact payment signed with its key, on every call (any x402 client makes one). It moves nothing and is never sent to the chain. Your Bridge verifies the signature in your process, refuses a replay, and admits the key if its address is on your list. Agent side: Agents guide, section 3.

  • Grants. A listed key can admit one other key: it signs a grant naming that key, one tool (or *) and an end time; the agent sends the grant as the x-grant header. A call signed by any other key is refused bad_grant. Remove the signing key from your list and every grant it signed ends. On a line the list is checked when the channel pays, at its deposit and at each purchase of time, not on each call that rides the line: a key you remove can use up time it already bought and can buy no more.

    Node:

    import { signGrant } from '@zeambridge/bridge'
    
    const grant = await signGrant({ key: process.env.ADMITTED_KEY, delegate: '0xTheirKeyAddress', scope: 'search', until: '2026-12-31T00:00:00Z' })

    Python:

    import os
    from zeam_bridge import sign_grant
    
    grant = sign_grant(os.environ["ADMITTED_KEY"], "0xTheirKeyAddress", "2026-12-31T00:00:00Z", scope="search")

    key: the private key of a listed address. scope: a tool's name, or * (the default) for every tool. until: an ISO time, a Date or a timezone-aware datetime. Both return the x-grant value for the agent. WordPress: the Grants box on the settings page signs with your browser wallet when it holds a listed key.

    The grant is base64url JSON {delegate, scope, until, signature}; signature is the admitted key's EIP-191 signature of:

    ZEAM Pass grant
    delegate: <the key's address, lowercase>
    scope: <tool, lowercase, or *>
    until: <ISO time>

    A grant made by hand with no scope is signed with the line scope: self; it covers every tool, as * does.

  • Price.

    • 10,000 checks a month are free.
    • Your Bridge makes a credit wallet the first time it runs in gate or both mode; status() (WordPress: the settings page, in gate mode) shows its address. It holds the gate's USDC on Base and needs no ETH. Node: keys: { credit } sets your own.
    • 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.
    • Credit at 0: refuse (default), or with "onEmpty": "allow", admit and warn.
  • Both mode: paid calls are not gate checks. both mode needs no USDC in the credit wallet.

Status

status() (WordPress: the settings page) answers:

  • unclaimed: microUSD paid to you and not yet claimed from the escrow, over channels channels.
  • claimedNotPaid: microUSD claimed and not yet paid to your wallet. splitDeployed is false until your first payout creates your split.
  • lastPayout: when, how much, which transactions. lastTick: the last background run.
  • gate: this month's used checks, the free allowance, credit left, onEmpty, and unpaidAt when calls were admitted with no credit.
  • creditWallet: its address and usdcMicro balance.
  • chainError: the chain read for the figures above failed.

Payouts below break-even. The relay pays out when ZEAM's 9.99% of the payout covers its gas; you keep 90.01%. The gas is priced live: your Bridge asks the relay, which estimates the payout transaction itself at the current gas price, adds Base's L1 fee and 15%. Only when the relay cannot be asked does your Bridge fall back to its own fixed estimate. A payout (claim, settle, distribute, withdraw) goes out as one Multicall3 transaction in that order, all or nothing. Below break-even, unclaimed or claimedNotPaid stays above 0, lastTick.claims.USDC.waiting reads "below break-even: waits until the fee share covers the gas", and payout() (Python: payout_now()) answers paidBy: "you" with valueUSD, gasUSD and a transaction to send from your own wallet, the seller's, which then needs ETH on Base for gas. Nothing else a seller does needs ETH.

What ZEAM runs

  • The ZEAM :: Bridge relay: sends to Base the transactions your Bridge signed (claims, payouts and refunds for your channels) and agents' deposits and MPP charges. It pays the gas of every deposit it sends, and of a claim, payout, refund or MPP charge by the rules under Money.
  • The ZEAM :: Bridge credit service: sells gate checks.

Neither holds anything of yours.