For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the ViralPost-only ClaimLauncher/ClaimVerifier entry with a generalized,
reason-tagged entry that carries a free-text claim + sources, migrate ViralPost onto it as
reason = ViralPost, and re-prove the existing live mint on the new rails.
Architecture: One general submit(reason, claim, sources, …) on Base; a reason-agnostic
claimId; a single reason-tagged payload bridged to ClaimVerifier, which decodes and dispatches
on reason to a per-reason handler (only ViralPost exists after this plan). The verdict payload
(GL→EVM) is unchanged, so VerdictReceiver and the relayer need no changes.
Tech Stack: Solidity (Foundry), GenLayer Python intelligent contracts (genlayer-test direct mode), Next.js/wagmi web app, GenLayer CLI for deploy, ethers/Supabase relayer (config only).
Reference: docs/design/2026-06-03-verified-claim-consensus.md (esp. §5 generalized entry,
§8 dedup/claimId).
sourcesis a single newline-separated string, not a Soliditystring[]. This keeps the cross-chain ABI to simple types only (gl.evm.decodeis proven forstr, not for dynamic string arrays), and avoids array handling on both sides. The web app joins URLs with\n.claimId = keccak256(abi.encode(uint8 reason, string canonicalPrimary))wherecanonicalPrimary= the first source line, or theclaimitself when there are no sources._canonicalstrips the query (?…) and any trailing/. This is the §8 "one token per(reason, source)" key.- Request payload tuple (Base → GL):
(bytes32 claimId, uint8 reason, string claim, string sources, string nameOverride, string symbolOverride, string descOverride, address launcher, uint256 graduationThreshold). - Verdict payload (GL → Base) is UNCHANGED:
(bytes32 claimId, uint8 status, string name, string symbol, string description, string claimRef). Reasonenum order (shared Solidity ⇄ Python):0 ViralPost, 1 RealWorldEvent, 2 GitHubRelease, 3 SportsResult, 4 OnchainMetric, 5 PriceTarget. OnlyViralPostis handled in this plan; the rest decode but returnFAILED_TRANSIENT("not yet implemented") so an unsupported reason never mints and never hard-fails.- ViralPost's content (the post URL) travels in
claim;sourcesis empty for ViralPost (the post URL is its own source).
| File | Responsibility | Action |
|---|---|---|
packages/contracts-evm/src/ClaimLauncher.sol |
generalized entry, claimId, payload, verdict callback | Modify (significant) |
packages/contracts-evm/test/ClaimLauncher.t.sol |
Foundry tests for the generalized launcher | Modify (rewrite cases) |
contracts-genlayer/ClaimVerifier.py |
decode generalized request, dispatch on reason, ViralPost handler | Modify |
contracts-genlayer/tests/direct/test_claim_verifier.py |
direct tests for decode/dispatch/ViralPost | Modify |
apps/web/src/hooks/useLaunchClaim.ts |
call new submit |
Modify |
apps/web/src/lib/launch.ts |
map wizard state → new args; join sources | Modify |
apps/web/src/lib/contracts.ts |
new launcher ABI + address | Modify |
packages/contracts-evm/deployments/base-sepolia.json |
record redeploy | Modify |
No changes: VerdictReceiver.sol, TokenFactory.sol, TokenpostToken.sol, the relayer code
(supabase/functions/relay/index.ts), the deploy script constructor (DeployClaimLoop.s.sol).
Files:
-
Modify:
packages/contracts-evm/src/ClaimLauncher.sol -
Test:
packages/contracts-evm/test/ClaimLauncher.t.sol -
Step 1: Write the failing test (append to
ClaimLauncherTest)
function test_claimId_isReasonPlusCanonicalSource() public view {
// ViralPost: no sources → primary = claim (the post URL), canonicalized (query+slash stripped)
bytes32 expected = keccak256(abi.encode(uint8(0), "https://x.com/jack/status/1934567890"));
assertEq(
launcher.claimIdForClaim(0, "https://x.com/jack/status/1934567890", ""),
expected
);
assertEq(
launcher.claimIdForClaim(0, "https://x.com/jack/status/1934567890?s=20", ""),
expected
);
assertEq(
launcher.claimIdForClaim(0, "https://x.com/jack/status/1934567890/", ""),
expected
);
}
function test_claimId_usesFirstSourceLineWhenPresent() public view {
// RealWorldEvent with a source → primary = first source line, canonicalized
bytes32 expected = keccak256(abi.encode(uint8(1), "https://docs.base.org/airdrop"));
assertEq(
launcher.claimIdForClaim(1, "Base teased an airdrop", "https://docs.base.org/airdrop?utm=x\nhttps://news.site/x"),
expected
);
}- Step 2: Run test to verify it fails
Run: cd packages/contracts-evm && forge test --match-test test_claimId_ -vv
Expected: FAIL — claimIdForClaim not defined.
- Step 3: Implement enum, struct, helpers in
ClaimLauncher.sol
Replace the Claim struct and add the Reason enum + helpers. The struct gains reason,
claim, sources; tweetUrl is removed.
enum Reason { ViralPost, RealWorldEvent, GitHubRelease, SportsResult, OnchainMetric, PriceTarget }
struct Claim {
Status status;
address launcher;
uint256 graduationThreshold;
uint8 retryCount;
uint8 reason;
string claim;
string sources; // newline-separated URLs ("" allowed)
string nameOverride;
string symbolOverride;
string descOverride;
}Add these pure helpers (replace the old _extractTweetId/claimIdFor):
/// @notice Public helper so off-chain callers and tests can derive the claimId.
function claimIdForClaim(uint8 reason, string calldata claim, string calldata sources)
external
pure
returns (bytes32)
{
return keccak256(abi.encode(reason, _canonical(_primary(claim, sources))));
}
/// @dev first line of `sources`, or `claim` when `sources` is empty.
function _primary(string memory claim, string memory sources) internal pure returns (string memory) {
bytes memory s = bytes(sources);
if (s.length == 0) return claim;
uint256 end = s.length;
for (uint256 i = 0; i < s.length; i++) {
if (s[i] == "\n") { end = i; break; }
}
bytes memory out = new bytes(end);
for (uint256 i = 0; i < end; i++) out[i] = s[i];
return string(out);
}
/// @dev strip query (from first '?') and any trailing '/'.
function _canonical(string memory url) internal pure returns (string memory) {
bytes memory b = bytes(url);
uint256 end = b.length;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == "?") { end = i; break; }
}
while (end > 0 && b[end - 1] == "/") end--;
bytes memory out = new bytes(end);
for (uint256 i = 0; i < end; i++) out[i] = b[i];
return string(out);
}- Step 4: Run test to verify it passes
Run: forge test --match-test test_claimId_ -vv
Expected: PASS (2 tests).
- Step 5: Commit
git add packages/contracts-evm/src/ClaimLauncher.sol packages/contracts-evm/test/ClaimLauncher.t.sol
git commit -m "feat(launcher): reason enum, generalized claim struct, claimId from (reason, source)"Files:
-
Modify:
packages/contracts-evm/src/ClaimLauncher.sol -
Test:
packages/contracts-evm/test/ClaimLauncher.t.sol -
Step 1: Replace the
_submit()test helper and write the submit test
Replace the existing _submit() helper (it used submitClaim) and add the test:
function _submit() internal returns (bytes32 claimId) {
string memory url = "https://x.com/jack/status/1934567890";
claimId = launcher.claimIdForClaim(0, url, "");
vm.prank(user);
launcher.submit{value: 1 ether}(0, url, "", "", "", "", 1 ether);
}
function test_submit_setsPending_andSends_andRefunds() public {
uint256 balBefore = user.balance;
bytes32 claimId = _submit();
(ClaimLauncher.Status status, address l, uint256 thr, uint8 rc, uint8 reason,,,,,) =
launcher.claims(claimId);
assertEq(uint8(status), uint8(ClaimLauncher.Status.Pending));
assertEq(l, user);
assertEq(thr, 1 ether);
assertEq(rc, 0);
assertEq(reason, 0);
assertEq(sender.sendCount(), 1);
assertEq(sender.lastTarget(), verifier);
assertEq(sender.lastValue(), 0.001 ether);
assertEq(user.balance, balBefore - 0.001 ether);
}
function test_submit_rejectsEmptyClaim() public {
vm.prank(user);
vm.expectRevert(ClaimLauncher.InvalidClaim.selector);
launcher.submit{value: 1 ether}(0, "", "", "", "", "", 1 ether);
}
function test_submit_rejectsDuplicateActive() public {
_submit();
vm.prank(user);
vm.expectRevert(ClaimLauncher.ClaimActiveOrDone.selector);
launcher.submit{value: 1 ether}(0, "https://x.com/jack/status/1934567890", "", "", "", "", 1 ether);
}
function test_submit_insufficientFeeReverts() public {
sender.setFee(2 ether);
vm.prank(user);
vm.expectRevert(ClaimLauncher.InsufficientFee.selector);
launcher.submit{value: 1 ether}(0, "https://x.com/jack/status/1934567890", "", "", "", "", 1 ether);
}- Step 2: Run to verify it fails
Run: forge test --match-test test_submit_ -vv
Expected: FAIL — submit / InvalidClaim not defined.
- Step 3: Implement
submit,retryClaim,_send, errors
Replace submitClaim, the old _send, and the InvalidUrl error. New code:
error InvalidClaim();
function submit(
uint8 reason,
string calldata claim,
string calldata sources,
string calldata nameOverride,
string calldata symbolOverride,
string calldata descOverride,
uint256 graduationThreshold
) external payable returns (bytes32 claimId) {
if (bytes(claim).length == 0) revert InvalidClaim();
claimId = keccak256(abi.encode(reason, _canonical(_primary(claim, sources))));
Claim storage c = claims[claimId];
if (c.status != Status.None && c.status != Status.Retryable) revert ClaimActiveOrDone();
c.status = Status.Pending;
c.launcher = msg.sender;
c.graduationThreshold = graduationThreshold;
c.reason = reason;
c.claim = claim;
c.sources = sources;
c.nameOverride = nameOverride;
c.symbolOverride = symbolOverride;
c.descOverride = descOverride;
_send(claimId, c);
emit ClaimSubmitted(claimId, msg.sender, c.retryCount);
}
function _send(bytes32 claimId, Claim storage c) internal {
bytes memory data = abi.encode(
claimId, c.reason, c.claim, c.sources, c.nameOverride, c.symbolOverride, c.descOverride,
c.launcher, c.graduationThreshold
);
(uint256 fee,) = bridgeSender.quoteSendToGenLayer(claimVerifier, data, lzOptions);
if (msg.value < fee) revert InsufficientFee();
bridgeSender.sendToGenLayer{value: fee}(claimVerifier, data, lzOptions);
uint256 refund = msg.value - fee;
if (refund > 0) {
(bool ok,) = payable(msg.sender).call{value: refund}("");
require(ok, "refund failed");
}
}Also remove the now-unused REASON constant and update _handleFailure to read c.reason for
the mint (see Task 3). retryClaim stays as-is (it calls _send).
- Step 4: Run to verify it passes
Run: forge test --match-test test_submit_ -vv
Expected: PASS (4 tests).
- Step 5: Commit
git add packages/contracts-evm/src/ClaimLauncher.sol packages/contracts-evm/test/ClaimLauncher.t.sol
git commit -m "feat(launcher): generalized submit + reason-tagged payload"Files:
-
Modify:
packages/contracts-evm/src/ClaimLauncher.sol -
Test:
packages/contracts-evm/test/ClaimLauncher.t.sol -
Step 1: Update the VERIFIED→mint test to assert the reason string
function test_verified_mintsToken_withReasonString() public {
bytes32 claimId = _submit(); // reason 0 = ViralPost
bytes memory v =
_verdict(claimId, 0, "Jack Signal", "JS", "A verified post about X.", "https://x.com/jack/status/1934567890");
vm.prank(bridgeReceiver);
launcher.processBridgeMessage(uint32(40305), verifier, v);
address token = factory.tokenForClaim(claimId);
assertTrue(token != address(0));
assertEq(TokenpostToken(payable(token)).reason(), "ViralPost");
(ClaimLauncher.Status status,,,,,,,,,) = launcher.claims(claimId);
assertEq(uint8(status), uint8(ClaimLauncher.Status.Launched));
}- Step 2: Run to verify it fails
Run: forge test --match-test test_verified_ -vv
Expected: FAIL (compile error — REASON removed, or wrong reason string).
- Step 3: Implement reason→string mapping in
processBridgeMessage
In processBridgeMessage, where it currently passes reason: REASON, replace with a lookup
from the stored claim's reason byte:
function _reasonString(uint8 r) internal pure returns (string memory) {
if (r == 0) return "ViralPost";
if (r == 1) return "RealWorldEvent";
if (r == 2) return "GitHubRelease";
if (r == 3) return "SportsResult";
if (r == 4) return "OnchainMetric";
if (r == 5) return "PriceTarget";
return "Unknown";
}And in the status == 0 branch of processBridgeMessage, set
reason: _reasonString(c.reason) in the CreateParams.
- Step 4: Run the full launcher suite
Run: forge test --match-contract ClaimLauncherTest -vv
Expected: PASS — update any remaining old cases (test_submitClaim_*, test_claimId_isKeccakOfTrailingDigits, test_claimId_rejectsNonNumericTail) by deleting them; they referenced the removed submitClaim/claimIdFor. The retained idempotency/gating tests (Task 7/8 block) compile unchanged because the struct tuple now has 10 members — fix their destructuring (Status,,,,,,,) → add the extra commas to match 10 fields.
- Step 5: Commit
git add packages/contracts-evm/src/ClaimLauncher.sol packages/contracts-evm/test/ClaimLauncher.t.sol
git commit -m "feat(launcher): mint with per-claim reason string; drop ViralPost hardcoding"- Step 1: Run the entire suite
Run: cd packages/contracts-evm && forge test -vv
Expected: PASS — all contracts (was 48 tests; count changes with rewritten launcher cases).
Fix any struct-tuple destructuring mismatches in ClaimLauncher.t.sol revealed here (every
launcher.claims(id) now returns 10 fields).
- Step 2: Commit if any test files changed
git add packages/contracts-evm/test
git commit -m "test(launcher): align all cases with 10-field Claim struct"Files:
-
Modify:
contracts-genlayer/ClaimVerifier.py -
Test:
contracts-genlayer/tests/direct/test_claim_verifier.py -
Step 1: Regenerate the request fixture with
cast
The old REQUEST_HEX encodes the tweet-shaped tuple. Generate a new one for the generalized
tuple (claimId, reason=0, claim=the URL, sources="", overrides "", launcher, grad):
Run:
cast abi-encode "f(bytes32,uint8,string,string,string,string,string,address,uint256)" \
0x$(cast keccak $(cast abi-encode "g(uint8,string)" 0 "https://x.com/jack/status/1934567890" | sed 's/^0x//')) \
0 "https://x.com/jack/status/1934567890" "" "" "" "" \
0x000000000000000000000000000000000000a11c 1000000000000000000Record the output as REQUEST_HEX (strip the leading 0x). Also recompute CLAIM_ID =
cast keccak $(cast abi-encode "g(uint8,string)" 0 "https://x.com/jack/status/1934567890").
- Step 2: Write the failing decode test (replace
test_decodes_request_layout)
def test_decodes_generalized_request(direct_vm, direct_deploy, direct_alice):
c = _deploy(direct_deploy)
direct_vm.sender = direct_alice
d = c.decode_request(REQUEST)
assert d["claim_id"].lower() == CLAIM_ID
assert d["reason"] == 0
assert d["claim"] == URL
assert d["sources"] == ""
assert d["launcher"].lower() == "0x000000000000000000000000000000000000a11c"
assert d["graduation_threshold"] == "1000000000000000000"- Step 3: Run to verify it fails
Run: .venv/bin/pytest contracts-genlayer/tests/direct/test_claim_verifier.py::test_decodes_generalized_request -v
Expected: FAIL — decode_request returns the old shape / reason KeyError.
- Step 4: Update the request type +
decode_request
In ClaimVerifier.py replace _REQUEST_T and decode_request:
_REQUEST_T = tuple[gl.evm.InplaceTuple, gl.evm.bytes32, u8, str, str, str, str, str, Address, u256]@gl.public.view
def decode_request(self, message: bytes) -> dict:
claim_id, reason, claim, sources, name_ovr, sym_ovr, desc_ovr, launcher, grad = \
gl.evm.decode(_REQUEST_T, message)
return {
"claim_id": "0x" + bytes(claim_id).hex(),
"reason": int(reason),
"claim": claim,
"sources": sources,
"name_override": name_ovr,
"symbol_override": sym_ovr,
"desc_override": desc_ovr,
"launcher": str(launcher),
"graduation_threshold": str(int(grad)),
}- Step 5: Run to verify it passes
Run: .venv/bin/pytest contracts-genlayer/tests/direct/test_claim_verifier.py::test_decodes_generalized_request -v
Expected: PASS.
- Step 6: Commit
git add contracts-genlayer/ClaimVerifier.py contracts-genlayer/tests/direct/test_claim_verifier.py
git commit -m "feat(verifier): decode generalized reason-tagged request"Files:
-
Modify:
contracts-genlayer/ClaimVerifier.py -
Test:
contracts-genlayer/tests/direct/test_claim_verifier.py -
Step 1: Write failing tests
def test_phase1_viralpost_dispatch_is_verified_stub(direct_vm, direct_deploy, direct_alice):
c = _deploy(direct_deploy) # phase2 False
direct_vm.sender = direct_alice
d = c.decode_verdict(c.preview_verdict(REQUEST))
assert d["status"] == 0 # VERIFIED stub
assert d["claim_ref"] == URL
def test_unsupported_reason_is_transient(direct_vm, direct_deploy, direct_alice):
c = _deploy(direct_deploy)
direct_vm.sender = direct_alice
# reason 2 (GitHubRelease) — not implemented in Phase 1
req = _request(reason=2, claim="org/repo v1.2.3", sources="https://github.com/org/repo/releases/tag/v1.2.3")
d = c.decode_verdict(c.preview_verdict(req))
assert d["status"] == 2 # FAILED_TRANSIENT (not-yet-implemented; never mints, never terminal)Add a _request helper at the top of the test file that builds the generalized payload via the
contract's own encoder so tests don't hand-encode:
def _request(reason, claim, sources="", name="", symbol="", desc="",
launcher=LAUNCHER, grad=10**18, claim_id=None):
# Encode through cast-compatible layout using the verifier's gl.evm at deploy time is overkill;
# build with eth_abi to match Solidity abi.encode.
from eth_abi import encode as _abi_encode
from eth_utils import keccak as _keccak
def _canon(u):
u = u.split("?")[0]
return u.rstrip("/")
primary = (sources.split("\n")[0] if sources else claim)
cid = claim_id or _keccak(_abi_encode(["uint8", "string"], [reason, _canon(primary)]))
data = _abi_encode(
["bytes32", "uint8", "string", "string", "string", "string", "string", "address", "uint256"],
[cid, reason, claim, sources, name, symbol, desc, launcher, grad],
)
return data(pip install eth-abi eth-utils into .venv if missing.)
- Step 2: Run to verify they fail
Run: .venv/bin/pytest contracts-genlayer/tests/direct/test_claim_verifier.py -k "dispatch or unsupported" -v
Expected: FAIL — dispatch not implemented.
- Step 3: Implement dispatch in
_build_verdict/_decide
Replace _build_verdict and _decide so they read the generalized fields and route on reason.
Keep the existing ViralPost logic (_run_verification, _build_token_identity, _describe) and
the Phase-1 stub, but drive them from claim (the post URL) instead of tweet_url. Constants:
REASON_VIRALPOST = 0def _build_verdict(self, message: bytes) -> bytes:
claim_id, reason, claim, sources, name_ovr, sym_ovr, desc_ovr, launcher, grad = \
gl.evm.decode(_REQUEST_T, message)
status, name, symbol, description = self._decide(
bytes(claim_id), int(reason), claim, sources, name_ovr, sym_ovr, desc_ovr
)
return gl.evm.encode(_VERDICT_T, (claim_id, status, name, symbol, description, claim))
def _decide(self, claim_id: bytes, reason: int, claim: str, sources: str,
name_ovr: str, sym_ovr: str, desc_ovr: str):
if reason == REASON_VIRALPOST:
return self._decide_viralpost(claim_id, claim, name_ovr, sym_ovr, desc_ovr)
# Reasons not implemented in this phase: reply TRANSIENT so the claim is retryable,
# never minting and never terminally rejected.
return (FAILED_TRANSIENT, "", "", "")Rename the old _decide body to _decide_viralpost(self, claim_id, claim, name_ovr, sym_ovr, desc_ovr)
and inside it use claim wherever it previously used tweet_url (the post URL is now claim).
Update the Phase-2 dedup guard formula to match the new on-chain claimId:
# claimId must equal keccak256(abi.encode(uint8 reason, canonical(primary)))
expected_cid = Keccak256(gl.evm.encode(tuple[gl.evm.InplaceTuple, u8, str],
(REASON_VIRALPOST, _canonical(claim))).__bytes__()).digest()
if claim_id != expected_cid:
return (REJECTED_EXTERNAL, "", "", "")Add a _canonical pure helper mirroring the Solidity one:
def _canonical(url: str) -> str:
return url.split("?")[0].rstrip("/")- Step 4: Run to verify they pass
Run: .venv/bin/pytest contracts-genlayer/tests/direct/test_claim_verifier.py -k "dispatch or unsupported" -v
Expected: PASS.
- Step 5: Update the Phase-2 ViralPost tests to the generalized payload
Rewrite test_phase2_* to build requests via _request(reason=0, claim=URL) and keep their
web/LLM injections. Re-point WRONG_CID/WITH_DESC through _request (e.g.
_request(reason=0, claim=URL, claim_id=bytes(32)) for the mismatch case).
Run: .venv/bin/pytest contracts-genlayer/tests/direct/test_claim_verifier.py -v
Expected: PASS (all).
- Step 6: Lint + commit
Run: ~/.local/bin/genvm-lint check contracts-genlayer/ClaimVerifier.py
Expected: no errors.
git add contracts-genlayer/ClaimVerifier.py contracts-genlayer/tests/direct/test_claim_verifier.py
git commit -m "feat(verifier): reason dispatch; ViralPost reads claim; unsupported→transient"Files:
-
Modify:
apps/web/src/hooks/useLaunchClaim.ts,apps/web/src/lib/launch.ts,apps/web/src/lib/contracts.ts -
Step 1: Write/adjust the unit test for arg mapping
In apps/web/src/lib/__tests__/ add launch.test.ts (or extend existing):
import { buildLaunchArgs } from "@/lib/launch";
it("maps viralpost wizard state to generalized args", () => {
const a = buildLaunchArgs({
reason: "ViralPost", claim: "https://x.com/jack/status/1934567890",
sources: "", name: "", symbol: "", description: "", graduationThresholdEth: "5",
});
expect(a.reason).toBe(0);
expect(a.claim).toBe("https://x.com/jack/status/1934567890");
expect(a.sources).toBe("");
});
it("joins multiple source URLs with newlines", () => {
const a = buildLaunchArgs({
reason: "RealWorldEvent", claim: "Base teased an airdrop",
sources: "https://docs.base.org/airdrop\n https://news.site/x ",
name: "", symbol: "", description: "", graduationThresholdEth: "5",
});
expect(a.reason).toBe(1);
expect(a.sources).toBe("https://docs.base.org/airdrop\nhttps://news.site/x");
});- Step 2: Run to verify it fails
Run: cd apps/web && pnpm vitest run src/lib/__tests__/launch.test.ts
Expected: FAIL — reason/claim/sources not on LaunchArgs.
- Step 3: Update
LaunchArgs,buildLaunchArgs,WizardState, the hook, and the ABI
launch.ts — add a reason map and new shape:
export const REASON_INDEX: Record<string, number> = {
ViralPost: 0, RealWorldEvent: 1, GitHubRelease: 2, SportsResult: 3, OnchainMetric: 4, PriceTarget: 5,
};
export interface WizardState {
reason: string;
claim: string; // for ViralPost: the post URL
sources: string; // newline- or line-separated URLs (textarea)
name: string; symbol: string; description: string; graduationThresholdEth: string;
}
export function buildLaunchArgs(s: WizardState): LaunchArgs {
const sources = s.sources.split("\n").map((l) => l.trim()).filter(Boolean).join("\n");
return {
reason: REASON_INDEX[s.reason] ?? 0,
claim: s.claim.trim(),
sources,
name: s.name.trim(),
symbol: normalizeSymbol(s.symbol),
description: s.description.trim(),
graduationThresholdEth: s.graduationThresholdEth,
};
}useLaunchClaim.ts — new LaunchArgs + call submit:
export interface LaunchArgs {
reason: number; claim: string; sources: string;
name?: string; symbol?: string; description?: string; graduationThresholdEth: string;
}
const submit = (a: LaunchArgs) =>
writeContractAsync({
address: CONTRACTS.claimLauncher,
abi: claimLauncherAbi,
functionName: "submit",
args: [a.reason, a.claim, a.sources, a.name ?? "", a.symbol ?? "", a.description ?? "", parseEther(a.graduationThresholdEth)],
value: LZ_FEE_BUFFER,
});In contracts.ts, update claimLauncherAbi to include the new submit(uint8,string,string,string,string,string,uint256) signature (and keep retryClaim, processBridgeMessage, claims, claimIdForClaim). The address is updated in Task 9.
- Step 4: Run to verify it passes
Run: pnpm vitest run src/lib/__tests__/launch.test.ts
Expected: PASS.
- Step 5: Update any callers / wizard components that pass the old
tweetUrlfield
Run: cd apps/web && pnpm tsc --noEmit
Expected: PASS — fix LaunchWizard.tsx and any component still referencing WizardState.tweetUrl
to use claim/sources (for the ViralPost flow, bind the post-URL input to claim).
- Step 6: Commit
git add apps/web/src
git commit -m "feat(web): call generalized submit(reason, claim, sources, ...)"These tasks mutate live Base Sepolia + GenLayer studionet state. Run them in order; do not batch (avoid the nonce race noted in prior sessions). Read the GenLayer CLI gotchas in
genlayer-cliskill before Task 8.
- Step 1: Deploy fresh launcher + factory (the deploy script constructor is unchanged)
Run (from repo root, env per the script header — TREASURY, ADAPTER, BASE_BRIDGE_SENDER, BASE_BRIDGE_RECEIVER, LZ_OPTIONS):
cd packages/contracts-evm
forge script script/DeployClaimLoop.s.sol --rpc-url "$BASE_SEPOLIA_RPC" --broadcastRecord the new TokenFactory and ClaimLauncher addresses from the console output.
- Step 2: Redeploy
ClaimVerifier.pyto studionet
Run:
genlayer network set studionet
genlayer deploy --contract contracts-genlayer/ClaimVerifier.py \
--args 0xce87655D60dCa6CA76183DEDc8582766e5DE4e57 \
0xcfBD25E80e075e7F34E79C7DaCbA2EAbD6Aca22f \
<NEW_CLAIM_LAUNCHER>Verify: genlayer receipt <tx> --stderr (clean) and genlayer schema <addr> (resolves).
Record the new verifier address. Do not pass --rpc (breaks tx polling for built-in nets).
- Step 3: Commit the new addresses
Update packages/contracts-evm/deployments/base-sepolia.json claimLoop block (factory,
launcher, verifier). Commit:
git add packages/contracts-evm/deployments/base-sepolia.json
git commit -m "chore(deploy): record generalized launcher/factory/verifier addresses"- Step 1: Point the launcher at the verifier
Run: cast send <NEW_CLAIM_LAUNCHER> "setClaimVerifier(address)" <NEW_VERIFIER> --rpc-url "$BASE_SEPOLIA_RPC" --private-key "$PK"
Verify: cast call <NEW_CLAIM_LAUNCHER> "claimVerifier()(address)" --rpc-url "$BASE_SEPOLIA_RPC" == new verifier.
- Step 2: Gate the new factory to the new launcher (deploy script already set MintMode+caller on the fresh factory in Task 8, so verify rather than re-set)
Verify: cast call <NEW_FACTORY> "authorizedCaller()(address)" --rpc-url "$BASE_SEPOLIA_RPC" == new launcher.
- Step 3: Confirm the GL verifier config points at the new launcher
The Task 8 deploy passed the launcher as a constructor arg; confirm:
genlayer call <NEW_VERIFIER> get_config (or schema inspection) shows claim_launcher ==
new launcher (lowercased). If not, genlayer write <NEW_VERIFIER> set_config <bridge_sender> <new_launcher>.
-
Step 4: Update the relayer env + web app
-
Supabase secret:
CLAIM_LAUNCHER = <NEW_CLAIM_LAUNCHER>(so the relayer filters GL outbox to the new launcher). Set via the Management API as in the prior session; redeploy the function. -
apps/web/src/lib/contracts.ts: setclaimLauncherandfactoryto the new addresses.
Commit web change:
git add apps/web/src/lib/contracts.ts
git commit -m "chore(web): repoint to generalized launcher + factory"- Step 1: Submit a real ViralPost claim against the new launcher
Use a fresh, public X post URL. Either drive tokenpost.vercel.app (after the web redeploy) or:
cast send <NEW_CLAIM_LAUNCHER> \
"submit(uint8,string,string,string,string,string,uint256)" \
0 "<POST_URL>" "" "" "" "" 1000000000000000000 \
--value 0.02ether --rpc-url "$BASE_SEPOLIA_RPC" --private-key "$PK"Record claimId = keccak256(abi.encode(uint8 0, canonical(POST_URL))).
- Step 2: Watch it through to mint
Poll cast call <NEW_CLAIM_LAUNCHER> "claims(bytes32)" <claimId> until status → Launched (2),
then cast call <NEW_FACTORY> "tokenForClaim(bytes32)(address)" <claimId>. Expected: a non-zero
token address within ~10–12 min (forward LZ hop dominates; relayer delivers the verdict directly).
- Step 3: Confirm the minted identity
cast call <token> "name()(string)", symbol(), description(), reason() — reason() must
read "ViralPost". This proves the generalized rails mint exactly as before.
- Step 4: Final commit / record
Append the proven claimId + token address to deployments/base-sepolia.json and commit:
git add packages/contracts-evm/deployments/base-sepolia.json
git commit -m "chore(deploy): record generalized-rails ViralPost mint proof"- Step 5: Push
git push- §5 generalized entry → Tasks 1–3 (submit/claimId/payload), Task 5–6 (verifier decode/dispatch). ✓
- §8 dedup
(reason, source)→ Task 1_canonical/_primary, Task 2 claimId, Task 6 guard. ✓ - Verdict payload unchanged / relayer untouched → confirmed in Key Decisions; Task 9 only sets the launcher filter env. ✓
- ViralPost migrated, still mints live → Part E. ✓
- Reason enum shared Solidity⇄Python → Task 1 (enum), Task 3 (
_reasonString), Task 6 (REASON_VIRALPOST), Task 7 (REASON_INDEX) — all use the same 0–5 order. ✓ - Unsupported reasons never mint / never hard-fail → Task 6 returns
FAILED_TRANSIENT. ✓
Out of scope (own later plans): the subjective claim-truth pipeline (gate A, independent search, comparative consensus), the search proxy (§9), and the objective family (§7). This plan deliberately ships only the rails + ViralPost migration so each piece is independently provable.