EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8470: Token Launch Observations

Block-pinned facts an independent detector records about a token launch, keyed like ERC-8376 and launch declarations

Authors
Created2026-10-27
Discussion Linkhttps://ethereum-magicians.org/t/erc-8470-launch-observations/30112
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#2121PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationInterfaceTermsLaunch keysRecordingFindingsText flagsERC-8376 signalsTallyReportImmutability and supersedingDetector identityReadingArbitrum chainsDeploymentRelation to ERC-8376 and to declarationsUse before a tradeRationaleBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

Abstract

This proposal defines a registry in which any address, acting as a detector, records what it observed about a token launch at a given block. An observation is keyed to the launch's ERC-8376 launchId when an ERC-8376 directory lists the launch, to its venue-scoped declaration identifier under the Token Launch Declarations proposal (a separate draft) when a launchpad declared it outside ERC-8376, and otherwise to the chain id and token address. It carries a small set of typed findings, such as the share of supply bought in the block of the first trade (the first-block share), the share bought by early buyers, the creator's prior launches, the share held by wallets linked through funding, whether a product was found behind the token, and whether the token's own text carries content written to instruct AI agents, together with any of the base signals ERC-8376 defines, measured up to the observed block; a tally of how the detector reads its findings; the block the findings hold at; and the hash and URI of the detector's full report. Observations are never edited or removed. A detector records a newer observation to supersede its own earlier one, and both stay readable. A detector MAY link an ERC-8004 identity. Together with the facts a launchpad declares at launch, observations let anyone compare what was declared with what independent detectors saw, detector by detector. An agent, or a guard around it, reads a trusted detector's latest typed finding for a token in one call before it buys.

Motivation

A launch has two sides that a buyer, a wallet, a lender or an agent needs to compare: what the launchpad and the creator say, and what the chain shows. The declared side is being standardized separately: the Token Launch Declarations proposal (a separate draft) records the creator, allocations, liquidity locks and fee recipients a launchpad declares, and the Pool Liquidity Locks proposal (a separate draft) gives every liquidity locker one interface. Its stated purpose is that "detectors, wallets and lenders can compare what a launchpad declared with what they observe". Nothing standardizes the observed side.

Today every detector publishes through its own interface and its own vocabulary. Three problems follow:

  1. No common shape. A wallet that wants to show "34% of supply was bought in the first trade block" must integrate each detector separately, and two detectors measuring the same thing cannot be compared because nothing says they measured the same thing.
  2. No block. A figure such as "the creator holds 12%" is true at one block and false a few blocks later. Without the block it was read at, a reader cannot reproduce it, and a detector cannot be shown wrong.
  3. No history. Off-chain results are replaced in place. A reader cannot see what a detector said before the outcome was known, which is the only way to judge whether its findings mean anything.

Measured data shows why both sides are needed. On Robinhood Chain, the only disclosure a launch on the Pons launchpad carries is the venue's list of wallets exempt from its trading tax, through which a creator declares the wallets that buy at launch. Over the 14 days to 2026-10-29, with each outcome read 24 hours after launch and classified by the measuring detector, launches whose exempt wallets bought no supply (3,329) were rugged in 14.7% of cases and graduated in 1.4%; where they bought under 5% of supply (398), 37.7% were rugged; where they bought 30% or more (141), 2.8% were rugged and 13.5% graduated. The one flag covers opposite cases and cannot tell a farm of wallets from a team. Typed declarations, as the Token Launch Declarations proposal defines them, say what an allocation is; observations say what the declared wallets did. On the same chain, two launchers made about 7,400 launches in 16 days that removed all of their launch liquidity, while an indexer kept showing it: a figure copied from an indexer cannot be checked, which is why an observation is pinned to a block of the chain and its hash.

ERC-8376 defines a registry of bonded abuse reports, each scoring one abuse pattern from 0 to 100. That fits a party prepared to stake a bond on a claim of abuse. Most useful readings of a launch are not claims of abuse: the share bought by early buyers, how many launches the creator made before, whether a product exists behind the token. Many detectors can state such facts and stand behind the block they read them at, without asserting a pattern or posting a bond. This proposal is that layer: append-only facts without a bond or a score, at a block, keyed so that they line up with ERC-8376 launches and with declarations.

Readers it serves:

  • Agents and the guards around them read a trusted detector's latest typed findings for a token in one call, before or inside the transaction that buys it, and refuse a stale or missing reading.
  • Detectors read each other's observations to corroborate or dispute a figure at a named block.
  • Wallets show findings from the detectors their user chose, next to what was declared.
  • Lenders and contracts read the latest observation of a trusted detector inside the transaction that acts on it.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Interface

An observations registry MUST implement ILaunchObservations and ERC-165, returning true for the interface identifier of ILaunchObservations, 0x125ee299, and for 0x01ffc9a7.

interface ILaunchObservations {
    enum KeyKind { None, Erc8376, Token, Erc8462Venue }

    enum Lean { None, Clear, Careful, Against }

    struct Tally {
        uint16 against;
        uint16 careful;
        uint16 clear;
    }

    struct Finding {
        bytes32 kind;
        uint256 value;
        uint64 param;
        Lean lean;
    }

    struct ObservationInput {
        KeyKind kind;
        bytes32 launchId;
        uint256 chainId;
        address token;
        uint64 observedBlock;
        bytes32 observedBlockHash;
        Tally tally;
        bytes32 reportHash;
        string reportURI;
        bytes32 evidenceRoot;
    }

    struct Observation {
        bytes32 launchKey;
        uint256 chainId;
        address token;
        KeyKind kind;
        uint64 observedBlock;
        bool hashChecked;
        address detector;
        uint64 recordedAt;
        address identityRegistry;
        Tally tally;
        uint256 agentId;
        uint256 supersedes;
        bytes32 reportHash;
        bytes32 evidenceRoot;
        bytes32 declarationId;
        string reportURI;
    }

