New here?Prefer pictures over prose?Read the visual guide →

X Payday reference

Contracts, fee math, API and operations behind xpayday.money. The same markdown is served as plain text at /llms.txt.

X Payday technical reference

New here? Start with the visual guide at /guide — diagrams first, short captions, every step ends in a call to action. This file is the full technical reference: come back to it when you need to verify a claim, an address or a fee.

This is the canonical technical description of X Payday: what it is, exactly how a dollar moves from a trade on a Pons V2 bonding curve to a payment in someone's X Money account, what is enforced on chain versus what depends on a human, and how to check every claim below yourself. It is written for a person auditing the product and for an AI agent reading the site and trying to describe it accurately. Every fact below names the file it came from. Where a value could change on chain (a fee, a threshold, an address), the file and the exact getter to re-read it live are both given, because a document goes stale and a chain read does not.

This document describes the code in this repository. It is not a marketing page and it makes no claims about usage, volume or traction: read the live API (GET /status, GET /vaults, GET /activity) and the chain for those. The one place the brand is hardcoded is packages/shared/src/brand.ts; every page and this file derive their copy from it.

Status as of this writing (2026-09-13): the contracts under contracts/src/ compile, the shared/API packages are complete, and the X Payday contracts are deployed to Robinhood Chain mainnet (chain 4663) and Sourcify exact-match verified. The addresses, roles and live configuration below are read from contracts/deployments/4663.json and the chain. A local deployment also exists on an anvil fork of chain 4663 (contracts/deployments/local.json).

WhatValue
Domainxpayday.money
X account@xpayday
Factory (HandleVaultFactory)0xbFd7945e9Dd1C13eD4B5cDAb133a0fCc4F0c3032 — chain 4663 mainnet, deployed at block 62414357 (contracts/deployments/4663.json)
Local factory (anvil fork, not public)0x8ac87219a0F5639BC01b470F87BA2b26356CB2B9 (contracts/deployments/local.json)

Two previous factory deployments are superseded and never used: 0x2B6DcfebA0053b6F1e554CFbb88ce0751D222eF6 and 0xbA62739B111Fe0253163Fcd9dE3d5B6A7C1988f6.

The brand values live only in packages/shared/src/brand.ts (BRAND.domain, BRAND.url, BRAND.handle). The deployable addresses are written by the deploy scripts to contracts/deployments/<chainId>.json (script/Deploy.s.sol) and contracts/deployments/local.json (script/DeployLocal.s.sol). If this table and a deployment record ever disagree, the record on disk wins and this table is stale.

What X Payday is

X Payday lets anyone launch a Pons V2 bonding-curve coin for an X handle. The coin's on-chain creator fee recipient is not a person's wallet and not X Payday's wallet: it is a per-coin FeeSplitter that receives every creator fee Pons credits and divides it, by basis points of trade volume pinned at launch, into the handle's on-chain pot (a per-xUserId HandleVault), the holders' pool (a per-coin trustless StakingRewards stream), and the platform treasury. The handle owner proves ownership by signing in with X once; an oracle-signed EIP-712 Attestation then lets anyone submit their CLAIM (pay any amount to any wallet), SET_PAYOUT (auto-pay future fees to a wallet after a 24-hour notice), LAUNCH_LOCK (freeze the handle against further launches), or CANCEL_DELIVERY. Anyone may also ask X Payday to deliver an unclaimed pot to the owner's X Money account for a 3% fee, but only after a 24-hour owner window, and the last mile is a human send with a public proof. A pot nobody touches for 180 days flows to that coin's holders' pool, never to the treasury. The primary payout path is the plain on-chain claim to any address the owner names — most simply the USDG deposit address of a Robinhood Crypto account on Robinhood Chain (CLAIM, 0% fee); the community delivery to X Money is the fallback for US handles without a Robinhood account. Every fee split is arithmetic on a base-unit balance: the contracts are the source of truth, and this document is a pointer to them.

Getting started without a wallet

X Payday does not require a crypto wallet. /start (apps/web/app/start/page.tsx) is a plain-language onboarding page with five short sections — what a pot is, how to get paid (a claim to a Robinhood USDG deposit address, one-click X Money delivery, or a wallet), how to buy a coin with USDG on Robinhood Chain, how to buy with Robinhood Wallet, and what X Payday never does — and links into /claim and /launch. Every primary call to action is Sign in with X first; the wallet is optional and is labelled Advanced wherever it appears.

Three paths work with no wallet at all:

  • Launch for any handle (sponsored). A signed-in X user can create a coin for any handle at POST /launch/sponsored. X Payday's keeper submits the on-chain transaction and pays the Pons launch fee; the user needs no wallet and no ETH. Costs, caps and limits are in "The end-to-end flow" and "Cost of sponsoring a launch".
  • Claim to a Robinhood USDG deposit address. A signed-in handle owner can claim the pot to the USDG deposit address of a Robinhood Crypto account on Robinhood Chain — no wallet, no bridge, no fee. Because a contract-origin deposit is unverified, the card offers a $1 test first; see "Claim to Robinhood (custodial) and Robinhood Wallet (self-custody)".
  • Deliver to X Money. The alternative for US handles without a Robinhood account is one-click delivery to X Money: a 3% fee, a 24-hour owner window, and a human last mile with a public proof. An owner whose X profile does not confirm the US (the eligibility hint is non_us or unknown) sees Claim to a wallet only, plus a three-step explainer on getting a Robinhood Chain wallet — no promise X Payday cannot keep.

