Skip to content

RECENSUS-1 — an agent label for EVM calldata

Status: Final · Label version 2 · spec 2.2.1 · Any EVM chain; Recensus indexes Robinhood Chain (4663) · Last changed 2026-10-29 (§4.2, §8)

RECENSUS-1 is a 24-byte marker appended to the end of a transaction's calldata. It says: this transaction was sent by an automated agent, this is which agent, and this is the framework that built it.

It needs no contract, no registry call and no permission. It costs the calldata gas of 24 bytes. It works with every wallet, router and dapp on the chain, because standard ABI decoding ignores trailing bytes.


1. Format

24 bytes, appended to the end of the calldata:

 0                16        18      19       20            24
 +----------------+---------+-------+--------+-------------+
 |    agentId     |framework| flags |version |    magic    |
 |    16 bytes    | 2 bytes |1 byte | 1 byte |   4 bytes   |
 +----------------+---------+-------+--------+-------------+
FieldSizeValue
agentId16Identifies the agent. Any 16 bytes are valid.
framework2Big-endian code from the framework registry. 0x0000 = unknown/custom.
flags1Bit 0 autonomous, bit 1 test, bit 2 erc8004; bits 3–7 reserved and must be zero.
version10x02 for this document. 0x01 is still read (§1.3).
magic40x50554C53 — ASCII PULS. Always the last four bytes of the calldata.

A reader checks calldata[-4:] == 0x50554C53 and parses backwards. A label is well-formed only when the calldata is at least 24 bytes long, the version is a version it knows, and the reserved flag bits are zero. Anything else is not a RECENSUS-1 label and is ignored.

Why the magic still spells PULS. This standard was published as PULSE-1, was briefly NUMERUS-1, and is now RECENSUS-1. The magic is an opaque four-byte tag, not an abbreviation the parser reads, and changing it would buy nothing while breaking every reader and writer already built against it — including labels already in calldata, which cannot be rewritten. All three names describe the same 24 bytes on the wire: an implementation of any one of them reads and writes the others. The name changed; the format did not.

1.1 agentId

Recommended derivation, so that two operators never collide by accident and an operator can recreate an ID from a name:

agentId = keccak256(abi.encodePacked(operatorAddress, agentName))[0:16]

operatorAddress is the 20-byte address that will claim the ID on Recensus. agentName is UTF-8, not normalised, compared byte for byte.

Any other 16 bytes are valid. The derivation is a convention, not a rule: the chain cannot check it, so nothing on Recensus depends on it.

ERC-8004 agents. An agent registered in the chain's ERC-8004 Identity Registry may be labelled by its registry token ID instead. With the erc8004 flag set, agentId is the token ID as a big-endian uint128:

agentId = uint128be(tokenId)        // token 207 -> 0x000000000000000000000000000000cf

A token ID of 2^128 or more cannot be labelled. The flag is what tells the two kinds apart: the same 16 bytes without it are an ordinary ID, and a reader binds an ERC-8004 label to the registry only when the flag is set (§6).

1.2 flags

BitNameMeaning
0autonomousNo human approved this specific transaction.
1testTest traffic. Excluded from every public number. Visible to the operator only.
2erc8004agentId is an ERC-8004 Identity Registry token ID (§1.1). Version 0x02 only.
3–7reservedMust be zero. A label with a non-zero reserved bit is not well-formed.

test is load-bearing: it is how an integrator runs against mainnet without polluting the public scoreboard. Set it in every non-production environment.

1.3 version

0x02. Version 0x02 differs from 0x01 only in defining flag bit 2: in a 0x01 label bits 2–7 are reserved, in a 0x02 label bits 3–7. Writers write 0x02; readers accept both, so a 0x01 label is read as a 0x02 label with the erc8004 bit clear. A reader that does not know a version ignores the label rather than guessing, so a reader built for 0x01 alone skips 0x02 labels until it is updated. New versions keep the magic in the last four bytes and the version byte immediately before it, so any reader can always find the version.


2. Framework codes

Two bytes, big-endian, from config/frameworks.json:

CodeFramework
0x0000Unknown / custom
0x0001Recensus MCP server
0x0002Recensus SDK (TypeScript)
0x0003Recensus SDK (Python)
0x0011create-recensus-agent

Codes 0x0012–0xFFFF are assigned by pull request against config/frameworks.json. A pull request gives the framework a name, a public URL and a contact. Codes are never reassigned. A retired code leaves the registry file and is recorded in the history below, which is what keeps it from being handed out again. Using 0x0000 is always allowed and never wrong.

2.1 Registry history

Every code that has left the registry, and when. A code listed here is never assigned again, and a label carrying it still parses — the framework field is two bytes like any other — but it is shown as unregistered.

CodeAssignedRetiredRecord
0x00102026-09-192026-09-21Held for a first external adopter. No labelled transaction was indexed under it.

