Methodology

Karatum Reputation Feeds

Testnet pilotSource: docs/methodology/reputation-feeds.md

Status: testnet pilot (Base Sepolia, Ethereum Sepolia, OP Sepolia, Arbitrum Sepolia). Interface version 1 (frozen), feed build 1. Migration 037_feeds.sql, API /v1/feeds, Explorer /feeds. Code: contracts/src/feeds/ (IKaratumReputation.sol, KaratumReputationLib.sol, KaratumReputationFeed.sol, examples/), tests contracts/test/feeds/ (49 Foundry tests, 1,000-run fuzz), packages/feeds (standing, deviation policy, updater cycle, EIP-712 reports), services/feeds (CLI cycle | status | standings | deploy), services/api/src/routes/feeds*.ts, ops scripts/ops/feeds/ (systemd unit + timer), scripts/feeds-deploy.sh. Deployments: contracts/deployments/feeds.json.

1. What it is

A price feed, but for agent trust. A smart contract on any chain with a feed reads an agent's Karatum standing in one call:

FieldMeaning
classesbitmap of task classes with a VALID, unexpired Karatum certificate (bit 0 web3, bit 1 software; new classes take the next bit)
classTiers2 bits per class: certificate tier 0..3 (K >= 22: 3, >= 18: 2 "production grade", >= 12: 1, else 0)
tier, karatshighest tier / K among the certified classes (0 when classes is 0)
kcs, kcsBandKaratum Credit Score 300..850 (0 = unscored) and its band: 1 poor (<500), 2 limited, 3 fair (580), 4 good (660), 5 very good (740), 6 exceptional (800)
shieldTier0 none, 1 S0, 2 S1, 3 S2, 4 S3 (adversarial resistance)
honestyBand0 unranked, 1 low (AHI < 60), 2 moderate, 3 high (80), 4 exceptional (90). The AHI is pessimistic and needs 100 scored claims to rank
recalledan active Incident Registry recall
updatedAt, roundIdwhen Karatum last confirmed the record, and the global round that last wrote it

The tuple is IKaratumReputation.Standing (one 32-byte storage slot). A certified-but-low-grade agent (classes != 0, tier 0) is distinct from an uncertified one (classes == 0).

2. How data gets in

Sources. Standing is derived, never entered by hand: certificates of the build bound to the wallet (the Guard binding rule: operator binding, else the newest build registered with the wallet), the latest persisted KCS snapshot of that build (credit_scores), the latest Shield certificate, the latest Honesty Index snapshot, and the Incident Registry recall sets (recalledSubjects). A recalled wallet stays in the feed, flagged; it is never dropped.

Push. The updater key sends pushBatch(updates, fullSync) with only the agents whose standing deviates from what the chain holds: any recall change; any certificate, Shield or honesty change; any KCS band change; a KCS move of >= 10 points (FEEDS_KCS_DEVIATION). Every push is a full sync: the updater just recomputed everyone, so the batch also attests that every other agent the feed already knows is unchanged as of now, and their updatedAt reads as that time (one storage write however many agents there are). With nothing to write, a pure heartbeat full sync is sent once half the heartbeat has passed (24 h heartbeat, so at least every 12 h). The timer runs every 15 minutes, so a recall reaches every chain within one cycle; on Base Sepolia and Ethereum Sepolia it is visible in the same block the KaratumIncidentLog records it, because the feed ORs the log's live isRecalled into every read.

Pull. submitReport(report, signature) is permissionless. The report is an EIP-712 StandingReport (domain KaratumReputationFeed, version 1, bound to chain id and feed address), signed by the feed signer (a separate key that holds no funds; ERC-1271 contract signers work). Karatum serves signed reports at GET /v1/feeds/standing/{agent}?chainId= (10-minute deadline). A report is applied only if it is newer than what the feed holds (including a later full sync), so a replayed old report can never roll back a standing or undo a recall.