    event ObservationRecorded(uint256 indexed observationId, bytes32 indexed launchKey, address indexed detector, KeyKind kind, uint256 chainId, address token, bytes32 declarationId, uint64 observedBlock, bytes32 observedBlockHash, bool hashChecked, uint256 supersedes, Tally tally, bytes32 reportHash, string reportURI, bytes32 evidenceRoot);
    event FindingObserved(uint256 indexed observationId, bytes32 indexed kind, uint256 value, uint64 param, Lean lean);
    event DetectorIdentitySet(address indexed detector, address indexed identityRegistry, uint256 indexed agentId);

    function launchDirectory() external view returns (address);
    function tokenKey(uint256 chainId, address token) external pure returns (bytes32);
    function declarationKey(uint256 chainId, bytes32 declarationId) external pure returns (bytes32);
    function recordObservation(ObservationInput calldata input, Finding[] calldata findings) external returns (uint256 observationId, bytes32 launchKey);
    function setDetectorIdentity(address identityRegistry, uint256 agentId) external;
    function detectorIdentity(address detector) external view returns (address identityRegistry, uint256 agentId);
    function getObservation(uint256 observationId) external view returns (Observation memory observation, Finding[] memory findings);
    function latestOf(bytes32 launchKey, address detector) external view returns (uint256 observationId);
    function latestByToken(uint256 chainId, address token, address detector) external view returns (uint256 observationId);
    function latestFinding(uint256 chainId, address token, address detector, bytes32 kind, uint64 param) external view returns (uint256 observationId, uint64 observedBlock, uint64 recordedAt, uint256 value, Lean lean);
    function latestFindingFrom(uint256 chainId, address token, address[] calldata detectors, bytes32 kind, uint64 param) external view returns (address detector, uint256 observationId, uint64 observedBlock, uint64 recordedAt, uint256 value, Lean lean);
    function observationsOf(bytes32 launchKey, uint256 start, uint256 limit) external view returns (uint256[] memory);
    function observationsBy(bytes32 launchKey, address detector, uint256 start, uint256 limit) external view returns (uint256[] memory);
    function detectorsOf(bytes32 launchKey, uint256 start, uint256 limit) external view returns (address[] memory);
}

Terms

  • A detector is any address that records an observation. It is the caller of recordObservation.
  • The directory is the ERC-8376 ILaunchDirectory whose address launchDirectory() returns, or the zero address when the registry serves no directory. launchDirectory() MUST NOT change.
  • A launch key is the bytes32 under which observations of one launch are grouped.
  • An observation is one record by one detector about one launch key at one observed block.
  • A finding is one typed fact inside an observation.
  • The observed block is the block number, on the chain the launch is on, at which every finding of the observation holds. The observed block hash is the hash of that block as the detector read it.
  • A chain's block number is the number its own blocks carry, the number its nodes return for eth_blockNumber. Every block number in this proposal, observed or current, is in that domain. On most chains it is block.number. On an Arbitrum chain, such as Robinhood Chain, block.number inside the EVM returns a block number of the parent chain and blockhash does not return the chain's own block hashes; the chain's block number is ArbSys(address(100)).arbBlockNumber().
  • The creator is, for a launch the directory lists, the directory's deployerOf(launchId); otherwise the party the detector attributes the launch to, as stated in its report.

Launch keys

An observation has one of three kinds of launch key.

  • ERC-8376 keys (KeyKind.Erc8376). For a launch the directory lists, the launch key is its launchId. The registry MUST read the token from the directory's tokenOf(launchId) and MUST revert if it is the zero address, if the directory is the zero address, or if the input's chainId or token is not zero. The recorded chainId is block.chainid, since an ERC-8376 directory records only its own chain's launches.
  • Token keys (KeyKind.Token). For any other launch, the launch key is derived as below from the chain the token is on and its address. The registry MUST revert if chainId or token is zero, or if launchId is not zero. tokenKey(chainId, token) MUST return this value.
launchKey    = keccak256(abi.encode(TOKEN_SCOPED, chainId, token))
TOKEN_SCOPED = keccak256("erc.launch-observations.token-scoped")
  • Declaration venue keys (KeyKind.Erc8462Venue). For a launch a declarations contract of the Token Launch Declarations proposal records under a venue-scoped identifier (IdKind.Venue), the input's launchId carries that declarationId, derived as the Token Launch Declarations proposal specifies (keccak256(abi.encode(VENUE_SCOPED, block.chainid, venue, token, creator, nonce))), chainId is the chain of the declarations contract, which is the chain the token is on, and token is the declared token. The launch key is derived as below. The registry MUST revert if launchId, chainId or token is zero, MUST record launchId as the observation's declarationId, and declarationKey(chainId, declarationId) MUST return the launch key.
launchKey      = keccak256(abi.encode(VENUE_DECLARED, chainId, declarationId))
VENUE_DECLARED = keccak256("erc.launch-observations.venue-declared")

The domain tags keep the three kinds apart: a venue key never equals an ERC-8376 launchId or a token key, even when a detector passes a launchId or a token as the declaration identifier. The registry does not read the declarations contract: the Token Launch Declarations proposal has no canonical deployment, and the declarations contract a reader trusts may be on another chain. A venue key is therefore verified by the reader, who calls getDeclaration(declarationId) on the declarations contract it trusts and SHOULD treat the observation as unsupported unless that returns kind IdKind.Venue and the observation's token. A venue-scoped identifier only means something under the venue that wrote it, so a reader that trusts a set of launchpads SHOULD also check that the returned venue is one of them. Otherwise an observation could attach to a declaration that an untrusted venue opened for the same token. declarationId is zero for the other key kinds; for an ERC-8376 key the launch key is itself the identifier the Token Launch Declarations proposal declares under.