3. Where the label goes

3.1 Direct transactions from an EOA

Append to tx.data. A plain native transfer carries the 24 bytes as its entire data:

to:    0xRecipient
value: 0.01 ETH     (Robinhood Chain's native currency: 10^16 wei)
data:  0x<agentId><framework><flags><version>50554c53

The recipient must be an EOA or a contract whose receive() tolerates data — see §4.

3.2 ERC-4337

Append to the inner callData of the UserOperation, never to the handleOps calldata the bundler builds. Indexers decode EntryPoint.handleOps and check each op's callData tail.

Robinhood Chain has EntryPoint v0.6, v0.7 and v0.8 deployed.

3.3 Batches and multicall

Append once, to the outermost call. One transaction is one agent action; labelling the inner calls would double-count it.

3.4 The label in an EIP-3009 nonce

A payment the agent authorizes but does not send — an x402 exact payment settled by a facilitator or by the seller itself, a receiveWithAuthorization — carries no calldata of the agent's. The one field the agent chooses and signs is the authorization's 32-byte nonce, so the label goes there, with the magic first:

 0        4                20        22      23       24                 32
 +--------+----------------+---------+-------+--------+------------------+
 | magic  |    agentId     |framework| flags |version |   random (8)     |
 +--------+----------------+---------+-------+--------+------------------+

The fields mean what they mean in §1. The last 8 bytes must come from a secure random source: EIP-3009 requires only that a payer never reuses a nonce, and 64 random bits make a repeat negligible (a reused nonce is refused before anything moves, and the client signs a new one).

A reader takes the nonce from the token's AuthorizationUsed(authorizer, nonce) event, where it is exactly the value the payer signed, and parses it:

1. if len(nonce) != 32: no label
2. if nonce[0:4] != 0x50554C53: no label
3. version = nonce[23]; if version not in {0x01, 0x02}: ignore
4. flags = nonce[22]; if flags & reserved(version): malformed, ignore
5. framework = uint16be(nonce[20:22]); agentId = nonce[4:20]

The agent is the authorizer, not the transaction's sender, which is the facilitator's or the seller's relayer. Attribution is as in §6: the authorizer must be a declared sender of the agent. A random nonce begins with the magic by accident once in 2³², and must also name a known version with no reserved bit set.

The fixed prefix makes one agent's authorized payments linkable to each other. That is the purpose of a label, and it is opt-in.

Proven on Robinhood Chain testnet (46630) on 2026-10-09: a public third-party facilitator verified and settled a 0.01 USDG exact payment with the nonce 0x50554c53 fd84a5ff057029d0e3be10ed0764abcf 0002 03 02 3b4e1d2ddc720c0c, and the token logged AuthorizationUsed with exactly that nonce (transaction 0xca39b584…d638).


4. Safety — the label must never break a transaction

This is the rule that matters most. An agent that loses a transaction to a label will remove the label, and rightly.

4.1 Default-safe call types

Trailing calldata is inert for a call when the target ABI-decodes its arguments by offset and ignores the remainder. That is true for:

  • Native transfers to an EOA.
  • ERC-20 transfer, approve, transferFrom.
  • Permit2 calls.
  • Any address marked "tagSafe": true in config/labels.json.

The inner callData of an ERC-4337 UserOperation is not on this list. It is where a label goes in a UserOperation (§3.2), but no SDK labels an inner call and no conformance suite sends one, so it is placement guidance for a smart account that builds its own UserOperations, not a default-safe call type.

4.2 Never append when

  1. The target has no code and the caller is sending data on purpose. The label would be indistinguishable from the intended payload.
  2. The target is "tagSafe": false in config/labels.json — including the EntryPoints, where the label belongs on the inner call.
  3. The calldata is already labelled. Never stack two labels.
  4. The function packs arguments manually rather than ABI-decoding them — anything that reads msg.data.length, uses calldatacopy over the tail, or verifies a signature over the whole calldata. Permit2's signature covers a struct, not the calldata, so Permit2 is safe; a contract that signs over msg.data is not.
  5. **A simulation of the labelled call does not do exactly what the unlabelled call does**: it reverts where the unlabelled one succeeds, succeeds where the unlabelled one reverts, or returns different data. This is the backstop for everything the list above misses.
  6. The call carries no data and the target has code — a plain value send to a contract, "tagSafe": true or not. Empty calldata runs a contract's receive(); 24 bytes of label run its fallback(), and a proxy dispatches the label's first four bytes as a function selector. Where the two do different things and both return nothing, rule 5 cannot see it. "tagSafe": true vouches for calls that carry arguments, not for this: a wrapped-ether proxy that takes an empty send in receive() reverts the labelled one. A plain transfer to an EOA is unaffected, and so is a call that carries its own selector. Implementations refuse with EMPTY_CALL_TO_CONTRACT. Whether the target has code is read for each such call, never from a cache of "no code": an address gains code when a counterfactual (CREATE2) contract is deployed there or an EOA takes an EIP-7702 delegation. When that read fails, the answer is not known, and the call is sent unlabelled with CODE_UNKNOWN.
  7. The caller has appended bytes of its own after the encoded call — an ERC-2771 relayer appending the user's address, for one. The target reads that tail; a label after it would become the tail, the last 20 bytes taken as the sender, while the call may still succeed with the same return data, which rule 5 cannot see. Implementations send such a call as given, unlabelled, with CALLER_SUFFIX, whenever they can know of the suffix: one taken separately (viem's dataSuffix, set on the call or on the client); calldata longer than the call's own encoding where every argument has a fixed size (ERC-20 transfer, approve and transferFrom, Permit2's approve and transferFrom, EIP-3009's transferWithAuthorization, receiveWithAuthorization and cancelAuthorization); and a call the caller marks as carrying a tail of its own. A tail built into the data of any other call, and not marked, cannot be told from the call's own arguments: that is the known limit below, and a caller that appends a tail passes it separately or marks the call.