Validation on-chain. The feed refuses data a consumer could not interpret: unknown classes, tier bits on uncertified classes, a tier that disagrees with the class tiers or with K, a KCS outside 300..850, a band that disagrees with the score, out-of-range Shield or honesty codes. The updater validates the same rules locally first.

3. Staleness, heartbeat, pause

heartbeat() is the default maximum age (24 h on the pilot, 5 min to 30 days allowed). getStandingWithStatus(agent, maxAge) returns the standing and a stale flag (never reverts); getStandingFresh reverts with StaleStanding. maxAge 0 means the heartbeat; a consumer may pass a tighter or looser window. A feed is stale for an agent when the feed has never seen it, is paused by the owner, or the age exceeds the limit. A stale feed is never "clean". The library's check puts RECALLED before STALE, so a recalled agent is refused whatever the age. Where the consumer must not punish an outage (the NFT gate never burns a membership on STALE), say so explicitly, as the example does.

Round data: latestRoundData() returns (roundId, updatedAt, agentsUpdated); getRoundData(id) adds fullSync. Pull submissions do not open rounds.

4. Roles and trust

RoleCanCannot
owner (Ownable2Step; one EOA on the pilot; cannot renounce)set updater, signer, heartbeat, incident log (a contract, or none); pause; widen the class mask. Because it appoints the updater and the signer, it can in effect write any standing by appointing itself; a malicious incident log could mark any agent recalledwrite a standing in a single call
updaterpush batcheschange roles
signersign pull reportssend transactions that matter (holds no funds)
anyonesubmit a signed report; read

The feed is an oracle: consumers trust Karatum's computation of the standing (documented in the other methodology pages) and the updater/signer keys. It is not upgradeable, holds no ETH or tokens, and makes one optional external call (the incident log, staticcall, failure treated as "not recalled by the log": fail open). A failed log answer is visible: incidentLogStatus(agent) returns (recalled, ok), and pushBatch, submitReport and the public checkIncidentLog(agent) emit IncidentLogCheckFailed(log, agent), which the health check can alert on. setIncidentLog accepts only a contract (or address(0) to unwire). Testnet pilot: the owner is a single EOA; production would be a multisig with a timelock on role changes. The consumer examples are Foundry-tested but not audited and are not lending or custody products.

5. Developer guide

solidity
import {IKaratumReputation} from "karatum/feeds/IKaratumReputation.sol";
import {KaratumReputationLib as Rep} from "karatum/feeds/KaratumReputationLib.sol";

// raw, one call
IKaratumReputation.Standing memory s = IKaratumReputation(FEED).getStanding(agent);

// fail-closed gate: fresh, not recalled, certified for web3 at tier >= 2, KCS band >= good
Rep.require_(IKaratumReputation(FEED), agent, Rep.Requirement({
    classes: Rep.CLASS_WEB3, minTier: 2, minKcsBand: Rep.BAND_GOOD, minShieldTier: 0, minHonestyBand: 0, maxAge: 0
}));

// non-reverting: Rep.Verdict v; (v, s) = Rep.check(feed, agent, req); // OK, STALE, RECALLED, NOT_CERTIFIED, TIER_TOO_LOW, ...

Requirement.classes is a mask: every class in it must be certified, each at minTier or above (a per-class tier, so a 22K web3 agent with a 14K software certificate fails a software tier-2 gate). With classes 0 the overall tier is judged. Use the interface version check (interfaceVersion() == 1) in a deployment script. Do not cache a standing across transactions without keeping its updatedAt.

Examples (contracts/src/feeds/examples/): ReputationLendingPool (borrow cap by KCS band; recall, STALE or tier loss make the cap 0; no interest or liquidation), ReputationMembership (soulbound NFT / DAO gate for tier >= 2: isMember re-reads the feed, prune burns on recall or downgrade but never on STALE), ReputationSafeModule (a Safe module: approved agent wallets spend native token up to a daily cap per band; enable with enableModule). Tests include stale, recalled, tier-change and per-class paths.

