Creating a Verifier Spec

A Verifier Spec defines the complete specification for a type of verification — "what this type of transaction needs to verify, how to construct signatures, how to verify them." Immutable after deployment; multiple Verifier instances share the same Spec to form a competitive market.

This document uses the XRepostVerifierSpec contract class as a reference implementation; its on-chain name() is X(Twitter) Repost Verifier Spec.


Relationship Between Spec and Verifier Instance

X(Twitter) Repost Verifier Spec (1)
  ├── XRepostVerifier A (Instance 1, operated by Team A)
  ├── XRepostVerifier B (Instance 2, operated by Team B)
  └── XRepostVerifier C (Instance 3, auto-operated by an Agent)
  • Spec defines the specification — Parameter format, signature structure, check() verification logic
  • Instances provide the service — Each independently operates off-chain services, sets own prices, competes for business
  • Deal Contract references SpecrequiredSpecs() returns the Spec address required for each verification slot in the deal flow; Traders use this to search for matching Verifier instances

Step 1: Implement Metadata

contract XRepostVerifierSpec is VerifierSpec {
    function name() external pure override returns (string memory) {
        return "X(Twitter) Repost Verifier Spec";
    }

    function version() external pure override returns (string memory) {
        return "1.0";
    }

    function description() external pure override returns (string memory) {
        return
            "Verifies whether reposter_address's bound X(Twitter) account has reposted a given tweet via native repost(retweet) OR quote tweet. "
            "EIP-712 signature check. Result type: 1=pass, -1=fail, 0=inconclusive. "
            "request_sign params: {tweet_id, reposter_address}. "
            "specParams: abi.encode(tweet_id, reposter_address).";
    }
}

Importance of description(): It's not just human-readable documentation — off-chain services need to decode on-chain parameters based on the specParams format declared in the description. It must be precise down to parameter types and order.

Note: The VerifierSpec base contract only defines three methods: name(), version(), description(). check() is not in the base contract because each Spec's signature parameters differ — you need to define the check() signature yourself. Deal Contracts cast the Spec address to your specific type when calling it.


Step 2: Define the EIP-712 Signature Structure

bytes32 public constant VERIFY_TYPEHASH = keccak256(
    "Verify(string tweetId,address reposterAddress,uint256 fee,uint256 deadline)"
);

This typehash defines the content structure for the Verifier's pricing signature. The Verifier signs according to this structure during request_sign, and the contract verifies the signature according to this structure during createDeal.

Design Considerations

The signature includes: business parameters (solidifying verification content), fee (solidifying the price), deadline (solidifying the validity period). It does not include dealIndex (the deal hasn't been created at signing time) or partyA/partyB (the Verifier only cares about verification content).

EIP-712 is used instead of plain signatures because structured signatures allow wallets to display semantic signing content rather than a hex string.


Step 3: Implement check()

function check(
    address verifierInstance,
    string calldata tweet_id,
    address reposter_address,
    uint256 fee,
    uint256 deadline,
    bytes calldata sig
) external view returns (address) {
    bytes32 structHash = keccak256(abi.encode(
        VERIFY_TYPEHASH,
        keccak256(bytes(tweet_id)),
        reposter_address,
        fee,
        deadline
    ));

    return _recoverEIP712Signer(verifierInstance, structHash, deadline, sig);
}

Key Design Points

  • Call timingcheck() is called by the Deal Contract during createDeal to verify the signature was indeed issued by the specified Verifier's signer
  • DOMAIN_SEPARATOR — Read from verifierInstance via _recoverEIP712Signer; each instance has an independent domain to prevent cross-instance signature replay
  • Return value — Returns the recovered signer address; the Deal Contract compares this against IVerifier(verifierInstance).signer() to verify authenticity
  • EIP-2 low-s value_recoverEIP712Signer enforces low-s value to prevent signature malleability attacks

Step 4: Define the specParams Format

specParams is the parameter package passed to the Verifier's off-chain service during the verification phase. For XRepost, specParams is identical to check()'s signature parameters:

check() signature parameters (at createDeal stage):

tweet_id, reposter_address

specParams (at verification stage):

abi.encode(tweet_id, reposter_address)

specParams matches the check() signature parameters (both are tweet_id + reposter_address) because XRepost's claimDone does not introduce new fields (unlike XQuote's quote_tweet_id). The verifier uses these to check whether the Twitter account bound to reposter_address has reposted the specified tweet.

Constructing specParams in the Deal Contract

// XRepostDealContract.verificationParams()
specParams = abi.encode(d.tweet_id, d.poster);

Key point: specParams is constructed and returned by the Deal Contract's verificationParams(). The Verifier's off-chain service reads and decodes it from on-chain. The entire chain passes through no off-chain intermediary.


Step 5 (Optional): Create a Sample Verifier Contract Instance and Corresponding Off-Chain Verification Service

The Spec author doesn't necessarily need to operate a Verifier themselves; anyone can deploy an instance based on an existing Spec (see Deployment & Operations). The instance contract is lightweight:

contract XRepostVerifier is VerifierBase {
    address public immutable SPEC;

    constructor(address specAddress) VerifierBase("XRepostVerifier", "1") {
        require(specAddress != address(0), "spec cannot be zero");
        SPEC = specAddress;
    }

    function description() external pure override(VerifierBase) returns (string memory) {
        return "Verify reposts on X(Twitter). Matches native repost(retweet) OR quote tweet by the reposter's bound X(Twitter) account. EIP-712 signed. Verifier signatures expire within 1 hour (3600s) of issuance.";
    }

    function spec() external view override(VerifierBase) returns (address) {
        return SPEC;
    }
}

Full VerifierBase API in Developer Reference.

The focus for Verifiers is the off-chain verification service implementation; see Deployment & Operations.


Reference Implementation