Market Lifecycle
Every market goes through five on-chain states. The transitions are deterministic, time-bounded, and each one is enforced by the PropMarketHook contract — no off-chain bookkeeping is trusted.
The state machine
Committed ──▶ Open ──▶ Closed ──▶ Revealed ──▶ Resolved
│
└──▶ Refunded (only if reveal fails / dispute)1. Committed
The agent calls commit(commitHash, params). The factory deploys a hook at a CREATE2 address derived from (commitHash, agent, nonce). The market exists on chain but accepts no stakes yet.
The commit is keccak256(question || templateParams || salt) — a binding pre-commitment to exactly which market this will become. The agent can't walk it back without revealing the salt.
2. Open
At openAt, the hook starts accepting stakes. The OVER and UNDER sides each track aggregate USDT0. Stakers transfer in gaslessly via the x402 facilitator (see Gasless Staking); the hook'sbeforeSwap path verifies the EIP-3009 authorization before crediting either side.
3. Closed
At closeAt, the hook stops accepting new stakes. The market is now frozen in size. This window — between Close and Reveal — is what makes commit–reveal honest: the agent commits the market before knowing how much money is on each side.
4. Revealed
The agent calls reveal(question, templateParams, salt). The contract recomputes the hash, checks it equals the original commit, and stores the now-public market parameters. If the hash doesn't match, the call reverts; the market enters a refund window instead.
5. Resolved
Once the outcome window has elapsed (e.g. the question covered the next 30 minutes), anyone can call resolve(). The contract reads the outcome from the configured oracle interface, decides winner = OVER or UNDER, and unlocks payouts. Winners claim pro-rata against the losing pool, minus a small protocol fee.
The on-chain commit struct
The factory stores the minimum needed to verify a future reveal. Everything else is reconstructable from the inputs to the hash:
// packages/contracts/src/interfaces/IPropMarketHook.sol (excerpt)
struct CommitParams {
bytes32 commitHash; // keccak256(question || templateParams || salt)
address agent; // persona wallet — part of the marketId derivation
uint64 openAt; // when staking starts (unix seconds)
uint64 closeAt; // when staking ends
uint64 resolveAfter; // earliest resolve() time
address settlement; // USDT0 on X Layer
}The reveal + resolve pair
Reveal is the only place the question text becomes public on chain. Resolve is the only place USDT0 actually moves between sides:
function reveal(
string calldata question,
bytes calldata templateParams,
bytes32 salt
) external {
bytes32 expected = keccak256(abi.encodePacked(question, templateParams, salt));
if (expected != commitHash) revert RevealHashMismatch();
// ... store question + params, emit Revealed event
}
function resolve() external afterResolveAt {
Outcome o = _readOutcome(question, templateParams);
winningSide = o == Outcome.Over ? Side.Over : Side.Under;
// ... unlock claim() for winners
}