A detector SHOULD observe a launch the directory lists under its launchId, and a launch declared under a venue-scoped identifier under its venue key. A token key groups every launch of a token on a chain, including relaunches, and its observations describe the token at the observed block, whichever launch produced it. KeyKind.None is the kind of an observation that does not exist; recording with it MUST revert.

The chain in a token key is the chain the token is on, which need not be the chain of the registry: a registry on one chain MAY hold observations of launches on another.

Recording

recordObservation(input, findings) records one observation and returns its identifier and launch key.

  • It MUST NOT restrict its caller. Any address MAY act as a detector.
  • It MUST resolve the launch key as above.
  • It MUST revert if observedBlock is zero, or if the launch is on the registry's chain and observedBlock is not lower than the chain's current block number.
  • When the launch is on the registry's chain, it MUST revert if observedBlockHash is zero, and if observedBlockHash differs from the hash of block observedBlock where the registry can read that hash on chain: from the EIP-2935 history contract at 0x0000F90827F1C53a10cb7A02335B175320002935 when it has code and returns a hash for the block (how far back it answers depends on the chain, so the registry asks it for any block and treats a refusal as not read), and otherwise, on a chain where blockhash takes the chain's own block numbers, from blockhash(observedBlock) when the block is at most 256 blocks old. A hash the registry cannot read is not checked. It MUST record hashChecked as true only when it read the hash of block observedBlock on chain in this way and it matched, and as false otherwise, including for every launch on another chain. When the launch is on another chain, observedBlockHash SHOULD be the hash of the observed block on that chain and MAY be zero, meaning not given; the registry does not check it.
  • It MUST revert if reportHash is zero, if reportURI is empty, or if reportURI is longer than 512 bytes.
  • It MUST revert if findings holds more than 32 entries, if two findings share both kind and param, or if a finding is invalid as defined under Findings.
  • It MUST revert if tally.against, tally.careful or tally.clear is lower than the number of findings with lean Against, Careful or Clear respectively.
  • When the caller has recorded under the same launch key before, it MUST revert if observedBlock is lower than the observed block of the caller's latest observation of that key, and MUST set supersedes to that observation's identifier. Otherwise supersedes is zero.
  • It MUST assign an identifier one greater than the last one assigned, starting at 1, and MUST record the caller as detector and the block timestamp as recordedAt.
  • It MUST recheck the caller's linked identity as setDetectorIdentity checks it, and record it as identityRegistry and agentId only if the caller still holds it. Otherwise it MUST record both as zero, and MUST NOT revert for it.
  • It MUST emit ObservationRecorded, carrying the recorded declarationId and hashChecked, and observedBlockHash as given, then FindingObserved once per finding in the order given. observedBlockHash is emitted only; it is not part of the stored Observation, while hashChecked is.

Findings

A finding states that a quantity, identified by kind and qualified by param, had value at the observed block. lean is the detector's reading of that one finding for a prospective buyer: Against (it weighs against buying), Careful (it calls for care), Clear (the detector looked and found nothing adverse), or None (no reading). A lean is the detector's reading of one fact. It is not a determination about the launch.

Where a detector could not establish a value, it MUST report type(uint256).max (not read) and MUST NOT report zero. Zero asserts that the quantity was measured and was zero.

Kinds are bytes32 identifiers derived from namespaced strings, as ERC-8376 derives pattern identifiers. The erc. prefix is reserved for kinds defined in an ERC. This proposal defines the core kinds below; the registry MUST revert on a core finding whose param or value is outside the domain given. Shares are in basis points of the token's total supply, from 0 to 10000.

KindDerivationparamvalue
First-block sharekeccak256("erc.launch-observations.first-block-share")0Share of total supply acquired in the block of the token's first trade, by accounts other than the pool and the venue.
Early-buyer sharekeccak256("erc.launch-observations.early-buyer-share")N, seconds, not zeroShare of total supply acquired in the first N seconds after the first trade, by accounts other than the pool and the venue.
Creator prior launcheskeccak256("erc.launch-observations.creator-prior-launches")0Number of tokens launched by the creator before this launch, on the same chain.
Funding-linked sharekeccak256("erc.launch-observations.funding-linked-share")H, hops, not zeroShare of total supply held at the observed block by accounts that share a funding source, within H transfers of native currency, with the creator or with another holder in the set.
Product labelkeccak256("erc.launch-observations.product-label")0Whether the detector found a working product behind the token, checking each claim the project makes against the chain and its public interfaces: 1 found, 2 partly found, 3 not found, or not read.
Text flagskeccak256("erc.launch-observations.text-flags")F, a text field code from 0 to 9Bitmask of the reasons, defined under Text flags, that the detector found in field F of the token's own text; 0 when it read the field and found none, or not read.

For the creator prior launches, a detector that could establish only a lower bound, because its scan of the creator's history stopped before the observed block, MUST report not read, and MAY give the bound in its report. For the product label, the detector's report MUST list each claim it checked and what it found.

The first-block and early-buyer shares are fixed once their window has passed, so they hold at every later block. Every other finding MUST be read at the observed block itself. A detector that reads the parts of a check at different blocks MUST record as observedBlock the block at which it read the quantities that can still change, and MUST NOT record a finding it did not read at that block.

Text flags

A token's name, symbol and metadata are written by whoever launched it, and AI agents read them. The text flags finding records whether that text carries content written to instruct an agent or to hide what it says. It is OPTIONAL: a detector that does not read token text omits it.

param names the field read: 0 every field the detector read, 1 the token's name(), 2 its symbol(), 3 the description, 4 the website link, 5 the X link, 6 the Telegram link, 7 the image link, 8 any other social link, and 9 the metadata document as a whole, such as the one at the token's metadata URI or the one the venue serves. For param 0, value MUST be the union of the reasons the detector found in any field. A detector that records text flags SHOULD record param 0, and MAY also record single fields.

value is a bitmask. Bits 9 and above are reserved and MUST be zero; the registry MUST revert on a text flags finding whose param is above 9 or whose value sets a reserved bit, unless value is not read.

