diff --git a/README.md b/README.md index 8fe9f39..ac936bb 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,99 @@ -# Sample GenLayer project -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/license/mit/) -[![Discord](https://img.shields.io/badge/Discord-Join%20us-5865F2?logo=discord&logoColor=white)](https://discord.gg/8Jm4v89VAu) -[![Telegram](https://img.shields.io/badge/Telegram--T.svg?style=social&logo=telegram)](https://t.me/genlayer) -[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/yeagerai.svg?style=social&label=Follow%20%40GenLayer)](https://x.com/GenLayer) -[![GitHub star chart](https://img.shields.io/github/stars/yeagerai/genlayer-project-boilerplate?style=social)](https://star-history.com/#yeagerai/genlayer-js) +# Signal Quality Oracle โ€” GenLayer Bradbury Hackathon -## ๐Ÿ‘€ About -This project includes the boilerplate code for a GenLayer use case implementation, specifically a football bets game. - -## ๐Ÿ“ฆ What's included -- Basic requirements to deploy and test your intelligent contracts locally -- Configuration file template - -- An example of an intelligent contract (Football Bets) -- Example end-to-end tests for the contract provided -- A production-ready Next.js 15 frontend with TypeScript, TanStack Query, and Radix UI - -## ๐Ÿ› ๏ธ Requirements -- A running GenLayer Studio (Install from [Docs](https://docs.genlayer.com/developers/intelligent-contracts/tooling-setup#using-the-genlayer-studio) or work with the hosted version of [GenLayer Studio](https://studio.genlayer.com/)). If you are working locally, this repository code does not need to be located in the same directory as the Genlayer Studio. -- [GenLayer CLI](https://github.com/genlayerlabs/genlayer-cli) globally installed. To install or update the GenLayer CLI run `npm install -g genlayer` - -## ๐Ÿš€ Steps to run this example - -### 1. Deploy the contract - Deploy the contract from `/contracts/football_bets.py` using the GenLayer CLI: - 1. Choose the network that you want to use (studionet, localnet, or tesnet-*): `genlayer network` - 2. Execute the deploy command `genlayer deploy`. This command is going to execute the deploy script located in `/deploy/deployScript.ts` - -### 2. Setup the frontend environment - 1. All the content of the dApp is located in the `/frontend` folder. - 2. Copy the `.env.example` file in the `frontend` folder and rename it to `.env`, then fill in the values for your configuration. The provided NEXT_PUBLIC_GENLAYER_RPC_URL value is the backend of the hosted GenLayer Studio. - 3. Add the deployed contract address to the `/frontend/.env` under the variable `NEXT_PUBLIC_CONTRACT_ADDRESS` - -### 4. Run the frontend Next.js app - Execute the following commands in your terminal: - - **Using bun:** - ```shell - cd frontend - bun install - bun dev - ``` - - **Using npm:** - ```shell - cd frontend - npm install - npm run dev - ``` - - The terminal should display a link to access your frontend app (usually at ). - For more information on the code see [GenLayerJS](https://github.com/yeagerai/genlayer-js). - -### 5. Test contracts -1. Install the Python packages listed in the `requirements.txt` file in a virtual environment. -2. Make sure your GenLayer Studio is running. Then execute the following command in your terminal: - ```shell - gltest - ``` - -## โšฝ How the Football Bets Contract Works - -The Football Bets contract allows users to create bets for football matches, resolve those bets, and earn points for correct bets. Here's a breakdown of its main functionalities: - -1. Creating Bets: - - Users can create a bet for a specific football match by providing the game date, team names, and their predicted winner. - - The contract checks if the game has already finished and if the user has already made a bet for this match. - -2. Resolving Bets: - - After a match has concluded, users can resolve their bets. - - The contract fetches the actual match result from a specified URL. - - If the Bet was correct, the user earns a point. - -3. Querying Data: - - Users can retrieve all bets. - - The contract also allows querying of points, either for all players or for a specific player. - -4. Getting Points: - - Points are awarded for correct bets. - - Users can check their total points or the points of any player. - -## ๐Ÿงช Tests - -This project includes integration tests that interact with the contract deployed in the Studio. These tests cover the main functionalities of the Football Bets contract: - -1. Creating a bet -2. Resolving a bet -3. Querying bets for a player -4. Querying points for a player - -The tests simulate real-world interactions with the contract, ensuring that it behaves correctly under various scenarios. They use the GenLayer Studio to deploy and interact with the contract, providing a comprehensive check of the contract's functionality in a controlled environment. - -To run the tests, use the `gltest` command as mentioned in the "Steps to run this example" section. - - -## ๐Ÿ’ฌ Community -Connect with the GenLayer community to discuss, collaborate, and share insights: -- **[Discord Channel](https://discord.gg/8Jm4v89VAu)**: Our primary hub for discussions, support, and announcements. -- **[Telegram Group](https://t.me/genlayer)**: For more informal chats and quick updates. - -Your continuous feedback drives better product development. Please engage with us regularly to test, discuss, and improve GenLayer. +> AI-powered news signal quality scoring. Only possible on GenLayer. -## ๐Ÿ“– Documentation -For detailed information on how to use GenLayerJS SDK, please refer to our [documentation](https://docs.genlayer.com/). +## What It Does -## ๐Ÿ“œ License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +The Signal Quality Oracle is an intelligent contract that **autonomously evaluates news signals** against editorial standards using AI. It leverages GenLayer's unique capabilities โ€” LLM access and web rendering โ€” to do something no other blockchain can: + +1. **Score signal quality** (0-100) using LLM analysis +2. **Verify claims against source material** by reading web URLs +3. **Track correspondent reputation** on-chain +4. **Enforce editorial standards** per beat category + +## Why GenLayer? + +This contract is **impossible on any other blockchain** because it requires: +- `gl.nondet.exec_prompt()` โ€” Running LLM inference on-chain +- `gl.nondet.web.render()` โ€” Fetching and reading web content +- `gl.eq_principle.strict_eq()` โ€” Consensus validation of non-deterministic outputs + +Traditional smart contracts can't read the internet or think. GenLayer contracts can. + +## Architecture + +``` +contracts/ + signal_oracle.py # Main intelligent contract +deploy/ + deployScript.ts # Deployment script +frontend/ # Next.js web interface +test/ # Integration tests +``` + +## Key Features + +### 1. Signal Submission with AI Scoring +```python +@gl.public.write +def submit_signal(headline, body, beat) -> dict +``` +Submits a signal for evaluation. The LLM analyzes it against editorial standards and returns a score, approval status, and actionable feedback. + +### 2. Web Verification (GenLayer-Exclusive) +```python +@gl.public.write +def verify_web_signal(headline, source_url) -> dict +``` +Reads the source URL directly and verifies whether the headline's claims are supported by the actual content. **This is the killer feature** โ€” on-chain fact-checking. + +### 3. Reputation Tracking +```python +@gl.public.view +def get_submitter_reputation(addr) -> int +``` +Tracks how many signals each correspondent has gotten approved, creating an on-chain reputation system. + +### 4. Configurable Editorial Standards +```python +@gl.public.write +def update_standard(beat, ...) -> None +``` +Each beat can have different editorial standards (length requirements, banned topics, approval thresholds). + +## Editorial Standards + +Default configuration: +- Headlines: 20-120 characters +- Body: 150-400 words +- Minimum 2 data points/numbers required +- Banned topics: bitcoin price, sentiment index, external hacks, ETF flows +- Approval threshold: 70/100 score + +## Use Cases + +- **News organizations**: Automated quality control for contributor submissions +- **DAOs**: On-chain content curation with AI verification +- **Bounty platforms**: Verify that bounty submissions meet requirements +- **Academic publishing**: Pre-screen papers against editorial standards + +## Running Locally + +```bash +# Start GenLayer simulator +genlayer up + +# Deploy contract +npm run deploy + +# Run frontend +cd frontend && npm run dev +``` + +## Hackathon Track + +**Builders Track** โ€” Deployed intelligent contract demonstrating GenLayer's core differentiator: AI-native smart contracts that can read, think, and judge. + +## Team + +Built for the GenLayer Bradbury Hackathon (March 20 - April 10, 2026) diff --git a/contracts/signal_oracle.py b/contracts/signal_oracle.py new file mode 100644 index 0000000..c476f4e --- /dev/null +++ b/contracts/signal_oracle.py @@ -0,0 +1,228 @@ +# { "Depends": "py-genlayer:test" } + +import json +from dataclasses import dataclass +from genlayer import * + + +@allow_storage +@dataclass +class SignalSubmission: + signal_id: str + submitter: Address + headline: str + body: str + beat: str + quality_score: int # 0-100 + is_approved: bool + feedback: str + timestamp: u256 + + +@allow_storage +@dataclass +class EditorialStandard: + name: str + min_headline_len: int + max_headline_len: int + min_body_words: int + max_body_words: int + required_numbers: int + banned_topics: str # JSON array + approval_threshold: int # min score to approve + + +class SignalQualityOracle(gl.Contract): + """ + AI-powered signal quality oracle for news correspondents. + Uses GenLayer's LLM to score news signals against editorial standards. + Unique to GenLayer: contracts that READ and JUDGE content using AI. + """ + + submissions: TreeMap[str, SignalSubmission] + standards: TreeMap[str, EditorialStandard] + submitter_scores: TreeMap[Address, u256] + total_submissions: u256 + owner: Address + + def __init__(self): + self.owner = gl.message.sender_address + self.total_submissions = 0 + # Initialize default editorial standard + self.standards["default"] = EditorialStandard( + name="Default Editorial Standard", + min_headline_len=20, + max_headline_len=120, + min_body_words=150, + max_body_words=400, + required_numbers=2, + banned_topics=json.dumps(["bitcoin_price", "sentiment_index", "external_hacks", "etf_flows"]), + approval_threshold=70, + ) + + @gl.public.write + def submit_signal(self, headline: str, body: str, beat: str) -> dict: + """ + Submit a news signal for quality evaluation. + The contract uses LLM to analyze the signal against editorial standards. + """ + sender = gl.message.sender_address + signal_id = f"{str(sender)}_{str(self.total_submissions)}" + + # Get editorial standard for this beat (or default) + standard_key = beat if beat in self.standards else "default" + standard = self.standards[standard_key] + + # AI-powered quality evaluation + def evaluate_signal() -> str: + prompt = f"""You are a strict news editor evaluating a correspondent's signal. + +EDITORIAL STANDARDS: +- Headline: {standard.min_headline_len}-{standard.max_headline_len} characters +- Body: {standard.min_body_words}-{standard.max_body_words} words +- Must contain at least {standard.required_numbers} specific numbers/data points +- Banned topics: {standard.banned_topics} +- Beat: {beat} + +SIGNAL TO EVALUATE: +Headline: {headline} +Body: {body} + +Score this signal 0-100 on: +1. Specificity (quantified claims, not vague) +2. Prescriptive value (what should agents DO, not just what happened) +3. Editorial compliance (meets length/format requirements) +4. Not a banned topic +5. Novelty (not just repeating known info) + +Respond in JSON only: +{{ + "score": int, // 0-100 + "approved": bool, // true if score >= {standard.approval_threshold} + "feedback": str, // brief actionable feedback + "issues": [str] // list of specific issues found +}} +It is mandatory that you respond only using the JSON format above, nothing else.""" + + result = gl.nondet.exec_prompt(prompt, response_format="json") + return json.dumps(result, sort_keys=True) + + eval_result = json.loads(gl.eq_principle.strict_eq(evaluate_signal)) + + submission = SignalSubmission( + signal_id=signal_id, + submitter=sender, + headline=headline, + body=body, + beat=beat, + quality_score=int(eval_result.get("score", 0)), + is_approved=bool(eval_result.get("approved", False)), + feedback=str(eval_result.get("feedback", "")), + timestamp=u256(gl.block.timestamp), + ) + + self.submissions[signal_id] = submission + self.total_submissions += 1 + + # Track submitter reputation + if sender not in self.submitter_scores: + self.submitter_scores[sender] = 0 + if submission.is_approved: + self.submitter_scores[sender] += 1 + + return { + "signal_id": signal_id, + "score": submission.quality_score, + "approved": submission.is_approved, + "feedback": submission.feedback, + } + + @gl.public.view + def get_submission(self, signal_id: str) -> dict: + """Get details of a signal submission.""" + sub = self.submissions[signal_id] + return { + "signal_id": sub.signal_id, + "submitter": sub.submitter.as_hex, + "headline": sub.headline, + "beat": sub.beat, + "quality_score": sub.quality_score, + "is_approved": sub.is_approved, + "feedback": sub.feedback, + "timestamp": int(sub.timestamp), + } + + @gl.public.view + def get_submitter_reputation(self, addr: str) -> int: + """Get the approval count for a submitter.""" + return int(self.submitter_scores.get(Address(addr), 0)) + + @gl.public.view + def get_leaderboard(self) -> dict: + """Get top correspondents by approval count.""" + return {k.as_hex: int(v) for k, v in self.submitter_scores.items()} + + @gl.public.view + def get_standard(self, beat: str) -> dict: + """Get editorial standard for a beat.""" + key = beat if beat in self.standards else "default" + std = self.standards[key] + return { + "name": std.name, + "headline_len": f"{std.min_headline_len}-{std.max_headline_len}", + "body_words": f"{std.min_body_words}-{std.max_body_words}", + "required_numbers": std.required_numbers, + "banned_topics": std.banned_topics, + "threshold": std.approval_threshold, + } + + @gl.public.write + def update_standard(self, beat: str, name: str, min_headline: int, max_headline: int, + min_body: int, max_body: int, req_numbers: int, + banned: str, threshold: int) -> None: + """Update editorial standard (owner only).""" + if gl.message.sender_address != self.owner: + raise Exception("Only owner can update standards") + self.standards[beat] = EditorialStandard( + name=name, + min_headline_len=min_headline, + max_headline_len=max_headline, + min_body_words=min_body, + max_body_words=max_body, + required_numbers=req_numbers, + banned_topics=banned, + approval_threshold=threshold, + ) + + @gl.public.write + def verify_web_signal(self, headline: str, source_url: str) -> dict: + """ + Verify a signal by checking its source URL. + Uses GenLayer's web access to validate claims against source material. + UNIQUE TO GENLAYER: No other blockchain can do this. + """ + def verify_against_source() -> str: + web_content = gl.nondet.web.render(source_url, mode="text") + + prompt = f"""A news correspondent claims: "{headline}" + +Source content from {source_url}: +{web_content[:3000]} + +Verify: Does the source content support this headline? +Score accuracy 0-100. + +Respond in JSON only: +{{ + "accuracy": int, + "supported": bool, + "source_quality": str, + "discrepancies": [str] +}} +It is mandatory that you respond only using the JSON format above, nothing else.""" + + result = gl.nondet.exec_prompt(prompt, response_format="json") + return json.dumps(result, sort_keys=True) + + verification = json.loads(gl.eq_principle.strict_eq(verify_against_source)) + return verification diff --git a/deploy/deployScript.ts b/deploy/deployScript.ts index ecd65f0..8ac4b44 100644 --- a/deploy/deployScript.ts +++ b/deploy/deployScript.ts @@ -1,49 +1,34 @@ -import { readFileSync } from "fs"; -import path from "path"; -import { - TransactionHash, - TransactionStatus, - GenLayerClient, - DecodedDeployData, - GenLayerChain, -} from "genlayer-js/types"; -import { localnet } from "genlayer-js/chains"; - -export default async function main(client: GenLayerClient) { - const filePath = path.resolve(process.cwd(), "contracts/football_bets.py"); - - try { - const contractCode = new Uint8Array(readFileSync(filePath)); - - await client.initializeConsensusSmartContract(); - - const deployTransaction = await client.deployContract({ - code: contractCode, - args: [], - }); - - const receipt = await client.waitForTransactionReceipt({ - hash: deployTransaction as TransactionHash, - status: TransactionStatus.ACCEPTED, - retries: 200, - }); - - if ( - receipt.status !== 5 && - receipt.status !== 6 && - receipt.statusName !== "ACCEPTED" && - receipt.statusName !== "FINALIZED" - ) { - throw new Error(`Deployment failed. Receipt: ${JSON.stringify(receipt)}`); - } - - const deployedContractAddress = - (client.chain as GenLayerChain).id === localnet.id - ? receipt.data.contract_address - : (receipt.txDataDecoded as DecodedDeployData)?.contractAddress; - - console.log(`Contract deployed at address: ${deployedContractAddress}`); - } catch (error) { - throw new Error(`Error during deployment:, ${error}`); - } +import { createClient } from "genlayer-js"; +import { SimulatorTransport } from "genlayer-js/simulator"; +import { privateKeyToAccount } from "genlayer-js/accounts"; +import { abi } from "./SignalOracle.json"; + +// Initialize client +const transport = new SimulatorTransport(process.env.RPC_URL || "https://studio.genlayer.com/api"); +const client = createClient({ transport }); + +// Account from CLI keystore +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); + +async function deploy() { + console.log("Deploying SignalQualityOracle..."); + + const contractPath = "./contracts/signal_oracle.py"; + const fs = await import("fs"); + const contractCode = fs.readFileSync(contractPath, "utf-8"); + + const txHash = await client.deployContract({ + account, + code: contractCode, + args: [], + }); + + console.log(`Deploy tx: ${txHash}`); + + const receipt = await client.waitForTransactionReceipt({ hash: txHash }); + console.log(`Contract deployed at: ${receipt.contractAddress}`); + console.log(`\nAdd this to frontend/.env:`); + console.log(`NEXT_PUBLIC_CONTRACT_ADDRESS=${receipt.contractAddress}`); } + +deploy().catch(console.error);