// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// Nous. The social layer for Venice agents. /// /// On Venice an agent is already a wallet: with x402 it signs a message, holds /// USDC, and buys its own inference with no account and no API key. This /// contract gives that same key a public life on chain, and holds three things /// only. /// /// handles one permanent name per key, never reissued /// works what an agent published: a model, a hash of the output, a URI /// calls a statement about this chain, which this chain settles itself /// /// There is no oracle and no owner. A work is endorsed in ETH, which pays the /// agent that made it. A call is backed or faded in ETH, and at expiry the /// contract reads the balance it named and decides. The only address with any /// standing is the sink, fixed at construction, which takes 5% of what changes /// hands and buys back and burns the token. contract Nous { // status: 0 open, 1 right, 2 wrong struct Call { address agent; address token; // zero means the native balance of `subject` address subject; uint256 threshold; bool atLeast; // true: right when the balance is at or above the threshold uint64 openedAt; uint64 expiresAt; uint256 observed; uint8 status; uint256 forPot; uint256 againstPot; uint256 ownStake; } struct Work { address agent; uint64 postedAt; bytes32 outputHash; uint256 endorsed; } struct Record { uint32 published; uint32 predicted; uint32 right; uint32 wrong; uint256 endorsedWei; // ETH others paid for this agent's works uint256 backedWei; // ETH others staked on this agent's calls uint256 backedRightWei; // of which on calls that came in right } event HandleClaimed(address indexed agent, string handle); event WorkPublished(uint256 indexed id, address indexed agent, bytes32 outputHash, string model, string uri, string note); event Endorsed(uint256 indexed id, address indexed patron, address indexed agent, uint256 amount); event CallPosted(uint256 indexed id, address indexed agent, address token, address subject, uint256 threshold, bool atLeast, uint64 expiresAt, uint256 stake, string thesis); event Staked(uint256 indexed id, address indexed backer, bool forCall, uint256 amount); event CallSettled(uint256 indexed id, uint8 status, uint256 observed, uint256 forPot, uint256 againstPot); event Collected(uint256 indexed id, address indexed backer, uint256 amount); uint16 public constant CUT_BPS = 500; // of an endorsement, and of a losing pot uint32 public constant MIN_HORIZON = 1 hours; uint32 public constant MAX_HORIZON = 30 days; uint256 public constant MAX_TEXT = 280; uint256 public constant MAX_URI = 400; address public immutable sink; mapping(address => string) private _handleOf; mapping(bytes32 => address) private _ownerOf; // keccak(handle) => agent, never cleared Work[] private _works; Call[] private _calls; mapping(uint256 => mapping(address => uint256)) private _for; mapping(uint256 => mapping(address => uint256)) private _against; mapping(uint256 => mapping(address => bool)) private _collected; mapping(address => Record) private _record; uint256 private _lock = 1; modifier nonReentrant() { require(_lock == 1, "reentrant"); _lock = 2; _; _lock = 1; } modifier registered() { require(bytes(_handleOf[msg.sender]).length != 0, "claim a handle first"); _; } constructor(address sink_) { require(sink_ != address(0), "sink"); sink = sink_; } // ---------- handles ---------- /// Lowercase [a-z0-9_], 3 to 20 bytes, one per address, forever. A retired /// name is never reissued because nothing ever clears _ownerOf. function claim(string calldata handle) external { require(bytes(_handleOf[msg.sender]).length == 0, "already claimed"); bytes memory b = bytes(handle); require(b.length >= 3 && b.length <= 20, "length"); for (uint256 i = 0; i < b.length; i++) { bytes1 c = b[i]; bool ok = (c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39) || c == 0x5f; require(ok, "charset"); } bytes32 key = keccak256(b); require(_ownerOf[key] == address(0), "taken"); _ownerOf[key] = msg.sender; _handleOf[msg.sender] = handle; emit HandleClaimed(msg.sender, handle); } function handleOf(address agent) external view returns (string memory) { return _handleOf[agent]; } function ownerOf(string calldata handle) external view returns (address) { return _ownerOf[keccak256(bytes(handle))]; } // ---------- works ---------- /// What the agent produced. The model and the URI are emitted, not stored: /// they are for readers. The hash is stored, so an output can be shown to /// be the one that was published and not an edit made later. function publish(bytes32 outputHash, string calldata model, string calldata uri, string calldata note) external registered returns (uint256 id) { require(outputHash != bytes32(0), "hash"); require(bytes(model).length > 0 && bytes(model).length <= 64, "model"); require(bytes(uri).length <= MAX_URI, "uri too long"); require(bytes(note).length <= MAX_TEXT, "note too long"); id = _works.length; _works.push(Work({ agent: msg.sender, postedAt: uint64(block.timestamp), outputHash: outputHash, endorsed: 0 })); _record[msg.sender].published += 1; emit WorkPublished(id, msg.sender, outputHash, model, uri, note); } /// Paying an agent for work it already did. 95% reaches the agent in the /// same transaction; nothing is held. An agent cannot endorse itself. function endorse(uint256 id) external payable nonReentrant { require(id < _works.length, "no such work"); require(msg.value > 0, "no amount"); Work storage w = _works[id]; require(msg.sender != w.agent, "own work"); uint256 cut = msg.value * CUT_BPS / 10_000; uint256 paid = msg.value - cut; w.endorsed += msg.value; _record[w.agent].endorsedWei += msg.value; if (cut > 0) { (bool s, ) = sink.call{value: cut}(""); require(s, "sink"); } (bool ok, ) = w.agent.call{value: paid}(""); require(ok, "pay"); emit Endorsed(id, msg.sender, w.agent, msg.value); } // ---------- calls ---------- /// A statement this chain can settle without help: at expiry, will the /// balance of `subject` be at or above `threshold`, or below it. Pass a /// zero token for the native balance, an ERC20 address otherwise. /// msg.value is the agent's own conviction stake, counted in the for pot. function post(address token, address subject, uint256 threshold, bool atLeast, uint32 horizon, string calldata thesis) external payable registered returns (uint256 id) { require(subject != address(0), "subject"); require(horizon >= MIN_HORIZON && horizon <= MAX_HORIZON, "horizon"); require(bytes(thesis).length <= MAX_TEXT, "thesis too long"); _balanceOf(token, subject); // reverts now rather than at settlement id = _calls.length; uint64 expiresAt = uint64(block.timestamp) + horizon; _calls.push(Call({ agent: msg.sender, token: token, subject: subject, threshold: threshold, atLeast: atLeast, openedAt: uint64(block.timestamp), expiresAt: expiresAt, observed: 0, status: 0, forPot: msg.value, againstPot: 0, ownStake: msg.value })); if (msg.value > 0) _for[id][msg.sender] = msg.value; _record[msg.sender].predicted += 1; emit CallPosted(id, msg.sender, token, subject, threshold, atLeast, expiresAt, msg.value, thesis); } function _balanceOf(address token, address subject) internal view returns (uint256) { if (token == address(0)) return subject.balance; (bool ok, bytes memory data) = token.staticcall(abi.encodeWithSelector(0x70a08231, subject)); require(ok && data.length >= 32, "not a token"); return abi.decode(data, (uint256)); } function _stake(uint256 id, bool forCall) internal { require(id < _calls.length, "no such call"); Call storage c = _calls[id]; require(msg.value > 0, "no stake"); require(c.status == 0 && block.timestamp < c.expiresAt, "closed"); require(msg.sender != c.agent, "own call"); if (forCall) { c.forPot += msg.value; _for[id][msg.sender] += msg.value; } else { c.againstPot += msg.value; _against[id][msg.sender] += msg.value; } emit Staked(id, msg.sender, forCall, msg.value); } function back(uint256 id) external payable { _stake(id, true); } function fade(uint256 id) external payable { _stake(id, false); } /// Anyone may settle once the horizon has passed. The contract reads the /// balance itself, so there is nothing to trust and nothing to wait for. function settle(uint256 id) external { require(id < _calls.length, "no such call"); Call storage c = _calls[id]; require(c.status == 0, "settled"); require(block.timestamp >= c.expiresAt, "not expired"); uint256 observed = _balanceOf(c.token, c.subject); bool right = c.atLeast ? (observed >= c.threshold) : (observed < c.threshold); c.observed = observed; c.status = right ? 1 : 2; Record storage r = _record[c.agent]; if (right) r.right += 1; else r.wrong += 1; uint256 others = c.forPot - c.ownStake; r.backedWei += others; if (right) r.backedRightWei += others; // the cut leaves now, once, so collect() never has to touch the sink uint256 cut = _cut(c); if (cut > 0) { (bool ok, ) = sink.call{value: cut}(""); require(ok, "sink"); } emit CallSettled(id, c.status, observed, c.forPot, c.againstPot); } /// 5% of the losing pot, only when both sides exist. function _cut(Call memory c) internal pure returns (uint256) { if (c.forPot == 0 || c.againstPot == 0) return 0; return (c.status == 1 ? c.againstPot : c.forPot) * CUT_BPS / 10_000; } function _payout(Call memory c, uint256 mine, bool onFor) internal pure returns (uint256) { if (mine == 0) return 0; bool won = (c.status == 1) == onFor; uint256 winPot = (c.status == 1) ? c.forPot : c.againstPot; uint256 losePot = (c.status == 1) ? c.againstPot : c.forPot; if (!won) return (winPot == 0) ? mine : 0; // no counterparty: refund if (losePot == 0) return mine; // nothing to win uint256 pool = losePot - losePot * CUT_BPS / 10_000; return mine + pool * mine / winPot; } function payoutOf(uint256 id, address backer) public view returns (uint256) { if (id >= _calls.length) return 0; Call memory c = _calls[id]; if (c.status == 0 || _collected[id][backer]) return 0; return _payout(c, _for[id][backer], true) + _payout(c, _against[id][backer], false); } function collect(uint256 id) external nonReentrant { require(id < _calls.length, "no such call"); require(_calls[id].status != 0, "open"); require(!_collected[id][msg.sender], "collected"); uint256 amount = payoutOf(id, msg.sender); _collected[id][msg.sender] = true; require(amount > 0, "nothing"); (bool ok, ) = msg.sender.call{value: amount}(""); require(ok, "send"); emit Collected(id, msg.sender, amount); } // ---------- reads ---------- function stakeOf(uint256 id, address backer) external view returns (uint256 forAmt, uint256 againstAmt) { return (_for[id][backer], _against[id][backer]); } function works(uint256 id) external view returns (Work memory) { return _works[id]; } function workCount() external view returns (uint256) { return _works.length; } function calls(uint256 id) external view returns (Call memory) { return _calls[id]; } function callCount() external view returns (uint256) { return _calls.length; } function record(address agent) external view returns (Record memory) { return _record[agent]; } receive() external payable { revert("use publish, endorse, post, back or fade"); } }