Off-chain: GET /v1/feeds (deployments, last round, age, health), GET /v1/feeds/standings, GET /v1/feeds/standing/{agent}?chainId= (standing, what each chain holds, signed report). TypeScript: @karatum/feeds (loadStandings, planRound, signReport).

6. Deployments (testnet, 2026-10-16)

The feed has the same address on every chain, 0x3e98094028cF9879A82B66f62f16edCaEfE7f16b (CREATE2 through the deterministic deployer 0x4e59…956C with identical constructor arguments, contracts/script/DeployFeeds.s.sol; the owner then wires the incident log on Base Sepolia and Ethereum Sepolia). Redeployed 2026-10-17 with the review fixes; the first feed 0xa20e8546…c4a8 is retired. Roles: owner 0xa4AB…Af43, updater 0x4F90…0DFF, signer 0x9130…0799.

ChainDeploy tx (CREATE2)First round (11 agents, full sync)
Base Sepolia0x14671c…620d0xf43a20…adc8
Ethereum Sepolia0xae4b00…9fb30x8a8f89…683a
OP Sepolia0xe007cc…d56d0x690171…564e
Arbitrum Sepolia0x8e7a0b…04230x0bda51…4e67

Incident log wiring (owner): Base Sepolia 0xbfbb95…f725, Ethereum Sepolia 0x8e2500…9454. The first feed's transactions (2026-10-16) are kept in contracts/deployments/feeds.json under previous.

Pull mode, live: a signed report fetched from https://karatum.com/v1/feeds/standing/0x376c…fa69fae?chainId=84532 was relayed with submitReport on Base Sepolia, 0x431018…93eb (60,872 gas), proving the TypeScript signer and the contract's EIP-712 digest agree.

Moca Chain testnet (chain id 222888, public RPC https://testnet-rpc.mocachain.org answers): supported by the code (FEED_CHAINS), not deployed: the updater wallet holds no test MOCA and no public faucet could be used without a human. To add it: fund the updater address, FEEDS_CHAINS=222888 tsx services/feeds/src/cli.ts deploy 222888 <artifact> --send, then the timer picks it up.

7. Numbers (measured)

Foundry gas (test_gasBatches): a batch of 100 new agents 40,005 gas/agent, rewriting 100 existing agents 22,764 gas/agent, a single new agent 98,606, a heartbeat full sync (111 agents known) 59,689, a cold getStandingWithStatus read ~23,900. Live: the first 7-agent round cost 325,596 gas on Base, OP and Arbitrum and 1,030,956 on Ethereum Sepolia (Sepolia's gas accounting for the same calldata; the L2 figures exclude L1 data fees). Bytecode 13.6 KB runtime.

8. Monitoring

karatum-feeds.timer (15 min) records, per chain, the on-chain round, last full sync, heartbeat, pause flag and the updater balance in feed_chain_status. karatum-healthcheck (every 5 min, Resend email on change) fails feed chain <id> when the last full sync is older than 90% of the heartbeat, the updater has not looked for 3 hours, the feed is paused, an error is recorded, or the updater balance is under 0.0002 (native token). The updater refuses to push below that balance and says so in the status row.

9. Limits

  • Standing is as fresh as its inputs: the KCS is the last persisted snapshot (the credit timer), Shield and Honesty are periodic. updatedAt says when Karatum confirmed the record, not when each input was computed.
  • A KCS move under 10 points inside a band is not pushed; the on-chain kcs can lag by up to that. The band never lags beyond the heartbeat.
  • Only wallets bound to a Karatum build appear (7 on the pilot). The feed says nothing about other agents; a missing record is STALE, not "bad".
  • tier and karats are maxima over classes; use classTiers and the library for class-specific decisions.
  • Cross-chain recalls: where no Incident Log exists (OP, Arbitrum), a recall reaches the feed through the updater within one 15-minute cycle, not in the same block.