Everything not excluded by 4.2 may be labelled: outside the list, the rule is allow, checked by simulation. What a simulation cannot see is accepted as a known limit: a contract that reads its calldata tail without reverting and without returning anything that depends on it (hashing or storing msg.data, an ERC-2771-style tail reader), and a tail of the caller's own built into the data of a call whose size the implementation cannot know, unmarked (rule 7). An operator who calls such a contract adds it to the SDK's denylist; a public contract of that kind is recorded as "tagSafe": false. An EIP-7702-delegated account has code, so it is treated as a contract, not as an EOA.

4.3 The fallback is always to send unlabelled

An implementation simulates the labelled and the unlabelled call before sending (default on). Unless both succeed with the same return data, or both revert, it sends unlabelled and warns. "Both revert" means both calls came back as an execution revert. A failure that is not a revert (a rate limit, a timeout, an unreachable node, a node error) means nothing was checked, and it sends unlabelled too. Losing a row on a scoreboard is a small cost; losing a transaction is not. No implementation may make this behaviour non-default.

4.4 The conformance test

Every implementation ships a conformance test that proves, for each default-safe call type, that the labelled call and the unlabelled call produce the same state: same balances, same events, same status. It runs on a fork where the fork can execute the call, and otherwise on real chain state by debug_traceCall, labelled and unlabelled at the same block. On Robinhood Chain an Anvil fork executes every default-safe call type (native ETH, USDG transfer, approve and transferFrom, Permit2), so each is sent there. A release that has not run it is not a release.


5. Reading labels

1. data = tx.input
2. if len(data) < 24: no label
3. if data[-4:] != 0x50554C53: no label
4. version = data[-5]; if version not in {0x01, 0x02}: unknown version, ignore
5. flags = data[-6]; reserved = 0b11111100 if version == 0x01 else 0b11111000
   if flags & reserved: malformed, ignore
6. framework = uint16be(data[-8:-6])
7. agentId = data[-24:-8]
8. if flags & 0b100: the agent is ERC-8004 token uint128be(agentId)

For a transaction to an EntryPoint, run the same steps over each userOp.callData in the decoded handleOps call instead.

5.1 The 24-byte collision question

Ordinary calldata ends with 0x50554C53 by accident roughly once in 4.3 billion trailing words. In practice the last word of an ABI-encoded call is a padded argument, so the accidental rate observed has been zero: a 300-block sample of 3,512 transactions on Robinhood Chain, where this standard was first used, contained no accidental magic. Readers should nonetheless treat the label as a claim, never as proof — see §6.


6. What a label proves, and what it does not

A label is self-declared. Anyone can write any agentId into their own calldata. RECENSUS-1 does not try to prevent that; it makes it useless:

  • A transaction counts toward a claimed agent only if its **sender is on that agent's declared sender list**. Copying someone's ID produces an unclaimed row, never a row under their name.
  • An ERC-8004 label (erc8004 flag) needs no claim: its sender counts if, at the transaction's time, it was the token's owner or its agentWallet in the Identity Registry, or a sender the owner declared. Ownership moves the binding from the transfer on, never backwards.
  • Unclaimed labels still count in chain totals, and the totals always show the claimed/unclaimed split.
  • Staking behind an ID (planned) makes a verified label cost money to fake, and lose money when caught.

Read the label as "this sender says it is an agent". That claim is cheap, and it is still far more information than the chain gives you otherwise.


7. Worked example

Operator 0x1111111111111111111111111111111111111111, agent named price-watcher, built on the Recensus TypeScript SDK, running autonomously in production:

