V2 overview
sender just got an upgrade. v2 is a new set of contracts behind every new launch, built around one idea: fees should reach the people they belong to without anyone having to ask for them. Every trade now pays out on the spot, to the creator or to every holder through a new distribution system that works the same for ten holders or ten thousand. Markets open at a price the creator picks, cover the full price range with no ceiling, and can be priced in ETH or in a token of your choice.
The sections below walk through each piece: how a launch unfolds, where every fee goes, how holder rewards are earned and collected, how the launch window keeps snipers in check, and what the factory owner can change. At a glance:
What stays the same: a launch is one transaction your own wallet signs, opening a Uniswap v4 market, and sender never takes custody of tokens or funds. Supply is fixed at 1,000,000,000, liquidity is locked permanently, nobody gets an allocation, the trading fee is 1%, and a short launch window guards against snipers.
Tokens launched before v2 keep running on their original contracts. Their rules are described in the v1 docs.
Innovative holder distribution system
Paying every holder a share of every trade sounds simple, and on Ethereum it is not. A contract cannot walk through a list of thousands of wallets and pay each one: every payment costs gas, the cost grows with every new holder, and a popular token would soon make each trade too expensive to send. Balances also change constantly, so any list of who is owed what would be out of date by the next block. That is why most tokens that promise holder rewards ask you to wait for a periodic airdrop, or send them by hand.
v2 does it without a list. Every swap streams the holders’ share of its fee into the token, in the paired asset, and the token keeps one running tally of how much each token held has earned so far. Your rewards are simply your share of that tally: the percentage of the supply you hold, for as long as you hold it. Nothing is paid to anyone until it is theirs to collect, so a trade costs the same with ten holders or ten thousand.
Collecting happens on its own. Any buy, sell or transfer that involves your wallet harvests your rewards and pays them to you in the same transaction. If you would rather not trade, you can claim them individually at any time from the token page. See holder rewards for the details.
Launch lifecycle
Every launch follows the same four steps. None of them can be skipped or reordered.
- 01
Create
The creator sets a name, symbol, image and links, chooses the paired asset, the opening price and who receives fees, and pays a Ξ0.002 launch fee. The entire supply of 1,000,000,000 is placed straight into a locked Uniswap v4 position. No wallet, including the creator's, receives an allocation.
- 02
Launch window
For the first three blocks, buying is limited: a cap on each buy and a cap on how much any wallet can hold. The window ends on its own and cannot be re-armed.
- 03
Trade
Anyone can buy and sell against the market. The price rises as people buy and falls as they sell, and you can always sell back. There is no waiting for the creator to add liquidity.
- 04
Earn
Every trade pays a 1% fee, paid out in the same transaction. 70% goes to the creator side, either the creator or all holders, and 30% goes to the protocol.
Opening price
The creator chooses where the market opens. On a pairing with a known price, the create page suggests a default opening market cap; on any other, the creator sets it in the paired asset.
The whole supply sits in one locked position that covers every price from the opening one upward. Buyers take supply out of it as they buy, and the price climbs with them. There is no separate bonding curve and no migration: the market is a Uniswap pool from the first block, with real depth and a real chart.
Because the position reaches the top of the price range, there is no ceiling on how far a token can go. The market keeps working the same way at any valuation.
Pairings
A market does not have to be priced in ETH. It can be paired against another token, and that token becomes the currency of the whole market: you buy and sell in it, and fees are paid in it. The pairing is fixed at launch and can never change.
The create page lists the pairings sender has reviewed, such as meme coins and tokenised equities. Some pairings cannot be bought with ETH through sender, and in that case buyers need to already hold the paired asset. The create page warns you before you launch.
The protocol never supplies liquidity. Every market is seeded only by its own supply.
Snipe protection
For the first three blocks the market is guarded:
The wallet cap counts everything a wallet bought during the window, so selling and buying back does not reset it, and it applies to plain transfers as well as buys. The length of the window is fixed for each market at launch.
A determined sniper with many funded wallets can still accumulate. The guard raises the cost, it does not remove the behaviour.
Developer buy
A creator can buy in the launch transaction itself, in the same market and paying the same 1% fee as everyone else. The contract sets no ceiling on it, and it is not subject to the launch window’s caps.
Instead of limiting it, the protocol publishes it: every token page shows what the creator opened with and what they still hold.
Fees
Fees normally pay out inside every trade, without a collect step. Failed deliveries are deferred to the market’s locker for retry. The split is written into each market when it launches, so a later change to the protocol’s settings cannot reach back into it.
Holder rewards
At launch a creator can send the 70% creator side to holders instead of to themselves. It is then shared among everyone holding the token, in proportion to their balance, and paid in the paired asset. There is no minimum balance and no waiting period.
Rewards are paid out automatically whenever your balance changes: a buy, a sell or a transfer. Claiming is only needed to collect them without trading. The market itself and the protocol’s own contracts do not earn rewards, and a holder can opt out.
The choice is fixed at launch and can never be switched off, so it cannot be used to attract buyers and then taken away. If a wallet refuses a payout, that payout is forfeited.
Graduation
Elsewhere, graduation means migrating a curve to a DEX. Here there is nothing to migrate, so it is a milestone: the market has graduated once it holds about Ξ1 of value in its paired asset.
Nothing unlocks or modifies trading rules when a market crosses it. No migration, no unlock, no fee, and no change in how the market trades.
Owner powers
The factory owner can change the factory’s configuration: the fee split, the length of the launch window, the launch fee and whether new launches are paused. These settings apply to new launches. The owner can also recover forfeited rewards.
Markets that have already launched keep their fee split and their locked liquidity. Pausing stops new launches and does not touch existing markets.
The factory is an owner-upgradeable UUPS proxy. Its owner can also change the treasury and rebind future launches to a replacement hook and locker. Existing markets keep their original hook, locker and market snapshot. Treasury and administrative authority are read from the factory, so the upgrade authority remains part of the trust model.
Safety and guarantees
Guaranteed by the contracts. The liquidity position is held by a locker with no withdraw, burn or transfer path, so nobody can pull it, including us. Token supply is fixed, with no mint, no owner and no pause. The factory ends every launch holding zero tokens.
Not guaranteed by anything. That a token holds value, that anyone keeps trading it, that the creator does not sell, or that the price does not go to zero. Removing specific failure modes is not the same as making something safe.
Audit status. The contracts are not audited. They are covered by fork, fuzz and stateful invariant tests against the real Uniswap v4 singleton on a mainnet fork, including adversarial cases. Testing is evidence, not proof. Treat the code as unaudited software holding real funds.
Risk disclosures
Tokens launched here are created by users, with no review. They are volatile, often worthless, and transactions cannot be reversed. Graduation is a number, not an endorsement, and burning supply guarantees nothing about price. None of this is investment advice. Read the full risk disclosure before you trade.
Contracts
These are the v2 contracts on Ethereum mainnet, chain id 1. New launches use generation v2-production-r2, active from block 26134578. Send launch calls to the factory proxy; each launched token is a separate clone, not the implementation address below.
sender v2
Uniswap v4
Earlier v2 generations
The same factory can launch markets with different hook and locker generations. Read launchInfo(token) and match both the factory and market.key.hooks to the registry. The factory’s current locker is only the default for new launches.
v2-production, indexed from block 26110133.
Addresses and start blocks on this page come from web/src/lib/v2/deployment.production.json. The generated ABIs are in web/src/lib/v2/abi.ts; Solidity interfaces are in contracts/src/v2. The examples below use those repository imports and assume viem public and wallet clients configured for Ethereum mainnet, with a connected account.
Launching a token
Call launchV2(params) with the tuple defined by LaunchTypes.Params. Read launchFee() immediately before simulating. The launch fee is always ETH. initialBuy is an explicit gross budget in raw base units, including the hook fee; sending extra ETH does not increase it. Unspent opening-buy funds and excess ETH are refunded to the launcher.
import {parseEther, parseEventLogs, zeroAddress, type Address} from "viem";
import deployment from "@/lib/v2/deployment.production.json";
import {v2FactoryAbi} from "@/lib/v2/abi";
import {openingMinimumOutput} from "@/lib/v2/launch";
const factory = deployment.registry.generations[0].factory as Address;
const [account] = await wallet.getAddresses();
if (!account || await wallet.getChainId() !== 1) throw new Error("Connect a mainnet wallet");
const fee = await client.readContract({address: factory, abi: v2FactoryAbi, functionName: "launchFee"});
const block = await client.getBlock();
const params = {
name: "Doggie", symbol: "DOGGIE", metadataURI,
base: zeroAddress,
launchTick: 195800, // explicit even for ETH; no default-tick sentinel
beneficiary: account, holderRewards: false,
initialBuy: parseEther("0.01"),
minimumOutput: openingMinimumOutput("0.01", 195800, 18),
deadline: block.timestamp + 600n,
earlyBuyers: [],
};
const {request} = await client.simulateContract({
account, address: factory, abi: v2FactoryAbi, functionName: "launchV2",
args: [params], value: fee + params.initialBuy,
});
const hash = await wallet.writeContract(request);
const receipt = await client.waitForTransactionReceipt({hash});
if (receipt.status !== "success") throw new Error("Launch reverted");
const [launched] = parseEventLogs({
abi: v2FactoryAbi, eventName: "LaunchedV2",
logs: receipt.logs.filter(log => log.address.toLowerCase() === factory.toLowerCase()),
});
if (!launched) throw new Error("Launch event unavailable");
const token = launched.args.token;For an ERC-20 opening buy, approve the factory directly for initialBuy and send exactly the ETH launch fee. This approval does not use Permit2. Taxed transfers are rejected when the factory receives less than the requested budget. The protocol accepts base tokens beyond the site’s reviewed pairing list; API compatibility alone does not establish that trading and reward delivery will work.
metadataURI is immutable. The site renders metadata only from its trusted hosting paths; an arbitrary external URI does not guarantee a visible image or profile.
Buying and selling
Read the full pool key from the factory. V2 accepts either currency order: a buy uses zeroForOne = !market.token0, and a sell uses market.token0. The pool’s LP fee field is zero; the hook charges the 1% trading fee in the base asset. Keep the key’s hook and fee exactly as returned.
First get a quote for the chosen input amount and set a minimum output. For ERC-20 input, complete the approvals below before simulating the swap.
import {zeroAddress} from "viem";
import {UNISWAP} from "@/lib/contracts";
import {v2FactoryAbi} from "@/lib/v2/abi";
import {encodeV4ExactInSwap, universalRouterAbi} from "@/lib/swap";
const market = await client.readContract({
address: factory, abi: v2FactoryAbi, functionName: "launchInfo", args: [token],
});
const buy = true; // false for a sell
const zeroForOne = buy ? !market.token0 : market.token0;
const currencyIn = zeroForOne ? market.key.currency0 : market.key.currency1;
const {commands, inputs} = encodeV4ExactInSwap({
poolKey: market.key, zeroForOne, amountIn, amountOutMinimum: minOut,
});
const deadline = (await client.getBlock()).timestamp + 300n;
const {request} = await client.simulateContract({
account, address: UNISWAP.universalRouter, abi: universalRouterAbi,
functionName: "execute", args: [commands, inputs, deadline],
value: currencyIn === zeroAddress ? amountIn : 0n,
});
await wallet.writeContract(request);amountIn is in the input currency’s decimals and minOut in the output currency’s decimals. Quote the full swap and apply slippage to that result; do not subtract 1% again. ERC-20 inputs require two approvals: token allowance to Permit2, then Permit2 allowance to the Universal Router for that token, amount and expiry. This applies to sells and to ERC-20-base buys.
import {erc20Abi, parseAbi} from "viem";
// Only send these if the existing allowances are insufficient.
const approval = await wallet.writeContract({
account, address: currencyIn, abi: erc20Abi, functionName: "approve",
args: [UNISWAP.permit2, amountIn],
});
await client.waitForTransactionReceipt({hash: approval});
const permit = await wallet.writeContract({
account, address: UNISWAP.permit2,
abi: parseAbi(["function approve(address token, address spender, uint160 amount, uint48 expiration)"]),
functionName: "approve",
args: [currencyIn, UNISWAP.universalRouter, amountIn, Number(deadline)],
});
await client.waitForTransactionReceipt({hash: permit});During the guarded window, use a supported router that exposes the executing wallet through msgSender(). The hook recognizes its configured routers and quoter; arbitrary hookData cannot impersonate a wallet. Simulate with the same account that will submit the swap. The repository encoder matches the deployed router; adding fields from a newer router ABI can corrupt its calldata.
Getting a quote
Quoters run through eth_call using simulateContract, not a sent transaction. During the launch guard use the market generation’s guard-aware quoter and the real executing wallet. After the guard, the standard v4 quoter works too. A quote does not reserve a price or bypass the early-buyer and wallet caps.
import {type Address} from "viem";
import {quoterAbi, V4_QUOTER} from "@/lib/swap";
const generation = deployment.registry.generations.find(g =>
g.factory.toLowerCase() === factory.toLowerCase() &&
g.hook.toLowerCase() === market.key.hooks.toLowerCase()
);
if (!generation) throw new Error("Unknown v2 market generation");
const blockNumber = await client.getBlockNumber({cacheTime: 0});
const protectedWindow = blockNumber < market.launchBlock + market.guardDuration;
const {result: [amountOut, gasEstimate]} = await client.simulateContract({
account,
address: (protectedWindow ? generation.quoter : V4_QUOTER) as Address,
abi: quoterAbi, functionName: "quoteExactInputSingle",
args: [{poolKey: market.key, zeroForOne, exactAmount: amountIn, hookData: "0x"}],
});
const minOut = amountOut * 995n / 1000n; // example 0.5% slippage
if (minOut === 0n) throw new Error("Output too small");Distinguish an RPC failure from a contract rejection. If the guard state is unavailable, treat the market as protected and use its guard-aware quoter, or stop and retry.
Routing with ether
An ETH-paired v2 market trades native ETH directly. For an ERC-20 pairing, the site composes supported v2/v3/v4 swap legs inside the Uniswap Universal Router. These are liquidity venue versions, separate from the Sender protocol version. Quote the entry and exit routes and protect both intermediate and final amounts.
The legacy StockZap address in the v1 docs is not a general-purpose v2 router. Use the v2-aware encoders in web/src/lib/swap.ts and routing logic in web/src/hooks/useSwap.ts. The hook on every hop and either currency order must be preserved. A pairing without a viable ETH route requires the actual base token; do not silently assume WETH or ETH is interchangeable with it.
Reading state
launchInfo(token) returns the v2 Market struct, not the v1 six-field launch tuple. It contains the key, token, base, launcher, beneficiary, position id, launch/pool/boundary ticks, currency order, fee split, launch block and guard duration. Use the generated tuple ABI rather than a hand-written partial return type.
import {parseAbi, type Address} from "viem";
import {poolIdFor} from "@/lib/swap";
const poolId = poolIdFor(market.key);
const locker = generation.locker as Address;
const lockerReadAbi = parseAbi([
"function baseReserves(uint256 tokenId) view returns (uint256)",
"function graduationStatus(uint256 tokenId) view returns (uint256 principal, uint256 valueWei, bool priceAvailable, bool qualified)",
"function isGraduated(uint256 tokenId) view returns (bool)",
"function graduationThresholdFor(uint256 tokenId) view returns (uint256)",
]);
const [principal, valueWei, priceAvailable, qualified] = await client.readContract({
address: locker, abi: lockerReadAbi, functionName: "graduationStatus",
args: [market.positionId],
});principal is raw base units in this locked position, excluding fees; valueWei is its ETH valuation. priceAvailable = false means unavailable pricing, not zero value. isGraduated includes current qualification; graduated(positionId) is the stored historical flag and checkGraduation(positionId) can persist it. Split values are basis points summing to 10,000. Read split.holders to distinguish holder mode from creator mode.
Enumerate launches with totalLaunches() and allTokens(index), or index LaunchedV2. Read token name, symbol and metadata from the token; those strings are not included in the launch event. Historical reads need an archive RPC.
Claiming fees
Normal swaps pay the creator, treasury or holders immediately. Failed deliveries become backed allocations in that market’s locker. Anyone can call claim(poolId) to retry them; recipients and original shares stay fixed. The argument is a bytes32 pool id, not the NFT position id, and this is not the v1 collect/creator-claim flow.
import {v2LockerAbi, v2RewardAbi} from "@/lib/v2/abi";
const [total, creator, holders] = await client.readContract({
address: locker, abi: v2LockerAbi, functionName: "pending", args: [poolId],
});
if (total > 0n) {
const {request} = await client.simulateContract({
account, address: locker, abi: v2LockerAbi, functionName: "claim", args: [poolId],
});
await wallet.writeContract(request);
}
// Only for a holder-rewards market. Rewards use the base asset's decimals.
if (market.split.holders > 0) {
const owed = await client.readContract({
address: token, abi: v2RewardAbi, functionName: "pendingReward", args: [account],
});
if (owed > 0n) {
const {request} = await client.simulateContract({
account, address: token, abi: v2RewardAbi, functionName: "claim",
});
await wallet.writeContract(request);
}
}Holder rewards also settle automatically when balances change. claimFor(holder) pays the named holder, never the caller; setRewardExcluded(bool) controls the caller’s opt-out. A rejected holder reward payment is forfeited, so check receiving-wallet compatibility. This differs from a failed locker redemption: creator or holder-funding failures there revert the redemption for retry. A failing treasury share is retained in treasuryOwed(base) and retried with releaseTreasury(base).
Limit order compatibility
The deployed v1 limit order book is tied to its legacy factory and is not a v2 order endpoint. No v2 order book is listed in the production registry. Integrators providing orders must use an execution service that supports the v2 pool key, hook, fee accounting, currency order and guarded-wallet identity. Do not send v2 tokens to the legacy book.
Events to index
Scan each unique factory once from its earliest registered start block. Scan every generation’s hook and locker, including superseded ones. Match a market by factory and pool hook; where only a factory event and block are known, use the generation active at that block. Reconcile block hashes and roll back removed logs on reorgs.
FeeCharged records the charge; FeeDistributed and FeesClaimed record delivery. Do not sum all three as new fee revenue or count holder funding again when a holder claims it. PoolManager swap deltas and the hook’s base fee are separate parts of trade accounting.
Uniswap v4 pools
A v4 pool has no standalone address. Its id is keccak256(abi.encode(poolKey)), computed with all five fields. All markets share the PoolManager; its ETH or ERC-20 balance is not an individual market’s liquidity.
launchTick expresses token units per base unit regardless of address order; the stored poolTick reverses its sign when the token is currency0. Respect the market’s stored tick bounds. Opening at a range boundary can report no active liquidity until the first trade crosses it.
Errors
Use the ABI of the failing contract and unwrap PoolManager WrappedError and router execution errors before interpreting the inner revert. The v1 error names and six-field launch ABI do not describe v2.