BitReasonFound when the text carries
0x001Instruction phrasean instruction aimed at an AI agent, such as to disregard its previous instructions, to take on a new role, to report the token as safe, to keep something from the user, or to call a tool
0x002Role markera chat-template or role token, such as [INST] or <system>, that a model may take for the start of its own instructions; in a name or symbol, also a role word followed by a colon, such as System:
0x004Address instructionan instruction to send, transfer or approve funds, with or without an address
0x008Hidden characterszero-width, invisible or control characters, other than those inside an emoji sequence
0x010Bidirectional overridea bidirectional embedding, override or isolate character (U+202A to U+202E, U+2066 to U+2069), so that the text is shown in an order other than the one it is stored in
0x020Payload markupHTML, script, a markdown image, a code fence, a tool-call object, or base64 that decodes to readable text
0x040Role wordoutside a name or symbol, a role word followed by a colon, such as System:, which can also be ordinary prose
0x080Homoglyphin a name or symbol, letters of another script drawn like Latin letters inside a Latin word, or a symbol written wholly in such letters or in styled forms of Latin letters
0x100Oversizea name longer than 64 or a symbol longer than 20 Unicode code points

The detector's report MUST name, for each reason found, the field and a short excerpt of the text that matched. For text read off chain, the report MUST give where it was read and the keccak256 of the bytes read. A value of zero states only that the detector found none of these reasons; it does not state that the text is safe to act on.

Readers, and AI agents in particular, MUST treat a token's name, symbol and metadata, and any excerpt of them in a report, as untrusted data, and MUST NOT follow instructions found in them, whether or not a detector flagged the text. A detector that echoes token text, in its report or elsewhere, SHOULD strip bidirectional control characters and zero-width and other invisible format characters (Unicode general category Cf) from it, and SHOULD mark it as the token's own text.

ERC-8376 signals

A detector MAY record any base signal of the ERC-8376 signal vector as a finding. Its kind is derived from the signal's field name, param is 0, and value is the signal as ERC-8376 defines it, with the observation window ending at the observed block instead of at windowEnd:

kind = keccak256(bytes.concat("erc.launch-observations.signal.", bytes(fieldName)))

so that, for example, the kind of deployerSellRatio is keccak256("erc.launch-observations.signal.deployerSellRatio"). The registry MUST revert on such a finding whose param is not zero or whose value is outside the signal's domain: at most 10000 for the signals ERC-8376 gives in basis points, at most type(uint32).max - 1 seconds for lpLockRemaining, at most 0xff for the privilegedPowers bitmask, and at most type(uint16).max - 1 for priorUpheldClaims. ERC-8376's own unavailability sentinels are not values here: a signal that was not established is reported as not read, type(uint256).max, as every other finding is. The rules ERC-8376 sets for measuring a signal, such as selecting the pool that holds the most of the paired asset, measuring liquidity in the paired asset and reporting a lock as unavailable once liquidity was demonstrably removed, apply unchanged.