agentId   = keccak256(0x1111...1111 ‖ "price-watcher")[0:16]
          = 0x9f2a0c1e7b5d4a8f36c20e91d7b4a5c3   (illustrative)
framework = 0x0002
flags     = 0x01            (autonomous, not test)
version   = 0x02
magic     = 0x50554c53

label     = 0x9f2a0c1e7b5d4a8f36c20e91d7b4a5c3 0002 01 02 50554c53

Sending 5 USDG (6 decimals) on Robinhood Chain becomes:

data = 0xa9059cbb
         000000000000000000000000cafe...beef      <- recipient
         00000000000000000000000000000000004c4b40 <- 5_000000
         9f2a0c1e7b5d4a8f36c20e91d7b4a5c3 0002 01 02 50554c53

The ERC-20 decodes its two arguments and never looks past them.


8. Changelog

  • v1 (2026-09-19) — first published version, as PULSE-1.
  • 2026-09-20 — renamed to NUMERUS-1, then to RECENSUS-1, with the project. No byte changed in either rename: same 24 fields in the same order, same version 0x01, same magic 0x50554C53. A PULSE-1 or NUMERUS-1 implementation is a conformant RECENSUS-1 implementation and needs no edit. The one package published under the interim name, numerus-sdk 1.0.0 on PyPI, has been removed in favour of recensus-sdk; it labelled correctly, it was simply the wrong name.
  • 2026-09-21 — framework code 0x0010 retired and moved from the registry to the registry history (§2.1). The label format is unchanged; a label that carries 0x0010 still parses and is shown as unregistered.
  • 2026-09-21 — §4.2 rule 5 and §4.3: the simulation compares return data as well as reverts, and an unreachable node means unlabelled. Default-allow outside §4.2 is stated, with the cases it accepts. The label format is unchanged.
  • 2026-09-21 — §4.2 rule 6: no label on an empty-data call to a contract, since the label switches receive() to fallback(). §4.1: ERC-4337 inner callData moved out of the default-safe list; it stays placement guidance (§3.2), because no SDK labels an inner call. The label format is unchanged.
  • v2 (2026-09-25) — version 0x02 defines flag bit 2, erc8004: the agentId field then carries an ERC-8004 Identity Registry token ID as a big-endian uint128 (§1.1, §1.2, §1.3, §6). Same 24 bytes, same order, same magic; 0x01 labels are still read. The default-safe list loses the Uniswap and WETH entries (§4.1), and the conformance test may run on real state by debug_traceCall where a fork cannot execute a call (§4.4).
  • 2026-09-25 — §3.4: the label in an EIP-3009 nonce, for payments the agent authorizes and a facilitator sends. The calldata label is unchanged.
  • 2026-10-09 (spec 2.2.0) — Recensus moved to Robinhood Chain (4663, gas in ETH). Examples and wording only: the native-transfer example sends ETH (§3.1), the worked example sends USDG (§7), §3.4 records the nonce label proven on Robinhood Chain testnet, and §4.4 says the fork runs every default-safe call type there. No byte of the label or the nonce label changed.
  • 2026-10-10 — §2: framework code 0x0011 assigned to create-recensus-agent, the starter kit; the next free code is 0x0012.
  • 2026-10-29 — §4.2 rule 7: no label after a suffix the caller appended itself (CALLER_SUFFIX), since a tail reader would take the label as that suffix. §4.2 rule 6: whether the target has code is read for every empty-data call, never from a cache of "no code". §8: the interim PyPI package was removed, not yanked. The label format is unchanged.
  • 2026-10-29 — §4.2 rule 7: a caller's tail is also known when the suffix is set on the client, when a call whose arguments all have a fixed size (ERC-20, Permit2 allowance transfers, EIP-3009) is longer than its encoding, and when the caller marks the call; an unmarked tail in a call of unknown size is the stated limit. §4.2 rule 6: an empty-data call whose target's code cannot be read is not labelled (CODE_UNKNOWN). The label format is unchanged.

Changes that alter the byte layout get a new version byte and a new section here. The magic and the position of the version byte never move.

Byte layouts

The label on the end of calldata, for a transaction the agent sends itself, and the same fields in an EIP-3009 nonce, for an x402 payment a facilitator sends for it.

byte 024 bytes total · end of calldata
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • agentId16 bytes · 0–15
  • framework2 bytes · 16–17
  • flags1 byte · 18
  • version1 byte · 19
  • magic4 bytes · 20–23

magic 0x50554C53 · always the last four bytes · read by tail, no ABI

byte 032 bytes · the nonce the payer signs
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • magic4 bytes · 0–3
  • agentId16 bytes · 4–19
  • framework2 bytes · 20–21
  • flags1 byte · 22
  • version1 byte · 23
  • random8 bytes · 24–31

signed by the payer · sent by the relayer · counted under the payer