A wallet stays available as the labelled Advanced option for launching yourself, buying the coin, claiming to an address you control, and setting auto-pay. A Robinhood Wallet (Robinhood's self-custody app) supports chain 4663 natively and over WalletConnect, so it can be that wallet.

The end-to-end flow

Every hop, named, in order. Contract function names and module paths are exact; amounts move only where the named function moves them.

  1. A handle is resolved to its numeric X user id (off chain). GET /handles/:handle (apps/api/src/handles/routes.ts, resolution in apps/api/src/handles/resolve.ts) resolves the lowercase handle to a numeric xUserId through the X API v2 when X_BEARER_TOKEN is set, otherwise through fxtwitter (apps/api/src/handles/fxtwitter.ts), with a 24-hour cache (HANDLE_CACHE_TTL_SECONDS). A vault is keyed by xUserId, never by the handle string, because a handle can be released and re-registered but a numeric id cannot.
  2. A launcher gets an oracle-signed launch ticket. GET /launch/challenge?address= returns an EIP-191 challenge (apps/api/src/signer/challenges.ts); the wallet signs it and POST /launch/ticket verifies the signature, resolves the handle, refuses locked handles (409 LAUNCH_LOCKED) or a mismatched caller-supplied xUserId, and returns an oracle-signed EIP-712 LaunchTicket bound to (handleHash, xUserId, launcher, deadline, oracleEpoch, nonce). The ticket's deadline is issued 10 minutes out; the factory additionally refuses a deadline more than MAX_TICKET_TTL (1 hour) past the consuming block.
  3. A vault is created and the coin launches. HandleVaultFactory.createVaultAndLaunch(handle, xUserId, LaunchParams, LaunchTicket, sig) (payable) calls the private _createVault, which: reverts Paused when paused, VaultExists if vaultOf[xUserId] != 0, HandleNotLowercase on any A-Z byte, HandleMismatch if keccak256(bytes(handle)) != t.handleHash; runs the same checks as verifyAndConsumeTicket (its private _verifyAndConsume) to validate the oracle signature and mark the nonce used; deploys Clones.cloneDeterministic(vaultImplementation, bytes32(xUserId)); calls HandleVault.initialize(factory, xUserId, handleHash, handle); registers the vault and emits VaultCreated. It then calls HandleVault.launchFor{value: msg.value}(p, launcher). The one ticket authorizes both the creation and the launch. (Alternatively createVault then HandleVault.launch(p, t, sig), where the public launch path itself consumes the caller's ticket via HandleVaultFactory.verifyAndConsumeTicket.)

LaunchParams (contracts/src/libs/Structs.sol) carries the coin's metadata and pinned economics: { string name; string symbol; string metadataURI; uint16 creatorTaxBps; bool reflections; string description; string website; string twitter; string telegram; string discord; string github; }. The six trailing strings are optional profile fields: description becomes the token description, website/twitter/telegram/discord map onto Pons Socials (github has no Pons slot, so the vault appends it to the description as a trailing "\nGitHub: <url>" line when non-empty, and the indexer peels it back off).

No-wallet alternative to hops 2–3: the sponsored launch. A signed-in X user (session cookie from X OAuth) can launch a coin for any handle without a wallet by POSTing { handle, name, symbol, metadataURI?, creatorTaxBps?, reflections? } to POST /launch/sponsored (apps/api/src/signer/sponsored.ts). The API resolves the handle, refuses a locked handle (409 LAUNCH_LOCKED) and a disabled or over-cap path (503 SPONSORED_DISABLED, 429 SPONSORED_LIMIT), then issues the same EIP-712 LaunchTicket struct with launcher set to the keeper address, and enqueues a relay job of kind sponsoredLaunch. The keeper submits HandleVaultFactory.createVaultAndLaunch — or HandleVault.launch when a vault for that handle already exists — from the keeper key with value = PonsV2LaunchFactory.launchFee(). The response is { jobId }; poll GET /relay/:jobId, whose confirmed result carries the vault, token and curve parsed from the receipt's Launched event. Defaults are creatorTaxBps = 200 (2%) and reflections = true. The launcher pays no fee and needs no ETH: X Payday's keeper pays the Pons launch fee (see "Cost of sponsoring a launch"). Limits: one sponsored launch per xUserId per 24 hours, plus the global SPONSORED_LAUNCHES_PER_DAY cap (default 50). A sponsored launch launches the coin; it does not buy any of it, because buying needs a wallet. Hop 3's on-chain checks are otherwise unchanged — only the launcher identity and who pays the fee differ.

  1. The vault deploys the coin's pools and pins the split. HandleVault._openPools (contracts/src/HandleVault.sol) validates the tax (TaxBelowFloor, TaxAboveMax against the live PonsV2LaunchFactory.maxCreatorTaxBps(), TaxNotOnGrid when reflections are on and tax % 50 != 0, TaxPinned/ReflectionsPinned if a previous launch pinned different values), clones a FeeSplitter and — when reflections are on — a StakingRewards, calls FeeSplitter.initialize(vault, staking, ponsFactory, ponsEscrow, pairToken, R, P, C), reads PonsV2LaunchFactory.launchFee(), and calls FeeSplitter.launch{value: fee}(params, 0, USDG).
  2. The splitter launches on Pons as the coin's creator fee recipient. FeeSplitter.launch (contracts/src/FeeSplitter.sol) forces TokenParams.creatorFeeRecipient = address(this) and buybackEnabled = false, then calls PonsV2LaunchFactory.launchToken{value: msg.value}(params, 0, USDG) and records the returned (token, curve). The splitter, not the vault, is Pons's originalDeployer, which is what makes the coin's escrow claimable: PonsV2FeeEscrow keys balances strictly to msg.sender (see contracts/research/PONS-NOTES.md). The vault refunds any msg.value above the exact Pons launchFee() to the launcher and emits Launched.
  3. Trades accrue fees on the curve, then in Pons's escrow. Curve trades build up quoteFeeBalance() (the base curve fee bucket) and creatorTaxBalance() (the creator tax, paid to the creator in full). HandleVault.claimFees() (permissionless) calls each coin's FeeSplitter.collect(), which first calls curve.sweepFees(0) (best-effort) to move those bucket balances into PonsV2FeeEscrow, credited to the splitter. FeeSplitter.collect() additionally calls the Pons meme hook sweepPoolFees(poolId, 0, 0) best-effort so post-graduation (Uniswap v4) creator fees keep flowing; memecoin-denominated post-graduation fees still require Pons's fee-sweep operator.
  4. FeeSplitter.collect() divides the inflow. It pulls the splitter's escrow balances with claimToken(pairToken) and claim() (the native leg) and forwards any native balance to the vault — all best-effort, so a curve/escrow revert cannot brick distribution. For an inflow x = pairToken.balanceOf(this): toPlatform = mulDiv(x, P, R), toHolders = mulDiv(x, C, R) (only when staking != 0 && C != 0), and toVault = x - toPlatform - toHolders (the remainder, so no dust is stranded). It transfers toPlatform to factory.treasury() read live, transfers toHolders to the staking pool via notifyRewardAmount, transfers toVault to the vault, and emits Collected(toVault, toHolders, toPlatform). HandleVault.claimFees sums those legs and emits FeesClaimed(toVault, toHolders, toPlatform).
  5. The owner acts through an oracle attestation. POST /claim/attest (session-scoped) verifies the signed-in xUserId owns the vault (vault.xUserId()), reads claimNonce and oracleEpoch live, and returns a signature over the vault's own EIP-712 domain (apps/api/src/signer/attestations.ts). HandleVault.execute(a, sig) checks the signature, handleHash, xUserId, epoch, nonce and deadline (≤ 1 hour out), increments claimNonce, and dispatches:
  • CLAIM_claim(to, amount): transfers amount (0 means the whole available()) to to, cancels any active delivery, emits Claimed(to, amount, nonce); reverts NothingToClaim on an empty pot.
  • SET_PAYOUT_setPayout(to): to == 0 clears immediately and emits PayoutAddressSet(0); otherwise sets pendingPayoutAddress = to, pendingPayoutActivatesAt = now + 24h, emits PayoutAddressProposed. Anyone later calls activatePayout() after the delay to make it live (PayoutAddressSet(to)).
  • LAUNCH_LOCK_setLaunchLock(amount): amount == 1 locks, 0 unlocks; emits LaunchLockSet(bool).
  • CANCEL_DELIVERY → cancels the active delivery; emits DeliveryCancelled.
  1. Auto-pay pushes future fees. With an activated payoutAddress, anyone calls HandleVault.payout() (permissionless) and the whole current available() is transferred to that address (PaidOut(to, amount)). The keeper does this automatically (apps/api/src/keeper/tick.ts). Calling payout() is a no-op when auto-pay is off or the pot is empty, and is never blocked by pause.
  2. Community delivery to X Money. Anyone calls HandleVault.requestDelivery() (permissionless; reverts PayoutAddressConfigured when auto-pay is set, DeliveryActive, NothingToDeliver on an empty pot, Paused). It snapshots feeBps (live deliveryFeeBps()), wallet (live deliveryWallet()) and eta = now + deliveryDelay() into the Delivery record and emits DeliveryRequested. The owner keeps the whole owner window (the delay) to claim first or cancel via CANCEL_DELIVERY; the guardian can also cancelDelivery(). After eta, HandleVault.executeDelivery() may be called by factory.deliveryOperator() only: it pays fee = amount * d.feeBps / 10000 to factory.treasury() (live) and the rest to the snapshotted d.wallet, then emits DeliveryExecuted(requestId, amount, fee).
  3. The human last mile, with public proof. The delivered USDG sits in deliveryWallet. A human sends the equivalent to the handle owner's X Money account from X Payday's own X account (@xpayday), then records the send in the ops console (POST /ops/deliveries/:id/sent { proofUrl, note }), which marks the delivery sent and inserts a delivery_sent activity event with the proof URL and note. There is no X Money API and no automated fiat rail in this build; this step is deliberately manual and is what the public proof attests to.
  4. The activity feed is event-sourced. apps/api/src/indexer/ decodes factory, vault, splitter and staking logs into the events table, and GET /activity serves EventDTO rows. A claimed row only ever comes from a Claimed event with its tx hash; a delivery_sent row only from the ops record. There is no fabricated history.

The whole chain, compressed to one line: trade on the Pons curve → curve fee buckets → FeeSplitter.collect() (sweepFees + escrow claim) → platform/holders/pot split → HandleVault pot → owner execute(CLAIM) to a wallet, or execute(SET_PAYOUT) + payout() auto-pay, or requestDelivery()executeDelivery()deliveryWallet → a human X Money send → a public proof.

The fee model, exactly

Every number is pinned per coin at its first launch except where noted, and is in basis points (1 bps = 0.01%) of trade volume. The formula below is the Solidity in contracts/src/HandleVault.sol (_openPools) and contracts/src/FeeSplitter.sol (collect), reproduced exactly by packages/shared/src/fees.ts (splitBps) for the UI. Integer division floors; the vault always gets the remainder.

Let T = creatorTaxBps (the launcher's chosen creator tax), FLOOR = 100 (the hardcoded floor constant in the vault), and PONS_CREATOR_BASE_BPS = 70 (the creator's share of Pons's 1% base fee). Then:

  • R = T + 70 — the total bps of volume the FeeSplitter receives as Pons's creator fee recipient.
  • P = FLOOR * platformShareOfFloorBps / 10000 — the platform's share, 0.15% (15 bps) at the default platformShareOfFloorBps = 1500. MAX_PLATFORM_SHARE_OF_FLOOR_BPS = 2000 caps the share at 20 bps (0.20%). P is computed from the hardcoded FLOOR constant, not the factory's live floorTaxBps().
  • C = 50 + (T - FLOOR) / 2 when reflections are on, 0 when off — the holders' share.
  • H = R - P - C — the handle's pot, the remainder of the splitter's inflow.

Constraints enforced on chain: T >= factory.floorTaxBps() (default 100) else TaxBelowFloor; T <= PonsV2LaunchFactory.maxCreatorTaxBps() else TaxAboveMax (the shared constant MAX_CREATOR_TAX_BPS is 1000, i.e. 10%, but the live Pons ceiling is re-read at launch); when reflections are on, T % 50 == 0 else TaxNotOnGrid. The first launch pins T and the reflections flag forever (TaxPinned / ReflectionsPinned on any later launch at a different value).

Split table (bps of volume; % in parentheses). Pons protocol keeps 0.30% (30 bps) of volume on top of R and never reaches the splitter, so a trader's total tax is T + 1.00%.

Creator tax TReflectionsR (splitter)P (platform)C (holders)H (pot)
1% (100)on1.70%0.15%0.50%1.05%
1% (100)off1.70%0.15%0%1.55%
2% (200)on2.70%0.15%1.00%1.55%
2% (200)off2.70%0.15%0%2.55%
5% (500)on5.70%0.15%2.50%3.05%
5% (500)off5.70%0.15%0%5.55%
10% (1000)on10.70%0.15%5.00%5.55%
10% (1000)off10.70%0.15%0%10.55%

Delivery fee

deliveryFeeBps defaults to 300 (3%) and is capped at MAX_DELIVERY_FEE_BPS = 500 (5%). It is owner-settable instantly (no timelock), but the fee is snapshotted per request, so a later change can never re-price a delivery already in flight: executeDelivery computes the fee from d.feeBps, not a live read. The fee is paid to factory.treasury(); the remainder goes to the snapshotted deliveryWallet. Read deliveryFeeBps() live before trusting the number.

Delivery delay

deliveryDelay defaults to 86 400 seconds (24 hours) — the owner window during which the handle owner can CLAIM (which cancels the request) or cancel before an operator can execute. It is owner-settable instantly, bounded by MIN_DELIVERY_DELAY = 3600 and MAX_DELIVERY_DELAY = 7 days, and snapshotted into the request's eta at request time. Read deliveryDelay() live.

Launch fee

The launcher pays Pons's exact live launchFee(): the vault reads it, requires msg.value >= launchFee() (InsufficientLaunchValue), forwards exactly that amount through FeeSplitter.launch to PonsV2LaunchFactory.launchToken, and refunds the excess to the launcher (EthTransferFailed if the refund call fails). X Payday takes no launch fee of its own.

Cost of sponsoring a launch

A wallet launch costs the launcher the Pons launchFee() only. A sponsored launch moves that cost to X Payday's keeper:

  • 0.0005 ETH per launch at today's Pons fee (launchFee() = 500000000000000 wei; the contract reads it live, so re-read it before quoting a number — research/pons.md).
  • The keeper must hold enough ETH to cover the launch fee and the gas of the createVaultAndLaunch transaction it submits. Fund the keeper well above the per-launch cost; ops/DEPLOY.md §0.4 recommends 0.05 ETH and §8 alerts when its balance drops below 0.01 ETH.
  • The global SPONSORED_LAUNCHES_PER_DAY cap (default 50) bounds the daily spend; at today's fee that is at most 0.025 ETH/day of launch fees, plus gas, across all users.
  • SPONSORED_LAUNCHES_PER_DAY and SPONSORED_LAUNCH_ENABLED are read by the API (apps/api/src/env.ts); GET /status reports the keeper address so its balance can be watched.

Reflections: trustless, but you must stake

For a reflections-on coin, C bps of every collect() inflow is transferred to that coin's StakingRewards clone (contracts/src/StakingRewards.sol) via notifyRewardAmount, which streams it to stakers as the pair token (USDG) over a 7-day window (rewardsDuration = 7 days). It is "trustless" in the specific sense that the maths is on chain and no keeper chooses who gets what: there is no allocation step, no Merkle root and no privileged distributor. Everyone who stakes shares the stream in proportion to weight = balance * tierMultiplier / 10 at 1e18 precision, and any leftover from an unfinished window is rolled into the next window rather than dropped.

Lock tiers (longer locks take a proportionally larger share of the same stream):

TierLockMultiplierWeight factor
0none1.0×amount * 10 / 10
130 days1.5×amount * 15 / 10
290 days2.0×amount * 20 / 10

stake(amount, tier) re-weights the whole position at the new tier and pushes unlockAt to the later of the existing lock and now + tierLockSeconds(tier); a lower tier while still locked reverts TierDowngrade. withdraw(amount) reverts Locked before unlockAt; getReward() pays accrued USDG; exit() does both. notifyRewardAmount is callable only by the coin's splitter or its owning vault (the two notifiers), and the contract refuses a schedule whose implied rate exceeds its own reward-token balance (RewardRateTooHigh).

The limits, stated plainly: reflections only reach stakers. A holder who simply holds the coin earns nothing from the stream; C still leaves the pot, but it accrues to the staking pool, and unclaimed rewards sit in the contract until a staker calls getReward()/exit(). If a reflections-on coin's staking pool is empty, the stream accrues to nobody in practice (the balance is held by the pool contract). Reflections are a per-launch toggle pinned forever at the first launch.

Auto-pay and the 24-hour activation delay

Auto-pay is a persistent payoutAddress on the vault, set through an owner SET_PAYOUT attestation (see hop 8). It is deliberately delayed: proposing a non-zero address sets pendingPayoutAddress and pendingPayoutActivatesAt = now + 24h, and only activatePayout() after that time makes it live. Anyone may call activatePayout(). Clearing auto-pay (to == 0) takes effect immediately. The delay exists so a single compromised signing moment cannot silently redirect the pot: the owner (and anyone watching) gets a 24-hour public window, with the proposal visible on chain as PayoutAddressProposed, to notice and cancel by sending SET_PAYOUT to zero. Once active, payout() is permissionless and pushes the entire pot to the address; the keeper calls it on every tick (apps/api/src/keeper/tick.ts). Setting a payout address also cancels any active delivery request, and vice versa: requestDelivery() reverts PayoutAddressConfigured while auto-pay is on.

Handle lock

The handle owner can lock the handle through a LAUNCH_LOCK attestation (amount == 1) and unlock it (amount == 0); the vault emits LaunchLockSet(bool) and any later launch() reverts LaunchLocked. The API also refuses to issue a launch ticket for a locked handle (409 LAUNCH_LOCKED, POST /launch/ticket). A locked handle can still claim, set auto-pay, be delivered to, or be swept as abandoned — the lock only stops new coins for that handle.

Delivery to X Money

The X Payday-native exit is the on-chain claim (CLAIM to any wallet, no fee). Delivery to X Money is the community option, and it comes with real caveats:

The owner's one-click path (/claim). For a signed-in handle owner the claim page offers Send my pot to X Money in one click. It calls the same permissionless POST /delivery/request { vault } (apps/api/src/keeper/routes.ts), which enqueues a requestDelivery relay job — nothing extra or privileged happens on chain. The copy at the button states plainly that X Payday keeps a 3% fee and that the send happens after a 24-hour wait, during which the owner can still claim the pot, and that X Payday's team completes the X Money send and records a public proof (the ops step below). The request is rate-limited to 5 per hour per IP. An owner whose eligibility hint is not us sees Claim to a wallet only, with an explainer on getting a wallet (the Robinhood app → a Robinhood Chain wallet); X Payday does not promise an X Money send it cannot make. Delivery is never required: claiming to a wallet is free.

For everyone else the same request is permissionless, with these caveats:

  1. Permissionless request. Anyone may call HandleVault.requestDelivery() while the pot is non-empty and auto-pay is off. The request snapshots feeBps, wallet and eta = now + deliveryDelay().
  2. Owner window. During the delay (default 24h) the handle owner can claim the pot with CLAIM, or cancel the delivery with CANCEL_DELIVERY (also cancels a request incidentally). The factory guardian can cancelDelivery() at any time as an incident brake. Pausing the protocol blocks new requestDelivery/executeDelivery but never a claim.
  3. Operator execution. After eta, only factory.deliveryOperator() — a gas-only key that can never choose a destination — may call HandleVault.executeDelivery(). It pays the snapshotted fee to the treasury and the remainder to the snapshotted deliveryWallet.
  4. Custody hop. The remaining USDG lands in deliveryWallet, an X Payday-controlled address, before continuing on to the recipient. That is a real, named custody hop, not a hidden one.
  5. The human last mile. A person sends the X Money peer-to-peer payment to the handle owner from X Payday's own X account. There is no X Money API and no automated fiat rail in this build; the send is manual by design.
  6. Public proof. The operator records the send in the ops console (POST /ops/deliveries/:id/sent), which marks the delivery sent and writes a delivery_sent activity event with the proof URL and note. Nothing is announced as delivered before it is marked sent.
  7. Eligibility hint. The web app may hide or disable the delivery button for handles whose X "account based in" panel says they are not in the United States, because X Money is a US product. This hint is unofficial, derived from fxtwitter's mirror of that panel when no X API bearer is configured (apps/api/src/handles/types.ts, eligibilityFromBasedIn); only an exact "United States" is us, any other value is non_us, and a missing value is unknown (never assumed non-US). The gate only affects whether the delivery button is offered; it never affects launching, claiming or reflections.
  8. Honest regulatory note. Converting and sending an unaffiliated handle's own accrued funds to them, for a fee, can be money transmission under US rules (31 CFR 1010.100(ff)(5)), and nothing here is legal clearance for that activity. X Payday's launch/claim legs are on-chain transfers to addresses the owner chose; the delivery leg is the one with the open question, and it is stated here rather than buried.

Claim to Robinhood (custodial) and Robinhood Wallet (self-custody)

Two Robinhood-related paths. Neither changes a contract or a fee rule: the payout is the same CLAIM attestation and auto-pay is the same SET_PAYOUT described under "The end-to-end flow". The only thing here is where the owner points them.

Claim to a Robinhood USDG deposit address (custodial)

The signed-in owner's /claim page (apps/web/app/claim/claim-panel.tsx) offers Get paid in your Robinhood app (the card is apps/web/components/claim/robinhood-claim.tsx): paste the USDG deposit address of a Robinhood Crypto account and send a claim. The vault then transfers to that address exactly as it would to any wallet.

  • No fee. A claim costs nothing from X Payday; the 3% fee belongs only to the X Money delivery service above. There is no bridge and no third-party payment app.
  • The exact amounts. CLAIM carries an amount in USDG base units (a decimal string); amount = 0 pays the whole available(). The card's Send $1 test button sends 1000000 base units = $1.00 (USDG has 6 decimals); Send my whole pot sends 0.
  • Auto-pay. The toggle Also turn on auto-pay to this address runs SET_PAYOUT to the same address after the test claim confirms, so future fees are pushed without another sign-in. It is the standard 24-hour activation notice (propose → activatePayout() after eta → permissionless payout()), nothing new on chain.
  • Official Robinhood facts (read the sources; they can change):
  • Every Robinhood Crypto account has a unique USDG deposit address. USDG deposits are supported on Robinhood Chain, and also on Ethereum and Solana — the sender must pick Robinhood Chain, and a deposit sent on the wrong network is not credited and can be lost.
  • A USDG deposit needs 30 confirmations (about 5 minutes) and is then subject to a compliance review that "can take a few hours". There is no minimum.
  • USDG deposits are not available to New York residents (USDG is not tradable in NY or TX).
  • Sources: <https://robinhood.com/us/en/support/articles/crypto-transfers/>, <https://robinhood.com/us/en/support/articles/robinhood-chain-mainnet/>.
  • UNVERIFIED: contract-origin deposits. The pot is a contract (HandleVault) sending USDG, not a personal wallet, and Robinhood's docs describe wallet deposits. Whether a deposit that originates from a contract is credited normally is not verified — treat it as unknown until tested. That is why the UI offers the $1 test first: send 1000000 base units, confirm it shows up in the Robinhood app, and only then claim the rest or turn on auto-pay. If the $1 test is not credited, do not send more; use a plain wallet or the X Money delivery above.
  • Not a partner. Robinhood is not a partner and X Payday is not affiliated with Robinhood. A claim is a normal on-chain transfer to an address the owner names; whether and when Robinhood credits it is Robinhood's decision and outside our control.

Robinhood Wallet (self-custody)

Robinhood Wallet is Robinhood's self-custody app. It supports Robinhood Chain (chain 4663) natively and connects to dapps with WalletConnect, so a buyer or launcher can use it without bridging:

  • apps/web (apps/web/app/launch/, apps/web/lib/contracts.ts, RainbowKit + wagmi) targets Robinhood Chain; connect Robinhood Wallet — natively or through WalletConnect — to /launch to launch a coin, or use it to buy on the Pons site (the launch factory and curve addresses are under "Links"/"Contracts and addresses").
  • Buying needs USDG on Robinhood Chain plus a little ETH for gas. The app never custodies the wallet or its keys.
  • Source: <https://robinhood.com/us/en/support/articles/connect-to-dapps/>.

Where it sits relative to X Money delivery

A Robinhood claim is the primary no-bridge payout: CLAIM to a custodial deposit address, 0% fee, and no delay — a claim is immediate, and claiming also cancels any pending delivery. The X Money delivery above remains the fallback for US handles without a Robinhood account, and it is the path that charges 3% and waits 24 hours. Both end at an address the owner names; a Robinhood deposit address is simply another to in the same CLAIM.

Abandoned pots go to holders, never to the treasury

HandleVault.sweepAbandoned() is permissionless. It requires 180 days (ABANDONMENT_PERIOD) since lastActivity, no payoutAddress, no active delivery, a non-zero available(), and at least one coin with a holders' pool (staking != 0). It transfers the entire pot to the most recently launched coin's StakingRewards pool via notifyRewardAmount, and emits AbandonedSwept(staking, amount). There is no function anywhere in the vault that pays an abandoned pot to the treasury or to any admin-chosen address; NotAbandoned / NoHoldersPool guard the path. lastActivity is bumped on initialize, launch, claimFees (when the pot grew), execute, payout and requestDelivery.

Two consequences worth stating: a pot whose coins were all launched with reflections off has no staking pool, so sweepAbandoned() reverts NoHoldersPool and the funds simply stay in the vault (they are not burnable and not sweepable to the treasury); and any activity at all — a claim, a fee collection, a delivery request — resets the 180-day clock.

Trust model

The oracle is the single trust root. An oracle-signed EIP-712 Attestation is the only on-chain authorization for an owner action, and CLAIM pays the whole pot to the to address the attestation names, immediately. The oracle also signs LaunchTickets. There is no owner-only key on a vault and no contest window between signature and payout. A compromised oracle key, for as long as it went unrevoked, could sign a CLAIM to an attacker's address for any launched pot with a balance, sign SET_PAYOUT to redirect future fees, sign tickets to create vaults for arbitrary handles, and lock handles. That is the honest residual risk. In this build the oracle key is read from ORACLE_PRIVATE_KEY in the API process (apps/api/src/lib/clients.ts), so production isolation of that key (a separate signer service, an HSM or a policy-constrained worker) is a deployment requirement, not an implementation detail. The mitigations that exist on chain are: oracleEpoch (every signature pins the epoch, so a guardian revokeOracle() invalidates all of them at once), short deadlines (tickets ≤ 1h, attestations ≤ 1h, issued 10/30 minutes out by the API), single-use nonces (claimNonce, ticketUsed), and the 24-hour payout-activation delay.

The guardian can stop things but cannot move funds. revokeOracle() zeroes the oracle and increments oracleEpoch in one transaction (instant, deliberately asymmetric against the 7-day-timelocked appointment of a replacement). setPaused(true) blocks vault creation, launches, execute, requestDelivery and executeDelivery; it never blocks collect, claimFees, payout to an already-set address, or staking withdrawals/rewards. The guardian may also cancel any active delivery. None of these can send a pot anywhere.

Timelocks. Changes to the treasury, delivery wallet, floor tax, platform share and oracle sit behind a 7-day TIMELOCK (propose → execute after eta → optional cancel), each with its own proposeX/executeX/cancelX/pendingX/pendingXEta getters. Published on chain in advance, they affect only vaults that have not yet pinned their split. The instant, owner-only settings are setDeliveryFeeBps, setDeliveryDelay, setDeliveryOperator and setGuardian; delivery fee and delay are safe to change instantly because requests snapshot them.

Six distinct roles, enforced at deploy. HandleVaultFactory's constructor takes a FactoryConfig { owner, guardian, oracle, treasury, deliveryWallet, deliveryOperator, ponsFactory, ponsEscrow, pairToken } and reverts RolesNotDistinct unless the first six addresses are pairwise distinct and non-zero. A single key holding several roles defeats the separation. The six live roles (listed under "Contracts and addresses") are distinct, but owner is a single EOA today — the deployer key 0x0679626184519f7fCfbC12e8aA49f83219f7C176 — so a Safe multisig for owner (and ideally distinct keys for guardian/oracle/treasury/deliveryWallet/deliveryOperator) is recommended, and rotating the owner to a Safe is planned. Safe is deployed on Robinhood Chain: Safe v1.4.1 singleton 0x41675C099F32341bf84BFc5382aF534df5C7461a, SafeL2 v1.4.1 singleton 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762, SafeProxyFactory v1.4.1 0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC (research/chain-and-data.md §4; byte-for-byte identical to Ethereum mainnet). A hosted Safe Transaction Service for chain 4663 is not confirmed, so plan on a custom RPC.

What the delivery operator can and cannot do. executeDelivery() is callable only by factory.deliveryOperator(). It can move a requested vault's balance only to the wallet and fee snapshotted at request time, and only after eta; it cannot choose a destination, cannot execute without a prior request, and cannot pay the treasury anything other than the snapshotted fee.

The launcher and the owner are not privileged. Any wallet can launch once it holds a valid ticket; the ticket only proves the oracle authorized that (handle, xUserId, launcher). The vault's owner identity is not stored on chain; it is proven fresh at each execute by the oracle attestation, which the API only signs for a session that passed X OAuth.

Contracts and addresses

ContractSourceAddress
HandleVaultFactorycontracts/src/HandleVaultFactory.sol0xbFd7945e9Dd1C13eD4B5cDAb133a0fCc4F0c3032 (deploy block 62414357)
HandleVault implementationcontracts/src/HandleVault.sol0x46315720Ffb2c60233768161ca01A998A807aa83
FeeSplitter implementationcontracts/src/FeeSplitter.sol0xAc095dc1eE5Ba2cd80a9A58E95ca39614B49a645
StakingRewards implementationcontracts/src/StakingRewards.sol0x8Ba654f8545bF3786e13d6a628bd227b08Afd649
Local HandleVaultFactory (anvil fork, not public)contracts/deployments/local.json0x8ac87219a0F5639BC01b470F87BA2b26356CB2B9
USDG (pair token)packages/shared/src/chain.ts0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
Pons V2 launch factorypackages/shared/src/chain.ts0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e
Pons V2 fee escrowpackages/shared/src/chain.ts0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e
Pons V2 meme hookpackages/shared/src/chain.ts0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044

The table above is the live deployment: factory, implementations and configuration are on chain 4663 today, Sourcify exact-match verified, and match contracts/deployments/4663.json. Live reads: floorTaxBps() = 100, platformShareOfFloorBps() = 1500, deliveryFeeBps() = 300, deliveryDelay() = 86400, oracleEpoch() = 1; the pair token is USDG and ponsFactory() is the Pons V2 launch factory (0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e). The six roles are owner() 0x0679626184519f7fCfbC12e8aA49f83219f7C176 (also the deployer), guardian() 0xcfc7bd4Dde83bCE1C8840e3cAa3351F012DaF01C, oracle() 0x914e42FaD9fDC780258b3b91803d97A6DB53154d, treasury() 0x5122C7a24a2e89273050e35546A74B885945fBBE, deliveryWallet() 0x50F85CCA7e962a3057bba6157dB042E69A7BD201 and deliveryOperator() 0xE79C02c3Eba0623bd7922a2ea64d99ca7b7D2235 (the keeper). Honest note: owner is a single EOA today, so the role separation described under "Trust model" is not yet complete; rotating it to a Safe multisig is planned.

The deploy scripts write exactly which addresses are live:

  • Production: contracts/script/Deploy.s.sol reads OWNER, GUARDIAN, ORACLE, TREASURY, DELIVERY_WALLET, DELIVERY_OPERATOR from the environment (never from the broadcaster), deploys the three implementations and the factory, writes contracts/deployments/<chainId>.json { chainId, factory, vaultImplementation, splitterImplementation, stakingImplementation, deployBlock, txHashes }, and prints Sourcify verify commands. Runbook: ops/DEPLOY.md.
  • Local: contracts/script/DeployLocal.s.sol uses anvil accounts #0–#5 as the six roles and writes contracts/deployments/local.json plus contracts/deployments/local.env (FACTORY_ADDRESS, FACTORY_DEPLOY_BLOCK, ORACLE_PRIVATE_KEY = anvil #2, KEEPER_PRIVATE_KEY = anvil #5, DELIVERY_OPERATOR = anvil #5).

Reading the live configuration

Every number below is a getter on the factory. With F set to the deployed factory address and RPC to an RPC endpoint:

cast call $F "floorTaxBps()(uint16)"            --rpc-url $RPC   # 100 (1%)
cast call $F "platformShareOfFloorBps()(uint16)" --rpc-url $RPC   # 1500 -> P = 15 bps
cast call $F "deliveryFeeBps()(uint16)"         --rpc-url $RPC   # 300 (3%), cap 500
cast call $F "deliveryDelay()(uint32)"          --rpc-url $RPC   # 86400 (24h)
cast call $F "owner()(address)"                 --rpc-url $RPC   # 0x0679… (EOA today; Safe rotation planned)
cast call $F "oracle()(address)"                --rpc-url $RPC   # 0x914e…
cast call $F "oracleEpoch()(uint64)"            --rpc-url $RPC   # 1
cast call $F "guardian()(address)"              --rpc-url $RPC   # 0xcfc7…
cast call $F "treasury()(address)"              --rpc-url $RPC   # 0x5122…
cast call $F "deliveryWallet()(address)"        --rpc-url $RPC   # 0x50F8…
cast call $F "deliveryOperator()(address)"      --rpc-url $RPC   # 0xE79C… (keeper)
cast call $F "pairToken()(address)"             --rpc-url $RPC   # USDG 0x5fc5…
cast call $F "ponsFactory()(address)"           --rpc-url $RPC   # 0x7eD5…
cast call $F "paused()(bool)"                   --rpc-url $RPC
cat contracts/deployments/4663.json             # the authoritative address record

Public API

@pot/api (apps/api/) is a Bun + Hono service on port 8787, with a bun:sqlite store filled by the indexer. All responses are JSON; errors are { error: { code, message } }. CORS allows WEB_ORIGIN only. USDG amounts are decimal strings of base units (6 decimals); *_usd/_usd fields are plain numbers.

Public

EndpointResponse
GET /statusStatusDTO: { ok, chainId, factory, indexedBlock, headBlock, lagBlocks, counts: { vaults, coins, claims, deliveries }, totals: { potsUsdg, feesToPotsUsdg, feesToHoldersUsdg, claimedUsdg, paidOutUsdg, deliveredUsdg }, keeper: { enabled, address, lastRunAt }, deliveryFeeBps, deliveryDelay, platformBps }. deliveryFeeBps/deliveryDelay are read live from the factory (falling back to 300/86400 with no factory).
GET /vaults?limit&cursor{ items: VaultDTO[], nextCursor }
GET /vaults/:addressVaultDTO & { coins: CoinDTO[], recent: EventDTO[] }; 400 INVALID_ADDRESS, 404 VAULT_NOT_FOUND
`GET /pots?sort=balance\fees\volume&limit`{ items: PotDTO[] }; bad sort → 400 INVALID_SORT
`GET /coins?sort=new\volume\mcap&limit&cursor`{ items: CoinDTO[], nextCursor }
GET /activity?cursor&kind&handle&limit{ items: EventDTO[], nextCursor }; unknown kind → 400 INVALID_KIND
GET /deliveries?status&vault&limit{ items: DeliveryDTO[] }; status ∈ `requested\executed\cancelled\sent`
GET /prices/:tokenPriceDTO; 404 PRICE_NOT_FOUND
GET /h/:handle/summary`HandleSummaryDTO = { handle: HandleDTO, vault: VaultDTO\null, coins: CoinDTO[], activity: EventDTO[], delivery: DeliveryDTO\null }; 404HANDLE_NOT_FOUND`
GET /handles/:handleHandleDTO; 404 HANDLE_NOT_FOUND

VaultDTO = { address, xUserId, handle, handleHash, createdBlock, createdTx, creator, payoutAddress, launchLocked, lastActivity, balance, pendingFees, coinCount? }. CoinDTO = { token, vault, coinIndex, curve, splitter, staking, creatorTaxBps, reflections, name, symbol, launcher, launchedBlock, launchedTx, launchedAt, priceUsd, mcapUsd, volume24h, handle?, ponsUrl?, dexUrl? }. EventDTO = { id, kind, ts, block, tx, txUrl, handle, vault, token, symbol, actor, amount, amount2, amount3, data }; kindvault_created, launched, fees_claimed, claimed, payout_proposed, payout_set, paid_out, launch_lock, delivery_requested, delivery_executed, delivery_cancelled, delivery_sent, abandoned_swept, staked, withdrawn, reward_paid, oracle_set, oracle_revoked. PotDTO = { rank, handle, xUserId, vault, balance, feesTotal, volume24h, coinCount, claimedTotal, heat } with heatinferno|fire|hot|warm|cold. DeliveryDTO = { id, vault, requestId, requester, eta, feeBps, wallet, status, amount, fee, executedTx, sentProofUrl, sentNote, sentAt, handle? }. HandleDTO = { handle, xUserId, name, avatarUrl, verified, followers, eligibility, vault, predictedVault, launchLocked, source } with sourcex_api|fxtwitter|cache|dev. (The full TS definitions live in packages/shared/src/types.ts.)

Auth (X OAuth 2.0 PKCE), apps/api/src/auth/routes.ts

EndpointResponse
GET /auth/x/login?return_to=/path302 to X authorize (PKCE S256; state in a short-lived signed cookie)
GET /auth/x/callbackExchanges the code, reads users/me, sets the HMAC-signed sid cookie, 302 to WEB_ORIGIN + return_to
GET /auth/me`{ user: { xUserId, handle, name, avatarUrl } \null }`
POST /auth/logoutClears the session
GET /auth/dev/login?handle=&xUserId=&return_to=Only when DEV_FAKE_X=1; signs in without X (local/e2e)

Signer and owner actions

EndpointAuthResponse
GET /launch/challenge?address=none{ message, nonce, expiresAt } (EIP-191 text: domain, address, nonce, expiry); 400 INVALID_ADDRESS
POST /launch/ticket { handle, address, nonce, signature, xUserId? }none{ ticket: { handleHash, xUserId, launcher, deadline, oracleEpoch, nonce }, signature, handle, xUserId, vault, predictedVault, launchLocked }. Errors: CHALLENGE_NOT_FOUND/CHALLENGE_USED/CHALLENGE_EXPIRED, ADDRESS_MISMATCH, INVALID_SIGNATURE, HANDLE_NOT_FOUND, X_USER_ID_MISMATCH, 409 LAUNCH_LOCKED, FACTORY_UNAVAILABLE, ORACLE_UNAVAILABLE
POST /launch/sponsored { handle, name, symbol, metadataURI?, creatorTaxBps?, reflections? }session (X)202 { jobId } — the wallet-free launch (apps/api/src/signer/sponsored.ts). Issues the same LaunchTicket with launcher = the keeper address and enqueues a sponsoredLaunch relay job; the keeper pays the Pons launchFee() and submits createVaultAndLaunch (no vault yet) or HandleVault.launch (existing vault). Defaults creatorTaxBps=200, reflections=true; when reflections are on, creatorTaxBps must be a multiple of 50. Caps: 1 per xUserId/24h and SPONSORED_LAUNCHES_PER_DAY globally; SPONSORED_LAUNCH_ENABLED=0 closes the path. Errors: 401 UNAUTHORIZED, 400 INVALID_BODY/INVALID_HANDLE, 404 HANDLE_NOT_FOUND, 409 LAUNCH_LOCKED, 429 SPONSORED_LIMIT, 503 SPONSORED_DISABLED/QUEUE_FULL/KEEPER_UNAVAILABLE/ORACLE_UNAVAILABLE/FACTORY_UNAVAILABLE/CHAIN_UNAVAILABLE
POST /claim/attest `{ vault, action: 'CLAIM'\'SET_PAYOUT'\'LAUNCH_LOCK'\'CANCEL_DELIVERY', to?, amount? }`session (X){ vault, attestation: { handleHash, xUserId, to, action, amount, deadline, nonce, oracleEpoch }, signature }. Verifies the session's xUserId owns the vault (else 403 NOT_VAULT_OWNER); CLAIM/SET_PAYOUT require to; LAUNCH_LOCK requires amount "0" or "1"; deadline 30 min out; claimNonce/oracleEpoch read live
GET /me/potssession (X)MyPotsDTO = { vault, predictedVault, coins, deliveries, claimable }; 401 UNAUTHORIZED without a session

Relay, delivery and keeper

EndpointAuthResponse
POST /claim/submit { vault, attestation, signature }none202 { jobId } (enqueues an execute relay job the keeper submits)
GET /relay/:jobIdnone{ jobId, status, tx, error, result }; status ∈ `queued\sent\confirmed\failed.resultisnullexcept for a confirmedsponsoredLaunch, when it is{ vault, token, curve, tx }parsed from the receipt'sLaunched` event
POST /delivery/request { vault }none, rate-limited 5/hour per IP202 { jobId }; 429 RATE_LIMITED; 404 VAULT_NOT_FOUND
GET /ops/deliveriesX-Ops-Token{ items: DeliveryDTO[] } — executed but not yet sent
POST /ops/deliveries/:id/sent { proofUrl, note? }X-Ops-TokenThe updated DeliveryDTO; marks sent and writes a delivery_sent event
GET /ops/relayX-Ops-Token{ items: [{ id, kind, payload, status, tx, error, createdAt, updatedAt }] }
POST /ops/keeper/runX-Ops-Token{ ok: true, ...tickResult } — runs one keeper tick now

Background loops (apps/api/src/index.ts)

  • Indexer (src/indexer/): polls getLogs for the factory, known vaults, splitters and staking pools from FACTORY_DEPLOY_BLOCK in ≤2000-block chunks every 5 s, decodes with the shared ABIs, upserts the tables and keeps the cursor in cursors('indexer'). After each chunk it refreshes vaults.balance (available()), pending_fees (pendingFees()) and payout_address.
  • Keeper (src/keeper/): every 30 s: submit queued relay jobs (execute / requestDelivery / sponsoredLaunch); call claimFees() on vaults with at least 1 USDG of pendingFees; call activatePayout() when due; call payout() where payout_address != 0 && balance > 0; call executeDelivery() for due deliveries (only when the keeper address equals deliveryOperator). It never throws out of a tick. A sponsoredLaunch job is drained by sponsoredLaunch in src/keeper/chain.ts, which reads the live Pons launchFee() and sends it as value; the confirmed job's result carries the new vault/token/curve.
  • Prices (src/prices/): Dexscreener per token every 60 s, chain slug robinhood.

Known limitations

  • The X Money last mile is manual. There is no X Money API and no automated fiat rail in this build; a human sends the payment from @xpayday and records a proof. Everything up to deliveryWallet is automated; everything after it is a human tap.
  • Handle resolution falls back to fxtwitter. With no X_BEARER_TOKEN, profiles and the numeric xUserId come from fxtwitter, an unofficial mirror of X. A resolve failure means no ticket can be issued for that handle.
  • The eligibility hint is unofficial. eligibility (us/non_us/unknown) is derived from X's "account based in" panel via fxtwitter; it gates only whether the delivery button is offered, never the launch or claim paths, and it can be wrong.
  • Sponsored launches spend X Payday's ETH, not the user's. Each POST /launch/sponsored costs the keeper the live Pons launchFee() (0.0005 ETH at today's fee) plus gas, so the path is capped (one per xUserId per 24 h; SPONSORED_LAUNCHES_PER_DAY default 50) and can be closed with SPONSORED_LAUNCH_ENABLED=0. Over a cap the API answers 429 SPONSORED_LIMIT; if the keeper is unfunded the relay job fails with the send error. Fund the keeper and alert below 0.01 ETH (ops/DEPLOY.md §8).
  • The wallet-free paths do not cover everything. Launching a coin for a handle, claiming to a Robinhood USDG deposit address, and requesting X Money delivery work without a wallet; buying the coin, claiming to a self-custody address you control, and setting auto-pay still need a wallet (labelled Advanced) — Robinhood Wallet supports chain 4663 natively. A sponsored launch only launches — it does not buy any of the coin.
  • A Robinhood deposit is Robinhood's credit decision, and contract-origin deposits are unverified. Claiming to a Robinhood USDG deposit address is a normal on-chain transfer, but whether Robinhood credits a deposit that originates from a contract is not verified, which is why the UI sends a $1 test first. Deposits are not available to NY residents, need 30 confirmations plus a compliance review, and must be sent on Robinhood Chain. Robinhood is not a partner and X Payday is not affiliated with it; its docs can change. See "Claim to Robinhood (custodial) and Robinhood Wallet (self-custody)".
  • A delivery can be requested by anyone. POST /delivery/request is permissionless (rate-limited 5/hour/IP); the /claim button is a convenience, not a new on-chain privilege. The 24-hour owner window (claim cancels the request) and the guardian's cancelDelivery() are the protections, as described under "Delivery to X Money".
  • Reflections require staking. Holders who do not stake earn nothing from the holders' stream; C still leaves the pot into the pool contract.
  • Oracle centralization. The oracle key is the single trust root (see "Trust model"). The code reads it from ORACLE_PRIVATE_KEY in the API process today; isolating it (separate signer, HSM or policy-constrained worker) is required before production.
  • Delivery is a real custody hop and an open regulatory question. Funds pass through deliveryWallet, and money-transmission rules may apply to the delivery service. This is not legal clearance.
  • Delivery fee and delay are instantly owner-settable. They are bounded on chain (MAX_DELIVERY_FEE_BPS = 500, MIN/MAX_DELIVERY_DELAY) and snapshotted per request, but read them live.
  • Abandoned pots with no reflections-on coin cannot be swept. sweepAbandoned() reverts NoHoldersPool when every coin has reflections off, so such funds stay in the vault.
  • No factory admin UI. Owner/guardian actions are transactions (and cast commands); the repo ships no privileged dashboard. There is no platform token in this build.

How to verify each claim

Every command below reads public chain state or a public endpoint; none requires a secret. Set F to the factory from contracts/deployments/4663.json and RPC to an RPC endpoint first:

F=$(jq -r .factory contracts/deployments/4663.json)
RPC=https://rpc.mainnet.chain.robinhood.com   # public; or your own provider
API=http://localhost:8787                     # a running @pot/api
# 1. The factory's pinned configuration and roles (see "Reading the live configuration").
cast call $F "floorTaxBps()(uint16)"             --rpc-url $RPC   # 100
cast call $F "platformShareOfFloorBps()(uint16)" --rpc-url $RPC   # 1500
cast call $F "deliveryFeeBps()(uint16)"          --rpc-url $RPC   # 300
cast call $F "deliveryDelay()(uint32)"           --rpc-url $RPC   # 86400
cast call $F "oracle()(address)"                 --rpc-url $RPC
cast call $F "guardian()(address)"               --rpc-url $RPC
cast call $F "treasury()(address)"               --rpc-url $RPC
cast call $F "deliveryWallet()(address)"         --rpc-url $RPC
cast call $F "deliveryOperator()(address)"       --rpc-url $RPC

# 2. A vault is keyed by xUserId and its address is predictable before it exists.
cast call $F "vaultOf(uint256)(address)"    <X_USER_ID> --rpc-url $RPC
cast call $F "predictVault(uint256)(address)" <X_USER_ID> --rpc-url $RPC
cast call $F "vaultCount()(uint256)"        --rpc-url $RPC

# 3. A vault's real state: the pot, what claimFees would move, the pinned split, auto-pay,
#    the lock, the last activity and the current delivery.
V=<vault address>
cast call $V "available()(uint256)"             --rpc-url $RPC
cast call $V "pendingFees()(uint256)"           --rpc-url $RPC
cast call $V "pinnedCreatorTaxBps()(uint16)"    --rpc-url $RPC
cast call $V "pinnedReflections()(bool)"        --rpc-url $RPC
cast call $V "launchLocked()(bool)"             --rpc-url $RPC
cast call $V "payoutAddress()(address)"         --rpc-url $RPC
cast call $V "pendingPayoutAddress()(address)"  --rpc-url $RPC
cast call $V "pendingPayoutActivatesAt()(uint64)" --rpc-url $RPC
cast call $V "claimNonce()(uint256)"            --rpc-url $RPC
cast call $V "lastActivity()(uint64)"           --rpc-url $RPC
cast call $V "delivery()((uint256,address,uint64,uint16,address,bool))" --rpc-url $RPC
cast call $V "coins(uint256)((address,address,address,address,uint16,bool,uint64,address))" 0 --rpc-url $RPC

# 4. The coin's splitter holds the pinned split R/P/C, and the escrow balance it can pull.
S=<splitter address from coins(0)>
cast call $S "R()(uint16)" --rpc-url $RPC   # T + 70
cast call $S "P()(uint16)" --rpc-url $RPC   # 15
cast call $S "C()(uint16)" --rpc-url $RPC   # reflections ? 50 + (T-100)/2 : 0
cast call $S "pending()(uint256)" --rpc-url $RPC
PT=0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
cast call 0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e "balanceOfToken(address,address)(uint256)" $S $PT --rpc-url $RPC

# 5. Reflections are a real stream: reward rate, window end and total weight.
ST=<staking address from coins(0)>
cast call $ST "rewardRate()(uint256)"  --rpc-url $RPC
cast call $ST "periodFinish()(uint256)" --rpc-url $RPC
cast call $ST "totalWeight()(uint256)"  --rpc-url $RPC
cast call $ST "earned(address)(uint256)" <holder> --rpc-url $RPC

# 6. The API mirrors the chain. /status reports lag between indexed and head block; totals are
#    base-unit strings; the event kinds are exactly the ones the indexer decodes.
curl -s "$API/status"
curl -s "$API/vaults?limit=5"
curl -s "$API/activity?kind=claimed&limit=10"
curl -s "$API/handles/xpaydaymoney"
HANDLE=xpaydaymoney
curl -s "$API/h/$HANDLE/summary"

# 7. The contract tests (fork tests run against the real Pons contracts).
cd contracts && forge test

# 8. The local, end-to-end lifecycle on an anvil fork of chain 4663 (anvil + API with
#    DEV_FAKE_X=1; see ops/dev for the exact scripts).
ops/dev/anvil.sh
ops/dev/deploy-local.sh
bun ops/dev/e2e.ts

If any live read disagrees with a number in this document, the live read is correct and this document is stale on that point.