Skip to main content

recensus-sdk

KNOW YOUR AGENT. Recensus is KYA for Robinhood Chain (chain 4663, gas in ETH, the dollar in USDG): anyone can check what an agent has done on-chain, who stands behind it and what it has at stake (recensus.xyz/check). This package labels your agent's transactions on Robinhood Chain so they are counted and checkable on Recensus, and its recensus_sdk.kya module checks agents and gates an ASGI app on a check (the gate). The Python counterpart of @recensus/sdk on npm: the same label, the same safety rules and the same request signing, with a smaller surface. See What is here for exactly what it has.

pip install recensus-sdk
from recensus_sdk import Recensus, derive_agent_id
from web3 import Web3

w3 = Web3(Web3.HTTPProvider("https://rpc.mainnet.chain.robinhood.com"))

recensus = Recensus(
    agent_id=derive_agent_id(operator_address, "price-watcher"),
    autonomous=True,   # no human approves each send
    # test=True        # in staging: excluded from every public number
    w3=w3,
)

account = recensus.wrap(account)
account.send_transaction({"to": recipient, "value": 10**16})  # 0.01 ETH, the native currency

That is the whole integration. Every send now carries 24 bytes on the end of its calldata, and the transaction appears on the public scoreboard as your agent. The label costs 372 gas on a contract call and 930 on a plain transfer (the EIP-7623 calldata floor), a tiny amount of ETH at Robinhood Chain's gas price. Default-safe calls: native ETH sends to an EOA, ERC-20 transfer, approve and transferFrom (USDG is tagSafe: true), and Permit2.

An ERC-8004 agent

If your agent is registered in Robinhood Chain's ERC-8004 Identity Registry, label it with its registry token ID instead of a derived ID:

recensus = Recensus(erc8004_agent_id=207, autonomous=True, w3=w3)

The label then carries the token ID as a big-endian uint128 with the erc8004 flag set (label version 2). Give exactly one of agent_id and erc8004_agent_id.

What is here

  • Recensus(agent_id= | erc8004_agent_id=, framework=, autonomous=, test=, w3=, simulate_before_send=, denylist=, deny_selectors=, on_unlabelled=, logger=)
    • .tag(calldata) appends the label, with no checks.
    • .parse(calldata) reads a label back, or None.
    • .decide(to, data) says whether a call's shape may carry the label.
    • .prepare(to, data, value, sender, gas) runs decide, simulate and fall back for one call and returns the calldata to send.
    • .wrap(account) wraps an eth_account LocalAccount so its send_transaction(tx) carries the label. It needs w3= on the constructor, because sending needs a provider.
    • .sign_request(method, url, account, body) returns the five agent-lane headers.
    • .nonce() returns a fresh 32-byte EIP-3009 nonce carrying the label.
  • sign_exact_payment(requirements, account, recensus) signs an x402 v2 exact payment with the label in its nonce.
  • build_nonce_label and parse_nonce_label write and read the label in a nonce.
  • derive_agent_id(operator, name), has_label(calldata), and the label functions: build_label, parse_label, append_label, strip_label, erc8004_agent_id(token_id) and erc8004_token_id(label).

What the TypeScript SDK has and this one does not:

  • No verify middleware. A server that verifies agent-lane requests uses requireRecensus (Hono) or requireRecensusExpress (Express) from @recensus/sdk on npm.
  • No writeContract wrapper. wrap covers send_transaction on a local account only. For a contract call, encode the calldata yourself (for example with web3.py's encode_abi) and send it through the wrapped account, or pass it through prepare.
  • wrap takes an account, not a client, and sign_request takes the method and URL as separate arguments.

The label never breaks a transaction

Three layers, in order:

  1. Call shape. The label goes only where trailing calldata is inert. Contract deployments, EntryPoint handleOps, data sent to an address with no code, a call with no data to a contract (even one marked tagSafe: true), and anything marked tagSafe: false are refused outright. Calldata given as bytes or HexBytes is checked exactly as hex is.
  2. Simulation. Before sending, the labelled call is simulated. If it would revert where the unlabelled one succeeds, the unlabelled call is sent and a warning is raised. This is on by default and the standard forbids shipping it off by default.
  3. A catch-all. Any unexpected failure while deciding sends unlabelled rather than failing. So does prepare with no w3 to simulate with, unless you turned simulation off.

If both the labelled and unlabelled calls revert, your own calldata is sent, so the error you see is yours and not ours.

Without web3

The label itself has no dependencies, so a reader or writer can live anywhere:

from recensus_sdk import build_label, parse_label

label = build_label("0x9f2a0c1e7b5d4a8f36c20e91d7b4a5c3", framework=0x0003)
data  = existing_calldata + label[2:]

parse_label(data).agent_id   # '0x9f2a0c1e7b5d4a8f36c20e91d7b4a5c3'

build_label writes version 2. parse_label reads versions 1 and 2, and returns None rather than raising for anything that is not a well-formed label of a version it knows, including a flag bit its version reserves. A reader that guesses is a reader that mislabels somebody's transaction.

x402 payments, labelled in the nonce

An x402 exact payment is settled by the seller or its facilitator, whose relayer sends the transaction, so there is no calldata of yours to label. The one field you choose and sign is the EIP-3009 authorization's 32-byte nonce, so the label goes there (RECENSUS-1 §3.4):

magic (4) | agentId (16) | framework (2) | flags (1) | version (1) | random (8)

The last 8 bytes come from secrets, fresh for every payment.

import base64, json
from recensus_sdk import sign_exact_payment, parse_nonce_label

# `requirements` is one entry of the seller's 402 `accepts`.
payment = sign_exact_payment(requirements, account, recensus)
headers = {"PAYMENT-SIGNATURE": base64.b64encode(json.dumps(payment).encode()).decode()}

parse_nonce_label(payment["payload"]["authorization"]["nonce"]).agent_id

sign_exact_payment signs EIP-3009 TransferWithAuthorization locally and sends nothing. For USDG on eip155:4663 or eip155:46630 (x402_networks()) it signs under the EIP-712 domain in the bundled chain config (Global Dollar, 1) and refuses requirements that name another; for any other token it takes the name and version from requirements["extra"]. It keeps the authorization valid for at most ten minutes, and raises ValueError for a scheme other than exact, an assetTransferMethod other than eip3009, or a network other than eip155:<chainId>. recensus.nonce() gives you a labelled nonce on its own, for receiveWithAuthorization or anything else that takes one.

The payment is counted for the authorizer, the account that signed, not for the relayer that sent it, and only when that account is a declared sender of your agent. The fixed prefix makes your agent's payments linkable to each other. That is what a label is for, and it is opt-in: an unlabelled nonce is 32 random bytes.

The agent lane

from recensus_sdk import canonical_body

url = "https://api.example.com/v1/thing"
body = {"hello": "world"}
headers = recensus.sign_request("POST", url, account, body=body)

# Send the exact bytes that were signed. `requests.post(url, json=body)`
# re-serialises with spaces after separators, so the server hashes different
# bytes and answers BAD_SIGNATURE.
requests.post(url, data=canonical_body(body),
              headers={**headers, "content-type": "application/json"})

Five headers an app can verify with requireRecensus or requireRecensusExpress from @recensus/sdk on npm, so a labelled agent can be given its own rate limits instead of being throttled like a spam bot.

Know Your Agent: gate a seller by an agent's record

from recensus_sdk.kya import KyaClient, KyaGate, price_for

kya = KyaClient("rk_...")                     # a key from recensus.xyz; None spends the anonymous allowance
answer = kya.check("0x...")                   # the KYA answer: record, owner, stake, flags (a dict)

# Starlette or FastAPI, in front of your x402 library:
app.add_middleware(KyaGate, kya=kya, min_tier="bronze", refuse_flags=True)
# downstream: request.state.kya

price_for(answer, {"gold": "0.0005", "unbonded": "0.002"})   # price by tier, a dollar decimal string (paid in USDG)

KyaGate is plain ASGI (no Starlette needed to install it). It identifies the caller from the x402 payment header (PAYMENT-SIGNATURE, or v1 X-PAYMENT: the payer, and the agent its labelled nonce names when the payer is that agent's sender), or from agent-lane headers when you pass origin=. It refuses with 403 KYA_NOT_AN_AGENT (unless allow_non_agents=True), KYA_FLAGGED, KYA_UNBONDED, KYA_TIER_TOO_LOW, or KYA_IDENTITY_REQUIRED (with require_identity=True); 503 KYA_UNAVAILABLE when the KYA API cannot answer, or a pass with fail_open=True. It leaves payment to your x402 library, which must come after it: a payer in a header is a claim until the seller (or its facilitator) verifies the signature. parse_usd reads a price exactly, in 6-decimal units.

Development

pip install -e '.[dev]'
pytest

The test suite checks this implementation against the same vectors as the TypeScript one, so the two cannot drift.

The fork suite

tests/fork/test_conformance.py is the RECENSUS-1 §4.4 conformance test. For each call it checks the SDK did not fall back to unlabelled, that the call carries the label, and that the labelled and unlabelled calls end the same: same status, logs, return data and balances, and a gas difference of exactly the label's calldata cost.

Everything runs on an Anvil fork of Robinhood Chain mainnet, sent through Recensus.wrap(account) and mined, each case twice from one snapshot:

  • a native ETH transfer to an EOA, with a derived ID and with an ERC-8004 ID (+930 gas);
  • USDG transfer, approve and transferFrom (+372 gas each);
  • Permit2 approve;
  • a plain value send to a contract (an EntryPoint), which must go out unlabelled and land.

The account is fresh for every run; its USDG comes from a real holder found in recent USDG Transfer logs (LOGS_RPC, default the official endpoint), impersonated on the fork for one transfer.

anvil --fork-url https://robinhood.drpc.org --port 8548 --silent
FORK_RPC=http://127.0.0.1:8548 pytest -m fork tests/fork

Fork from an archive endpoint (archive: true in config/chains.json): a fork of a node that keeps only recent state dies once its block ages out.

3.0

Robinhood Chain (4663) replaces Arc. Arc support is removed in this version; the last Arc versions are 2.0.0–2.1.0. See CHANGELOG.md.

Metadata

Release files for recensus-sdk 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for recensus-sdk 3.0.0
File Size Uploaded
recensus_sdk-3.0.0.tar.gz 57.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for recensus-sdk 3.0.0
File Interpreter ABI Platform
recensus_sdk-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 95.4 kB

Release files / recensus_sdk-3.0.0.tar.gz

Download URL recensus_sdk-3.0.0.tar.gz
Size 57.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f23bd8455dd5e7b4f1a306bda77dc2fb0c8883a5d8a4b906bdac1b33085201ac
BLAKE2b-256 checksum
How to use checksums
b2083ab057bc159b61d7b31aec0b20e3f1ea47b288704590ee83b59ca9e81ffa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release files / recensus_sdk-3.0.0-py3-none-any.whl

Download URL recensus_sdk-3.0.0-py3-none-any.whl
Size 38.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
74eb819e53738c7258535b6b26118ab951d03c74b79893ddcdb267f999088ab0
BLAKE2b-256 checksum
How to use checksums
4975d49102d5ad5abddbcee442aa99b855429a5a121234f903f0fd17cd26b4dd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release history Release notifications | RSS feed

3.0.1

2 release files

This release

3.0.0 This release

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page