Bounty marketplace for Twitter API requests on GenLayer. Post a request, someone fulfills it with a cryptographic proof (TLS Notary or zkTLS), validators verify, response stored on-chain.
Two proving approaches available:
- TLS Notary - MPC-based, fast (~seconds), requires trusted notary
- zkTLS - Zero-knowledge, trustless, slower (~minutes, needs GPU)
See COMPARISON.md for detailed analysis.
Twitter keeps changing who can access their API. This creates a workaround: a marketplace where anyone can request Twitter data and anyone with API access can fulfill those requests. Responses are cryptographically verified using TLS Notary, so you don't have to trust the fulfiller.
The response gets stored on-chain. Other protocols (like Rally) can then consume this verified data.
- User posts bounty: "I'll pay X for
/2/users/by/username/elonmusk" - Worker sees request, calls Twitter API through TLS Notary
- Worker submits proof, claims bounty
- Verified response stored on-chain
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β GenLayer β
β ββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββ β
β β TwitterMarketplace β β TLS Verifier (WASM) β β
β β (Python) ββββββΆβ β β
β β β β - Parses TLS Notary presentation β β
β β - Request storage β β - Validates notary signature β β
β β - Bounty management β β - Extracts server name, response β β
β β - Response validation β β - Returns verification result β β
β β - Response storage β β β β
β ββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² β²
β create_request() β proof bytes
β get_response() β
β β
βββββββββ΄ββββββββ ββββββββββ΄βββββββββ
β User β β Worker β
β β β (tls-prover) β
βββββββββββββββββ ββββββββββ¬βββββββββ
β
β TLS Notary MPC
βΌ
ββββββββββββββββββ
β Notary Server β
β β
β Witnesses TLS β
β Signs proof β
βββββββββ¬βββββββββ
β
βΌ
ββββββββββββββββββ
β Twitter API β
β api.twitter.comβ
ββββββββββββββββββ
Third party that participates in the TLS session via MPC (multi-party computation). The notary:
- Cooperates with the prover to complete the TLS handshake
- Never sees the plaintext (only encrypted data + MPC shares)
- Signs a commitment attesting "this ciphertext came from api.twitter.com"
This creates a portable proof: anyone can verify the signature without re-running the TLS session.
Monitors the marketplace contract for unfulfilled requests. For each request:
- Makes HTTP request to Twitter API through TLS Notary
- Uses
tlsncrate for MPC-TLS protocol - Generates
Presentationproof with selective disclosure - Submits proof to marketplace contract
Stack: Rust, tlsn, tokio, serde
Build:
cd tls-prover
cargo build --releaseStateless contract that verifies TLS Notary proofs on-chain.
Input: Serialized proof bytes
Output: JSON with valid, server_name, response_body, connection_time, notary_key_hex
The verifier:
- Parses the proof format (custom binary, not full
Presentation) - Validates the notary's signature over the commitment
- Extracts revealed data (server name, HTTP response)
- Returns structured result
Stack: Rust, compiled to wasm32-wasip1, runs in GenVM
Build:
cd tls-verifier-wasm
cargo build --release --target wasm32-wasip1
wasm-opt -O3 --disable-reference-types \
target/wasm32-wasip1/release/tls_verifier_wasm.wasm \
-o verifier.wasmNote: --disable-reference-types required - GenVM doesn't support WASM reference types.
Stateful contract managing the bounty marketplace.
Storage:
requests: TreeMap[str, str]- request_id β endpointbounties: TreeMap[str, u256]- request_id β amountresponses: TreeMap[str, str]- request_id β verified response JSONfulfilled: TreeMap[str, bool]- request_id β fulfilled
Methods:
create_request(endpoint: str)- Post bounty for an endpoint (payable)submit_proof(request_id: str, proof: bytes)- Submit proof, claim bountyget_pending_requests()- List unfulfilled requests (for workers)get_response(request_id: str)- Get stored response
Response Validation: Rejects rate limits (HTTP 429, error codes 88/89) and auth errors (401/403, codes 32/64/215). Accepts "not found" errors (codes 34/50/144) as valid responses.
Stack: Python, GenLayer SDK (genlayer-js), runs in GenVM
Alternative prover using zero-knowledge proofs instead of MPC.
- Makes standard TLS connection to Twitter API
- Records TLS session using
RecordableStream - Generates ZK proof via SP1/RISC0
- Submits proof to marketplace contract
Stack: Rust wrapper around zkTLS
Build:
# Build zkTLS CLI (requires SP1)
cd zktls-repo
RISC0_SKIP_BUILD_KERNELS=1 cargo build --release -F sp1-backend
# Build wrapper
cd ../zktls-prover
cargo build --releaseValidates zkTLS proof output format.
Note: This verifier validates the proof format, not the full ZK proof. Full Groth16 verification requires specialized libraries not easily portable to WASM.
Build:
cd zktls-verifier-wasm
cargo build --release --target wasm32-wasip1
wasm-opt -O3 --disable-reference-types --enable-bulk-memory \
target/wasm32-wasip1/release/zktls_verifier_wasm.wasm \
-o zktls_verifier.wasm1. User calls create_request("/2/users/by/username/elonmusk") with 0.001 ETH
2. Worker polls get_pending_requests(), sees new request
3. Worker runs TLS Notary flow:
- Connects to notary server (WebSocket)
- Starts MPC-TLS session with api.twitter.com
- Sends GET /2/users/by/username/elonmusk
- Receives response, generates proof
- Serializes proof to bytes
4. Worker calls submit_proof(request_id, proof_bytes)
5. Marketplace contract:
- Calls WASM verifier with proof bytes
- Verifier returns {valid: true, server_name: "api.twitter.com", response_body: "..."}
- Contract validates server_name contains "twitter.com" or "x.com"
- Contract validates response isn't rate limited
- Contract stores response on-chain
- Worker receives bounty
6. Anyone can call get_response(request_id) to read verified data
The verifier runs as a WASM contract in GenVM. Key technical details:
Calldata Encoding: GenVM uses a custom binary format (not ABI):
- ULEB128 for integers and lengths
- Type tags: MAP(6), ARR(5), BYTES(3), STR(4), PINT(1), SPECIAL(0)
- Map keys are raw strings (length + bytes), not TYPE_STR
WASM I/O:
- Input:
ExtendedMessagemap from stdin - Output: JSON string to stdout
Cross-Contract Calls:
verifier = gl.get_contract_at(self.verifier_address)
result = verifier.emit().verify(proof_bytes) # calls WASM contract- Rust with
wasm32-wasip1target:rustup target add wasm32-wasip1 - wasm-opt (binaryen):
brew install binaryen/apt install binaryen - Node.js 18+: for deployment scripts
- GenLayer Studio: Docker-based development environment
- TLS Notary server: for generating proofs (or use public notary)
- Twitter API key: Bearer token with v2 API access (workers only)
# Clone
git clone https://github.com/genlayerlabs/tls-twitter-bounty
cd tls-twitter-bounty
# Install Node dependencies
npm install
# Build WASM verifier
cd tls-verifier-wasm
cargo build --release --target wasm32-wasip1
wasm-opt -O3 --disable-reference-types \
target/wasm32-wasip1/release/tls_verifier_wasm.wasm \
-o verifier.wasm
# Build worker
cd ../tls-prover
cargo build --release
# Start GenLayer (requires genlayer-studio installed separately)
genlayer upFrom project root:
# Deploy WASM verifier
node deploy_wasm.mjs tls-verifier-wasm/verifier.wasm
# Note VERIFIER_ADDRESS
# Deploy marketplace contract
node deploy_bounty.mjs <VERIFIER_ADDRESS>
# Note BOUNTY_ADDRESScd tls-prover
# One-off mode (fulfill one request, exit)
cargo run --release -- \
--bounty-contract <BOUNTY_ADDRESS> \
--notary-host <NOTARY_URL> \
--twitter-bearer <BEARER_TOKEN> \
--one-off
# Watch mode (continuously monitor and fulfill)
cargo run --release -- \
--bounty-contract <BOUNTY_ADDRESS> \
--notary-host <NOTARY_URL> \
--twitter-bearer <BEARER_TOKEN> \
--watchAny Twitter API v2 endpoint:
/2/tweets/:id- Get tweet by ID/2/users/:id- Get user by ID/2/users/by/username/:username- Get user by username/2/tweets/search/recent?query=...- Search tweets- etc.
Compare TLS Notary vs zkTLS:
# Run benchmark (uses httpbin.org by default)
node benchmark/benchmark.mjs --mode mock
# With Twitter API
TWITTER_BEARER_TOKEN=xxx node benchmark/benchmark.mjs --mode mockResults saved to benchmark/benchmark_results.json.
tls-twitter-bounty/
βββ tls-prover/ # TLS Notary worker (Rust)
βββ tls-verifier-wasm/ # TLS Notary verifier (WASM)
βββ zktls-prover/ # zkTLS worker wrapper (Rust)
βββ zktls-verifier-wasm/ # zkTLS verifier (WASM)
βββ zktls-repo/ # zkTLS library (git clone)
βββ tweet-bounty/ # Marketplace contract (Python)
βββ benchmark/ # Comparison benchmarks
βββ deploy_wasm.mjs # WASM deployment script
βββ deploy_bounty.mjs # Bounty contract deployment
βββ COMPARISON.md # Detailed comparison
WASM contracts must be compiled with specific constraints for GenVM:
- No float-to-int saturation instructions (
SATURATING_FLOAT_TO_INTdisabled) - No reference types (use
--disable-reference-typesin wasm-opt) - Bulk memory operations supported
- Return values via
gl_callwith calldata-encoded{"Return": value}
zkTLS full verification: The current zkTLS WASM verifier only validates the output format. Full Groth16 ZK proof verification requires implementing pairing operations, which is complex in WASM.
MIT - see LICENSE