A detector SHOULD record a quantity that ERC-8376 defines under its signal kind rather than under a kind of its own, so that readers of both proposals read one definition. Liquidity in particular is read through the lpLockedShare and lpLockRemaining signals, not through a finding of this proposal. Where it counts liquidity that one account can remove, a detector that records these signals SHOULD compute them from lockers of the Pool Liquidity Locks proposal, reading each locker's lockedLiquidity(pool, poolId) at the observed block as the Pool Liquidity Locks proposal's detector inputs specify, with the observed block in place of the block that ends the window, and SHOULD count a locker's liquidity only if the locker holds it at the observed block, by the Pool Liquidity Locks proposal's holding check (the position manager's ownerOf(positionId) is the locker and the position's liquidity is at least amount; for pool tokens, the locker's balanceOf is at least permanent plus timed), not because the locker is on a list of lockers of the detector's own.

A detector MAY report kinds outside the core set, derived from a namespaced string under a prefix it controls, such as a domain it owns, and MUST NOT use the erc. prefix for them. The registry stores them without checking param or value, except that kind MUST NOT be zero. A reader MUST treat a kind it does not recognize as informational.

Tally

tally counts the detector's readings over every finding in its full report, typed or not: how many weigh against, call for care, or are clear. It is how the detector summarizes the observation, and like a lean it is a reading of facts, not a determination about the launch. It MUST be at least the counts of the typed findings with each lean, as required above, and MAY be higher where the report holds findings that have no typed kind.

Report

reportURI locates the detector's full report, and reportHash MUST be the keccak256 of the exact bytes served at it. A detector SHOULD serve the report from content-addressed storage. The report SHOULD be a JSON object with at least:

  • launchKey, kind, chainId, token, declarationId, observedBlock, observedBlockHash and hashChecked, as recorded;
  • findings: every finding, typed or not, each with its kind or name, param, value, lean and a one-line description of how it was read;
  • tally: the three counts;
  • creator: the address the detector attributes the launch to, and how;
  • method: the detector's method version.

The report MAY also carry detail, the URL of a fuller report, which MAY require payment. reportHash does not cover it, so the report it hashes stays readable without payment.

evidenceRoot is OPTIONAL and zero when absent. Where present, it MUST be a root over evidence leaves built as ERC-8376 specifies for its evidenceRoot, so that a third party can check a finding against the transactions it was read from.

Immutability and superseding

  • An observation, and every finding recorded with it, MUST NOT be changed or removed. The registry MUST NOT expose any function that changes or removes one.
  • A detector that wants to correct or update an observation records a new one. The new observation names the earlier one in supersedes, and becomes the detector's latest for that launch key. The earlier one stays readable through getObservation and observationsBy.
  • A detector's observed blocks for one launch key never decrease. A correction of a reading at the same block records the same observedBlock again.
  • An observation supersedes only the same detector's earlier observation of the same launch key. No detector can supersede another's.

Detector identity

A detector MAY link an ERC-8004 identity, or another ERC-721 identity, with setDetectorIdentity(identityRegistry, agentId).

  • When identityRegistry is the zero address, the call MUST revert unless agentId is zero, and it removes the link.
  • Otherwise it MUST revert unless identityRegistry has code and the caller is its ownerOf(agentId), or its getAgentWallet(agentId) where the registry provides that function.
  • It MUST emit DetectorIdentitySet. detectorIdentity(detector) MUST return the current link.

The link held when an observation is recorded is copied into it, if the detector still holds the identity then, so a later change does not alter what an earlier observation records. A reader that trusts a detector by its agentId SHOULD recheck, when it reads, that the identity is still owned by the detector or names it as agent wallet. Through the linked identity, readers can find the detector's ERC-8004 registration file and its reputation, and can give feedback on the detector's observations in the ERC-8004 reputation registry.

A detector that links an ERC-8004 identity SHOULD list each registry it records to in its registration file, as a service named launch-observations whose endpoint is the registry's CAIP-10 address (eip155:<chainId>:<address>). A reader that acted on a detector's observation MAY give feedback to the detector's agentId in the ERC-8004 reputation registry with tag1 launch-observations and tag2 the finding kind it relied on, and a feedbackURI naming the registry and the observationId.

Reading

  • getObservation(observationId) MUST return the observation and its findings in the order recorded. For an identifier with no observation it MUST return an observation whose kind is KeyKind.None and whose other fields are zero, and no findings. It MUST NOT revert.
  • latestOf(launchKey, detector) MUST return the identifier of the detector's latest observation of the key, or zero.
  • latestByToken(chainId, token, detector) MUST return the identifier of the detector's observation, under any key kind, of token on chain chainId with the highest observed block (on a tie, the one recorded last), or zero. For an ERC-8376 key, the chain is the registry's chain and the token is the one read from the directory; for a declaration venue key, they are the ones the observation carries.
  • latestFinding(chainId, token, detector, kind, param) MUST return, for the observation latestByToken returns, its identifier, observed block and recordedAt, and the value and lean of its finding with that kind and param. When there is no such observation, the identifier, observed block and recordedAt MUST be zero. When there is no such observation or no such finding, value MUST be not read (type(uint256).max) and lean MUST be None, so that a missing reading never reads as a measured zero. It MUST NOT revert.
  • latestFindingFrom(chainId, token, detectors, kind, param) MUST try the detectors in the order given and return the first one whose latestFinding for kind and param has a value other than not read, with that detector and the results latestFinding gives for it. When no detector in the list has one, it MUST return the zero address, zero identifier, observed block and recordedAt, not read and None. It MUST NOT revert. Its gas grows linearly with the length of the list, so a reader SHOULD pass only the few detectors it trusts, in order of preference.
  • observationsOf(launchKey, start, limit) MUST return the identifiers of every observation of the key, by any detector, in the order recorded. observationsBy(launchKey, detector, start, limit) MUST do the same for one detector's observations of the key. detectorsOf(launchKey, start, limit) MUST return each detector that recorded under the key once, in the order of its first observation.
  • Each list MUST start at position start, return at most limit entries, and return an empty list when start is at or past its end.

A reader acting on an observation SHOULD refuse one whose observed block is more than a bound of its own choosing below the current block number of the launch's chain. The first-block and early-buyer shares do not change after their window; every other kind can. For an observation of another chain, the reader SHOULD bound recordedAt instead. The registry sets no bound: how fresh a reading must be depends on what the reader is about to do.

Arbitrum chains

On an Arbitrum chain, block.number and blockhash inside the EVM refer to the parent chain, while observed blocks are the chain's own. A registry deployed there MUST take the current block number from ArbSys(address(100)).arbBlockNumber() and MUST NOT compare an observed block with block.number or check its hash with blockhash; it checks the hash through the EIP-2935 history contract where the chain deploys it. A reader or guard on an Arbitrum chain MUST take its current block number from the same source when it bounds the age of an observation, since block.number there is smaller than every observed block and the difference underflows or compares numbers of two chains. A registry or reader that runs on several chains MAY use ArbSys when code exists at address(100) and the call succeeds, and block.number otherwise, as the reference implementation does.

Deployment

A registry MUST be permissionless, immutable and without an owner. It MUST NOT refuse an observation that meets the requirements above, since a registry able to refuse observations could suppress the ones its operator dislikes. Any address MAY deploy a registry. A reader MUST obtain the registry's address from a source independent of the launch it is examining.

A registry SHOULD be deployed through the EIP-7997 factory where that factory is present on the chain, with its salt and init-code hash published, so that a reader can derive its address and check its code without trusting the deployer.

Relation to ERC-8376 and to declarations

This section is informative.

  • An observation is not an ERC-8376 report. It carries no score, names no abuse pattern and posts no bond, and an ERC-8376 consumer MUST NOT treat it as a report. A detector that is also an ERC-8376 detector MAY derive inputs for its observe call from its own observations, and MAY cite an observation's report in its evidenceURI.
  • First-block and early-buyer shares are measured without regard to funding, so they differ from ERC-8376's sniperConcentration, which counts only buyers linked to the deployer. The funding-linked share is the measurement ERC-8376 needs for insiderAllocationShare and deployerSupplyShare, without its window. A detector that measures those signals as ERC-8376 defines them records them under their signal kinds.
  • Against declarations: a declared allocation is compared with the funding-linked share at a block; a declared liquidity lock is compared with the lpLockedShare and lpLockRemaining signals at a block after the lock was declared; a declared creator is compared with the creator the detector attributes the launch to. Both sides join directly by key: for a launch the directory lists, both use the same launchId; for a launch declared under a venue-scoped identifier, the observation carries that declarationId and its launch key is declarationKey(chainId, declarationId), so a reader goes from a declaration to its observations, and back, without a lookup by token. An observation under a token key, of a launch declared under a venue-scoped identifier but observed before the detector knew of it, is still joined through the token: a reader reads the declarations of the observation's token with declarationIdsOf.

Use before a trade

This section is informative.

A contract that buys on behalf of a user or an agent, such as a guard, a wallet module or an agent router, can check a trusted detector's latest reading of the token inside the same transaction, before the swap. The thresholds and the age bound below are the integrator's, not this proposal's.

// Inside a guard, wallet module or agent router, before the swap.
// The chain's own block number: ArbSys on an Arbitrum chain, block.number elsewhere.
uint256 current = ON_ARBITRUM ? IArbSys(address(100)).arbBlockNumber() : block.number;
(uint256 id, uint64 at,, uint256 linked,) = obs.latestFinding(block.chainid, token, DETECTOR, FUNDING_LINKED_SHARE, 2);
require(id != 0 && at <= current && current - at <= MAX_AGE, "no fresh observation");
require(linked != type(uint256).max && linked <= 1500, "funding-linked share");
(,,, uint256 first,) = obs.latestFinding(block.chainid, token, DETECTOR, FIRST_BLOCK_SHARE, 0);
require(first != type(uint256).max && first <= 2000, "first-block share");

A missing observation and a missing finding both come back as not read, so the guard refuses them rather than passing them as clean. A guard that trusts several detectors can read them in order of preference with latestFindingFrom and check which detector answered. An off-chain agent makes the same reads with eth_call and follows reportURI for the detail.

Rationale

  • Facts, not verdicts. A finding is a quantity at a block, which a third party can recompute. A verdict is not, and two detectors' verdicts cannot be compared. The lean and the tally carry the detector's reading without turning it into a claim about the launch, and keep the reading separate from the fact so that a reader can ignore one and keep the other.
  • Append-only. An observation is worth reading because it cannot be withdrawn once the outcome is known. A detector able to edit could rewrite a reading that turned out wrong, and nobody could measure its record. Corrections are new observations that point at the old one.
  • Block-pinned. Launch state changes block by block. Pinning every finding to one block makes it reproducible, makes a wrong finding demonstrably wrong, and lets a reader see whether an observation came before or after an event. The observed block is stated by the detector because the registry may sit on another chain; on its own chain the registry rejects a block that does not exist yet.
  • The block hash is emitted, not stored. A block number alone does not say which block a detector read if that block was reorganized out. The hash names it at the cost of one word of calldata and log data. It is checked on chain where the registry can read the block's hash, through EIP-2935 or blockhash, and otherwise left to readers, who can compare it with the canonical chain at no cost to the registry; storing it would add a storage write to every record for a value contracts can read back only for a bounded window. What is stored is one flag, hashChecked, packed beside observedBlock: whether the registry itself verified the hash. An on-chain guard cannot compare a hash with the canonical chain later, but it can refuse an observation whose hash nobody checked.
  • The chain's own block numbers. Observed blocks are what a detector's node reports, so the registry compares them with the same number. On an Arbitrum chain block.number is the parent chain's and would reject every same-chain observation, so the registry reads ArbSys there.
  • Identity rechecked at record time. A link set once would let a detector keep showing an agentId it sold or lost. Checking it at each record costs one or two static calls and keeps the copied identity true when it is made; recording zero rather than reverting keeps a detector's observations flowing while its link is fixed.
  • An ordered list of detectors. A guard that trusts more than one detector wants the first that has read the finding, without one call per detector. latestFindingFrom returns which detector answered, so the guard keeps control of whom it trusts; it does not merge or count readings.
  • Observed blocks do not go backwards. Without this, a detector could record an observation of an earlier block after a later one and make "latest" mean something other than "most recent state".
  • A typed core and an off-chain report. Contracts and wallets need a few comparable numbers they can read without parsing. Everything else a detector checks, and how, belongs in a report that changes shape with its methods. The hash binds the report to the record, and the optional evidence root reuses ERC-8376's leaf format instead of inventing one.
  • Six core kinds, and ERC-8376's signals by name. ERC-8376 already defines twelve signals with their units and measuring rules. Defining them again would give readers two definitions of one quantity, so an observation records them under kinds derived from their ERC-8376 field names. The six core kinds are the readings ERC-8376 does not define: what was bought in the first trade block and the first seconds by anyone, the creator's earlier launches, the share held by wallets that share a funding source, whether a product stands behind the claims a project makes, and whether the token's own text is written to instruct the agents that read it. New kinds are namespaced, as ERC-8376 patterns are, so the set grows without a central registry.
  • Text flags as reasons, not a score. Whether text is an attack depends on who reads it, so the finding records which patterns were found and in which field, as bits a contract can test, and leaves the reading to the lean. One kind with a field code, rather than one kind per reason, keeps a token's text within a few of the 32 findings allowed, and param 0 answers whether anything was flagged in one read.
  • A registry of its own, not a general attestation service. A general attestation registry, such as the Ethereum Attestation Service or the ERC-8004 validation registry, stores a payload under a schema and a subject. It does not check that a share is at most 10000, that "not read" is never written as zero, that a detector's observed blocks do not go backwards, or that an ERC-8376 key names a listed launch, and it cannot answer "this detector's latest observation of this launch" in one read. Those rules are what make two detectors' readings comparable and a contract's read of one safe, so they belong in the registry.
  • ERC-8004 reputation, not validation. The ERC-8004 validation registry is not used for observations. Its subject is an agent's work, not a token; a request must come from the validated agent's owner or operator, which would add a transaction before every trade; and its response is a score from 0 to 100. It is also not deployed everywhere the identity and reputation registries are: on Robinhood Chain only those two are deployed. The links this proposal uses, a service entry in the registration file and reputation feedback tagged with the kind relied on, exist wherever ERC-8004 identity and reputation do. Where an agent wants a validation record, the detector MAY answer its request with validationResponse(requestHash, response, responseURI, responseHash, tag), with responseURI the observation's reportURI, responseHash its reportHash, tag launch-observations:<observationId>, and response the percentage of core kinds it read, which measures coverage, not safety.
  • One call by token. An agent holds a token address, not a launch key. latestByToken and latestFinding find a trusted detector's newest reading of the token under any key kind without a directory lookup and without copying the report URI and every finding, so a guard can afford the read inside a trade. They return not read for anything missing, which makes absence fail closed.
  • The reader sets the age. A registry-wide limit on how old an observation may be would be a policy, and the right bound differs between a guard on a trade and a lender's daily check. The reader states its own bound.
  • Keyed like declarations. Using the ERC-8376 launchId, and reading the token from the directory as the Token Launch Declarations proposal reads the creator, means declared and observed facts about a listed launch meet under one identifier. Accepting the Token Launch Declarations proposal's venue-scoped identifiers as well, which only the declaring venue can write under, means every declared launch can be observed under its own key, as the authors of the Token Launch Declarations proposal pointed out in review. The venue key is derived under a domain tag of its own rather than used as is, so that an identifier the registry cannot verify on chain can never land in the lists of an ERC-8376 launch or a token key. The token key covers launches nobody declared.
  • Locks read, not listed. If each detector counts the lockers on a list of its own, two detectors report different lpLockedShare for the same pool and neither can show the other wrong. Reading lockedLiquidity(pool, poolId) from lockers of the Pool Liquidity Locks proposal, and counting a locker only where it holds the position, gives every detector the same figure from the chain. This composition was suggested in review by the authors of the Token Launch Declarations and Pool Liquidity Locks proposals.
  • No bond. A bond answers for a claim of abuse. Requiring one for facts would keep most detectors out, and the facts are checkable at their block without one. Readers decide which detectors to trust, by identity and by record.
  • Optional identity, copied at record time. An ERC-8004 identity lets a reader find a detector's reputation and registration file. Copying the link into each observation keeps the record of who stood behind it when it was made.
  • "Not read" is not zero. As in ERC-8376, a missing measurement reported as zero would read as a clean measurement.
  • Bounds and pages. At most 32 findings and a 512-byte URI keep a record's cost bounded. Every list can be made long by anyone, so every list is paged, and latestOf, latestByToken and latestFinding answer in one read for a reader that knows which detector it trusts.

Backwards Compatibility

No backward compatibility issues found. This proposal changes nothing in ERC-8376. A registry can serve no directory, in which case it records token keys only.

Test Cases

A conforming registry, next to an ERC-8376 directory that lists one launch, passes these cases:

  • A detector records an observation under a token key with five core findings and an ERC-8376 signal. getObservation returns every field and finding as given, latestOf returns it, and detectorsOf lists the detector once.
  • Under an ERC-8376 key, the registry records the token the directory names and the registry's chain id. An unlisted launchId, a non-zero input chainId or token, or a registry without a directory reverts.
  • A token key with a zero chain id, a zero token or a non-zero launchId reverts, as does KeyKind.None.
  • Under a declaration venue key, the launch key is keccak256(abi.encode(VENUE_DECLARED, chainId, declarationId)), the observation records the declaration identifier, token and chain given, and ObservationRecorded carries the identifier; the other key kinds record a zero declarationId. A zero identifier, chain id or token reverts; a registry without a directory accepts the key. An ERC-8376 launchId passed as a declaration identifier lands under its own venue key, not in the lists of the launchId.
  • No function changes or removes an observation; a second detector's observation leaves the first unchanged.
  • A detector's second observation of a key supersedes its first: supersedes names the first, latestOf returns the second, the first and its findings stay readable, and observationsBy lists both, oldest first. Another detector's observations are unaffected.
  • An observation at the same observed block as the detector's latest is accepted; one at an earlier block reverts.
  • ObservationRecorded is emitted, then one FindingObserved per finding, in order.
  • 32 findings are accepted and 33 revert. A 512-byte URI is accepted; 513 bytes, an empty URI and a zero report hash revert.
  • A share above 10000, an early-buyer share with N zero, a product label of 0 or 4, and a zero kind revert. "Not read" is accepted for a share and for the product label. Two findings with the same kind and param revert; the same kind with another param is accepted.
  • An ERC-8376 signal in basis points is accepted at 10000 and reverts at 10001 or with a non-zero param; lpLockRemaining reverts at type(uint32).max; privilegedPowers accepts 0 and reverts at 0x100; "not read" is accepted for each.
  • Text flags are accepted for param 0 to 9 with any value up to 0x1ff, with zero, and with not read, and latestFinding returns them by field; a reserved bit (0x200), param 10, and a value just below not read revert.
  • A tally lower than the leans of the typed findings reverts.
  • On the registry's own chain, an observed block of zero or of the current block reverts; on another chain a block number above the current one is accepted.
  • On the registry's own chain, without an EIP-2935 history contract, a wrong observed block hash reverts for a block up to 256 blocks old, including exactly 256, and is accepted unchecked for a block 257 blocks old; a zero hash reverts, as it does under an ERC-8376 key. On another chain a zero hash and an arbitrary hash are accepted. ObservationRecorded carries the hash given. With a history contract, a wrong hash for a block 1000 old reverts.
  • With ArbSys at address(100) returning a block number far above block.number, a same-chain observation at an earlier ArbSys block is accepted, and one at or above the ArbSys block reverts. With an EIP-2935 history contract a wrong hash reverts, at 100 blocks old and at 300,000 (no window is assumed); a block the history has no hash for is accepted unchecked, near the head or deeper than the history reaches, and blockhash is not used.
  • hashChecked is stored and emitted true for an own-chain hash read through blockhash or the history contract, and false for a block the history has no hash for and past blockhash's reach, and for a launch on another chain.
  • After the detector's identity is transferred away, its next observation records zero identityRegistry and agentId; once the identity names the detector as agent wallet again, the link is recorded again.
  • latestFindingFrom returns not read and the zero address when no listed detector has observed the token; skips a detector without an observation and one whose finding is not read; returns the first listed detector with a value even when a later one observed a later block; and returns not read for a kind nobody holds, an empty list, and another chain.
  • With five observations of a key, observationsOf(key, 1, 2) returns the second and third, a start at 5 returns an empty list, and a limit of type(uint256).max returns the rest.
  • The owner of an identity and its agent wallet can link it; another address, a registry without code, and a zero registry with a non-zero id cannot; a registry without getAgentWallet is read through ownerOf. An observation keeps the identity it was recorded under after the detector removes its link.
  • latestByToken finds a detector's observations of a token under every key kind: a higher observed block under another key becomes the latest, a lower one does not, a tie goes to the one recorded last, and another detector or another chain returns zero. latestFinding and latestFindingFrom read an observation under a declaration venue key.
  • latestFinding returns the identifier, observed block, recordedAt, value and lean of the finding asked for. With no observation it returns zeros, not read and None; for a kind or param the observation does not hold it returns the identifier and block with not read and None; a measured zero stays zero.
  • supportsInterface returns true for 0x125ee299 and 0x01ffc9a7, and false for 0xffffffff.

Reference Implementation

A reference implementation, LaunchObservations, and its tests are in this proposal's assets folder. It stores each observation and its findings, keeps per-key, per-detector and per-key detector lists, and validates the core kinds and ERC-8376 signals. The tests run under Foundry with mocks of an ERC-8376 directory and of ERC-8004 and plain ERC-721 identity registries, and need no external library.

Security Considerations

  • Anyone can be a detector. Recording is permissionless, so any address can record observations of any launch, with any figures. Readers MUST choose the detectors they trust and SHOULD read those detectors' observations with latestOf, latestByToken or latestFinding, and MUST NOT count observations or detectors as evidence. A thousand addresses agreeing may be one party.
  • Spam. Lists by key can be made arbitrarily long. A reader SHOULD read pages of a size it can afford and SHOULD NOT assume a first page is the whole list. A trusted detector's latest observation stays readable in one call however long the lists grow.
  • A detector can be wrong. An observation proves only that the detector recorded it. Its figures are facts at a block, as the detector read them, not a judgment that a launch is sound or fraudulent, and a reader MUST NOT present them as one. Readers SHOULD prefer detectors that publish evidence roots and reproducible reports, and that have a record of observations recorded before outcomes were known.
  • Absence. A launch with no observation, or a finding reported as not read, says nothing about the launch. It SHOULD be presented as unknown, never as clear, and a contract acting on it SHOULD refuse it.
  • Staleness. Most findings can change after their observed block. A reader that acts on an old observation acts on old facts; it SHOULD bound the age as described under Reading.
  • Front-running. An observation is visible in the mempool before it is mined. A copier can record the same figures first under its own address; this gains it nothing with readers who trust by detector identity, and recordedAt shows who recorded first. A creator watching the mempool can act on an adverse observation before it lands, so the observed block, not the time of recording, says when the facts held. Detectors SHOULD NOT rely on an observation staying private before it is mined.
  • Reorganizations and finality. Finality is the main rule: a detector SHOULD observe only finalized blocks. On an Arbitrum Orbit chain, such as Robinhood Chain, a block is final once the batch that holds it is final on the parent chain, which is what the node's finalized block tag returns. The registry cannot enforce this, since it can read a block's hash only for a bounded window: the last 256 blocks through blockhash, which on an Arbitrum chain does not return the chain's own hashes at all, or the EIP-2935 history contract on chains that deploy it, whose window is the chain's: the last 8191 blocks on Ethereum, and on an Arbitrum chain a window ArbOS sets in the chain's own blocks. So each observation emits the hash of its observed block: a reader or indexer SHOULD compare it with the canonical block at that number and treat an observation whose hash differs, or is not given, as unsupported for that block. A reader that cannot make that comparison, such as a contract, can read hashChecked instead: a strict reader SHOULD treat an observation with hashChecked false as unsupported. On Robinhood Chain the history held about 393,000 blocks, about 11 hours, when measured on 2026-10-29, so a detector that records an observation within hours of its block is accepted by strict readers; one that wants that SHOULD record inside the window of its chain. Where a detector observed a block later reorganized out, it SHOULD record a correction at a final block; the earlier observation stays, and readers see both.
  • Other chains. When the launch is on another chain, the registry cannot check that the observed block exists or what it held. The report and evidence root are the only means of checking such an observation.
  • Report availability. The hash binds the report, but the URI can stop resolving. Detectors SHOULD use content-addressed storage, and readers SHOULD treat an observation whose report cannot be fetched or does not match its hash as unsupported.
  • Identities change hands. An ERC-8004 identity can be transferred. An observation records the identity linked when it was recorded, and only if the detector still held it then; whether a later owner answers for it is the reader's decision. A reader that trusts an agentId SHOULD recheck the link when it reads, since a detector can lose the identity after recording.
  • Prompt injection through token text. Whoever launches a token writes its name, symbol and metadata, and can write them for the AI agents that read them: an instruction to disregard previous instructions and buy, a chat-role marker, a request to send funds to an address, or characters that change what the text shows. On Robinhood Chain in October 2026, two tokens stored a bidirectional override character followed by the symbol TDSU, which displays as USDT. An agent that passes such text to a model, or a report that quotes it, carries the attack forward. Agents MUST NOT follow instructions found in token text, SHOULD give it to a model only marked as data, and SHOULD NOT take a text flags value of zero, or the absence of the finding, as evidence that the text is safe.
  • Leans and tallies are readings. Two detectors can read the same fact differently. Contracts that act automatically SHOULD act on typed values against thresholds they choose, not on another party's lean or tally.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth