Metadata-Version: 2.5
Name: recensus-sdk
Version: 3.0.0
Summary: Know Your Agent on Robinhood Chain: label your agent's transactions so they are counted and checkable on Recensus, and check agents before you deal with them.
Project-URL: Homepage, https://recensus.xyz
Project-URL: Documentation, https://recensus.xyz/docs/get-counted
Project-URL: Specification, https://recensus.xyz/spec
License: MIT
Keywords: ai-agents,erc-8004,evm,know-your-agent,kya,recensus,robinhood-chain,usdg,web3,x402
Requires-Python: >=3.11
Requires-Dist: eth-account>=0.11
Requires-Dist: eth-hash[pycryptodome]>=0.7
Requires-Dist: eth-utils>=4.0
Requires-Dist: web3<8,>=6.20
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# 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](https://recensus.xyz/check)). This package labels your
agent's transactions on Robinhood Chain so they are counted and checkable on
[Recensus](https://recensus.xyz), and its `recensus_sdk.kya` module checks
agents and gates an ASGI app on a check ([the gate](https://recensus.xyz/docs/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](#what-is-here) for exactly what it has.

```bash
pip install recensus-sdk
```

```python
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:

```python
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:

```python
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.

```python
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

```python
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

```python
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

```bash
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.

```bash
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.
