About Basin
Basin is a social network on Robinhood Chain where any post can carry a reward pool: $BASIN that's shared with the people who engage with it. It works like the social apps you know (a feed, replies, reposts, DMs, profiles), with a built-in wallet so earning and paying happen right where you post.
Fund a post
Add $BASIN to a post. It's locked in a public contract for 24 hours.
Engage and earn
Follow, repost, reply, quote or like it to earn a share.
Claim for free
Rewards land in your Wallet. One tap claims them all, no gas.
How pools work
When you post, you can attach a pool of $BASIN. The tokens move into the pool contract when you post, and everyone sees the pool on the post. You can also add a pool later, to an older post of yours or to anyone else's: see boosting.
A pool stays open for 24 hours (a boost ends with the pool it joins, so boosting never restarts the timer). When it closes, it's split between everyone eligible who engaged while it was open. If nobody eligible engaged, the whole pool comes back to you. Basin never takes a cut of pools.
Boosting a post
Any post can get a pool after it's posted: an older post of your own, or someone else's post you want more people to see. Tap $ in the post's action bar (it shows a rocket when the post already has a pool running), choose how much $BASIN, and confirm.
- A boost is its own pool. If the post already has a pool running, the boost ends with it, so boosting never restarts the countdown; otherwise it runs for 24 hours. It starts when your $BASIN is locked, and only engagement after that counts, so a boost pays for the attention it brings, not for likes from last week.
- Anyone who already engaged can still earn from it by doing something they haven't done yet: a repost, reply, quote, or follow from the post.
- You can't earn from your own boost, and the post's author never earns from pools on their own post. You can still earn from the other pools on it: the author's pool, and other people's boosts.
- A post can have several pools at once. Its badge adds up everything still open and shows how many boosts it has.
- If nobody engages, the boost comes back to you, and so does anything unclaimed after 30 days. It never goes to the author.
The pool flywheel
Pools feed each other. Every funded post pays people to engage, that engagement gets the post seen, and the people who earned or got seen fund the next pool.
Step 1 · Fund
A creator adds $BASIN to a post
A creator attaches a pool of $BASIN to their post. The tokens go into the pool contract, and the pool shows on the post for everyone to see.
Who earns, and how much
Each kind of engagement is worth points. Your share of a pool is your points divided by everyone's.
Verified members' points count 1.5×, and holding $BASIN adds more: see holder multipliers.
Anyone can earn by engaging while the pool is open (new accounts count too), except the person who made the post. Your engagement counts in full once you do any one of these:
- link your X account (Settings);
- get Verified;
- hold at least 100,000 $BASIN in your Basin wallet.
Until then it counts at 0.1×. That keeps throwaway accounts from taking much of a pool.
Holder multipliers
The more $BASIN you hold, the more your engagement counts in every pool. $BASIN has a fixed supply of 1,000,000,000, so 1% is 10 million.
Your points count 2× (Backer 2×)
- Stacks with Verified. A Verified Whale counts 1.5 × 2.5 = 3.75×. The total never goes above 4×, so big holders can't take over a pool.
- Your balance is checked twice: when you engage and again when the pool pays out. The lower one counts, so tokens passed between accounts during a pool only count once.
- Holding 100,000 $BASIN or more also counts as one of the ways to earn in full, with or without X or Verified.
Claiming rewards
When a pool you earned from closes, you get a notification. Open Wallet → Rewards and tap Claim, or Claim all.
- Claiming is free. Basin sends the claim for you, so you don't need ETH for gas or to approve anything.
- Rewards only ever go to your own wallet. The contract checks every claim against a fingerprint of the payout list, so nobody else can claim your share or send it somewhere else.
- You have 30 days to claim.
Getting unclaimed $BASIN back
30 days after a pool pays out, anything nobody claimed goes back to the person who funded it. Basin does this automatically every day. You can also do it yourself from Wallet → Rewards → Pools you funded once it's due.
Signing up
Sign in with any of these. Your first sign-in creates your account:
- X (also links your X account, which lets you earn from pools);
- a wallet you already use, like MetaMask or Rainbow;
- email, with a 6-digit code.
Signing up with X or email gives you a built-in wallet automatically, with no seed phrase to write down. Then you pick a username, name, and profile picture. Use the same sign-in method each time: a different method counts as a different account.
Posting
- Where it goes: posts go to Home (your followers and the main feed) unless you pick one of your Basins with the Home button in the composer.
- What you can add: text, up to 5 photos or a video (a post can be just media), more posts to make a thread, a poll, and a $ reward pool (the last button in the composer).
- Polls: tap the chart button, write your question as the post, and add 2 to 6 choices. Pick how long voting stays open (1 hour to 7 days). Everyone gets one vote and can't change it. Voters see the results right after voting, and you can always see them on your own poll. A poll can't have photos or a video.
- Linking to Basin: paste a link to a Basin post and it shows inside your post the way a quote does, not as a link preview. A link to a Stream shows its live card.
- Discover: Search (under Home in the sidebar) shows live and upcoming Streams, coins pumping today, people getting popular, and trending posts. The Trending box on the right links there too.
Wallet
- You control your wallet. Basin's built-in wallet (made for you when you sign up with X or email) is secured by Privy; Basin can't move your funds.
- Export your private key any time from the Wallet page to use it in another app.
- If you signed in with MetaMask or Rainbow, that wallet is your Basin wallet and every send opens there for you to confirm.
- Basin pays the gas for your Basin wallet, so you never need ETH to claim, fund a pool, or send. With MetaMask or Rainbow, claiming is free but funding and sending need a little ETH for gas.
Verified
Basin Verified is $180/year or $18/month (yearly saves $36). Pay in USDG, the Paxos dollar on Robinhood Chain. You get:
- 1.5× reward weight on every pool you engage with
- Write full-length Articles with titles and formatting
- Early access to new features in Labs
- Earn from reward pools without linking X
- Ranked higher in the For You feed and in replies
- Higher posting and engagement limits
- Verified badge on your profile and posts
Launch offer: until Thu, Oct 8, 11:59 PM ET, anyone who isn't Verified yet pays $9/month (50% off) or $81/year (55% off).
Get it on the Verified page (or Settings → Verified). Verified starts as soon as your payment confirms and doesn't renew automatically.
Basins
Basins are communities: each has its own feed, profile picture, header, description, links, and members. Anyone can browse Basins, join them, and post in them.
- Posting in a Basin: when you write a post, pick where it goes with the button in the composer: Home, or one of the Basins you've joined. Basin posts also show in the main feed, marked in [Basin] next to the author's name; tap it to open the Basin.
- Creating: for now, only Verified members with Settings → Labs → Beta features on can create a Basin. The first 3 are free, then $15 each.
- Roles: the owner can do everything, including deleting the Basin and appointing admins. Admins edit the Basin and appoint moderators. Moderators hide posts from the Basin and ban members. Nobody can act on someone of equal or higher rank.
- A post hidden from a Basin stays on its author's profile.
Invite friends
Invite your friends to Basin and earn from it. Your link is basin.social/r/yourname and your code is your username; find both on the Invite friends page. Invite as many people as you like.
- When your friend joins: they sign up with your link or code, then either link an X account or hold at least $10 of $BASIN in their wallet for 7 days. When they do, you both get a week of Verified.
- When your friend gets Verified: you earn 20% of every Verified payment they make in their first year, in $BASIN at the price on payout day: $3.60 from every $18 month, $36 from every $180 year. Part of what Basin earns from Verified goes straight back to the people who bring members in.
- Getting paid: referral rewards are paid out automatically every day. They show up in your Rewards (in your wallet) as “Referral rewards”; tap Claim like any other reward. Claiming is free.
- Your link remembers you: after a friend opens your link, it counts for 30 days, even if they sign up later. Signing up shows who invited them, and they can type your code instead. Forgot? They can add your code from the Invite friends page within 3 days of joining.
- Fair play: you can't invite yourself or an account sharing your wallet, who invited someone can't be changed later, and each X account can count for one invite, ever. Referral Verified adds up to at most a year ahead. Fake or farmed accounts don't earn, and we can withhold rewards from abuse.
Leaderboards and awards
The Leaderboard (in the sidebar) ranks the people giving and earning the most $BASIN. Search it to find anyone.
- Top distributors: $BASIN put into reward pools that paid out, credited to whoever funded them (boosts count for the booster). Pools that were refunded don't count.
- Top earners: $BASIN earned from reward pools, claimed or not.
- Today, this week, this month, and all time. Boards reset at midnight UTC: daily every day, weekly on Monday, monthly on the 1st. A pool counts when it pays out, not when it's funded.
- Anyone can be on the leaderboard. Every profile also shows how much $BASIN that person has claimed and distributed.
Awards are coming. We're going to start giving daily, weekly, and monthly awards to the top of both boards. Top distributors will get the biggest awards, and top earners will be rewarded too.
- Only Verified members can receive awards. If someone who isn't Verified places, the award goes to the next Verified member on the board.
- Accounts that game the boards (funding their own alts, fake engagement, bots) can be left out of awards.
- We'll announce the details, and when awards start, before the first one is paid.
Streams: live voice rooms
A Stream is a live voice room: someone hosts, people drop in to listen, and the host brings people up to speak. Find them in Streams in the sidebar, in the Live now row at the top of your feed, in Live Streams on the right, on the Search page, or as a post in the feed with a live card on it. Every Stream shows how many people are listening (or, once it's over, how many joined).
- Listening: anyone can join. Tap Join and listen; the Stream keeps playing while you browse the rest of Basin (a small player sits at the bottom of the screen). On phones, the Streams tab in the bar at the bottom glows green while you're listening; tap it to go back to the room.
- Speaking: tap Raise hand. If the host or a co-host invites you up, your mic unlocks; tap Unmute when you want to talk. Until then your mic can't be heard at all.
- A Stream is a post: it can be liked, reposted, replied to, quoted, and boosted like any other post.
- Chat: everyone in the room can type in the chat beside it. On phones it's tucked into a bar at the bottom showing the latest message and how many you've missed; tap or swipe it up to open it. Links and @mentions are clickable. The host and co-hosts can delete messages, and you can delete your own. When someone boosts the Stream, the chat says so.
- Pins and the wall: the host and co-hosts can pin any message (theirs, a speaker's, or a listener's) to the top of the chat, one at a time. They can also put up to 6 things on the Stream's wall, a row between the speakers and the listeners: Basin posts or any other link (shown with its preview). Paste a link, or tap one someone shared in the chat. Everyone in the room sees changes right away.
- Sound: the headphones button deafens you (you hear nothing, and your mic goes off until you undeafen). The sliders button sets the Stream's volume, your mic volume (with a level meter so you can test it), noise suppression, and which mic and speakers to use. Tap anyone in the room to turn just them up, down, or off; only you hear the difference. Your settings are saved on this device.
- Scheduled Streams: tap RSVP and you'll get a notification when it starts. Everyone who RSVP'd is listed on the Stream's page, so others can see who's going. You're also notified when someone you follow goes live.
- After it ends, the Stream's page lists everyone who joined.
- Sharing: every Stream has its own link (the share button at the top of it). Scheduled Streams have one as soon as they're scheduled, so people can RSVP before it starts.
Hosting a Stream
Hosting is for Verified members. On the Streams page, tap Go live or Schedule (up to 4 days ahead) and fill in the form, or tap the mic in the composer. The title and description are both optional; the description is posted to your feed with the Stream. You can host it at home or in a Basin you've joined.
- Speakers and co-hosts: bring anyone up to speak, move them back to listening, or make them a co-host. Co-hosts can bring people up, move them back, and remove people. Anyone can speak or co-host when invited, Verified or not.
- Co-hosts and invites: in the composer's Stream panel, add co-hosts (up to 10) and invite people by username. They're notified right away, and again when a scheduled Stream goes live. You and your co-hosts can add more later from the Stream's page or Manage Stream in the room.
- If the host disconnects, the Stream keeps going and co-hosts take over: they can end it and change co-hosts until the host is back.
- Removing someone disconnects them, and they can't rejoin. Changed your mind? Open Manage Stream in the room and tap Allow back.
- End Stream closes the room for everyone. To step away without ending it, use the leave button next to it. A scheduled Stream starts when you open it and tap Start now.
Funding a Stream
A Stream can have a reward pool, just like a post: fund it with the $ in the Stream's room or on its post, or boost it once one is running (a live counter in the room shows how much $BASIN is in it). It works like any pool, with points for the time you spend in the room on top of the usual ones:
- Likes, reposts, replies, quotes, and follows on the Stream's post earn their usual points.
- Listening for at least 10 minutes while the pool is running earns 5 points. If the whole Stream lasted less than 10 minutes, having joined at all counts.
- Speaking (host, co-host, or speaker) earns 10 points instead of the listening points.
- The host can't earn from pools on their own Stream, and whoever funded a boost can't earn from it. Your usual multipliers, and the 0.1× rate before you link X, apply as with any pool.
- It pays out when the Stream ends, not on a 24-hour timer: a few minutes after the host ends it (or everyone leaves), everyone who earned gets their share. A boost only counts listening and engagement from after it was funded. If a Stream somehow never ends, its pools pay out 6 days after they were funded.
Streams and your privacy
- Your IP address stays private. Audio goes through our voice provider's servers (LiveKit), not directly between people, so other listeners and speakers never see where you're connecting from.
- Other people in a Stream see only your Basin username, name, profile picture, and role.
- Blocking works here too: you won't hear or see anyone you've blocked, and people blocked by the host can't join.
- Basin doesn't record Streams, but anyone listening could record what they hear. Talk like you would in public, and never share private keys, seed phrases, or personal details.
Contract addresses
Every pool is held by one contract on Robinhood Chain. Its activity is public, so you can check every pool, payout, and claim yourself.
Its source code is verified on RobinScan: the code you can read there is exactly the code that runs. It's open source (MIT) and built on OpenZeppelin's audited libraries.
Pool escrow Verified source
0x1fC0FB0dCCe88E3da5A52A045ED1b5e7801C02ED
Holds every pool until it pays out.
What the contract can and can't do
Basin can
- close a pool by publishing its payout list;
- refund a pool to the person who funded it;
- send claims and returns on your behalf (the tokens still go to you).
Nobody can
- move pool money to Basin or any address other than earners and the funder;
- pay out more than a pool holds, or use one pool to pay another;
- claim the same reward twice.
If Basin ever went offline, your money isn't stuck. If a pool hasn't been closed within 7 days, the person who funded it can take it back directly from the contract.
Two separate keys. The server's key can only close and refund pools. A second key, kept offline, can only replace the server's key, so a leaked server key can be switched off without touching anyone's pools.
How the escrow works
The escrow is one contract that holds every reward pool on Basin. It's about 220 lines of Solidity, and this part of the docs walks through it, with the real code next to each step. The code shown here is read from the same file that was deployed, and every block links to the verified source.
A pool moves through these steps in order, and none of them can happen twice:
01
Fund
Someone puts $BASIN into a pool for a post. The contract holds it.
02
Settle
When the pool closes, Basin publishes who earned what as one fingerprint (a Merkle root).
03
Claim
Each earner pulls their own share out, with a proof that their line is in the list.
04
Return
Anything left after 30 days goes back to whoever funded it. Never to Basin.
The contract states its own trust model at the top of the file:
/// @title PoolEscrow/// @notice One contract holds every post's reward pool, keyed by `poolId`/// (keccak256 of the post's id). Pools pay out via Merkle pull-claims rather/// than pushed transfers, so settling a pool is one transaction no matter how/// many people engaged.////// Trust model:/// - The settler (the server's hot key) can only (a) commit one Merkle root per/// pool, or (b) refund an unsettled pool to its own funder. It can never move/// funds to an arbitrary address./// - The owner (a cold wallet or Safe, never the server) can only replace the/// settler. If the server key leaks, the owner swaps it out; no redeploy./// - Total claims are capped at the pool's deposit, so even a bad root can't/// drain other pools./// - `claimed[poolId][account]` is set in the same transaction as the/// transfer, so a double-claim race (the classic hot-wallet treasury bug)/// can't happen here./// - Anything unclaimed after CLAIM_WINDOW — including rounding dust — goes/// back to the funder, never to Basin.contract PoolEscrow is Ownable, ReentrancyGuard {It's built on OpenZeppelin's libraries for token transfers, Merkle proofs, ownership, and reentrancy protection, rather than writing any of those by hand.
What a pool is
Every pool is a row keyed by a poolId (see pool ids). The contract also remembers who has claimed from each pool, and which address is allowed to settle.
uint256 public constant CLAIM_WINDOW = 30 days;/// If the settler never settles a pool (key lost, backend down), the funder/// can take their money back after this long. Pools settle at 24h normally.uint256 public constant STALE_AFTER = 7 days; struct Pool { address token; // address(0) = native ETH address funder; uint256 totalDeposited; uint256 totalClaimed; bytes32 merkleRoot; uint64 fundedAt; uint64 settledAt; bool settled; bool closed; // refunded or swept — no further movement possible} mapping(bytes32 poolId => Pool) public pools;mapping(bytes32 poolId => mapping(address account => bool)) public claimed; /// The only address that can settle or refund pools.address public settler;| Field | What it means |
|---|---|
| token | What the pool holds. $BASIN for every pool on Basin today. |
| funder | Who put the money in. Refunds and leftovers can only go back here. |
| totalDeposited | What actually arrived in the contract (not what was asked for). |
| totalClaimed | Paid out so far. Can never pass totalDeposited. |
| merkleRoot | The fingerprint of the payout list, set once at settlement. |
| settled / closed | Settled: claims are open. Closed: refunded or swept, nothing can move again. |
Funding a pool
Funding happens from the funder's own wallet: Basin asks you to approve the amount, then calls fundPool. A pool id can only ever be funded once, by one funder.
/// @notice Fund a pool with an ERC-20 (Basin token, USDG, or a tokenized equity)./// Each pool is funded exactly once, by one funder.function fundPool(bytes32 poolId, address token, uint256 amount) external nonReentrant { require(token != address(0), "use fundPoolNative"); require(amount > 0, "amount must be > 0"); Pool storage pool = pools[poolId]; require(pool.funder == address(0), "pool already funded"); pool.token = token; pool.funder = msg.sender; pool.fundedAt = uint64(block.timestamp); // Credit what actually arrived, so a fee-on-transfer token can't make // the pool promise more than it holds. uint256 before = IERC20(token).balanceOf(address(this)); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); uint256 received = IERC20(token).balanceOf(address(this)) - before; require(received > 0, "nothing received"); pool.totalDeposited = received; emit PoolFunded(poolId, msg.sender, token, received);}The contract measures its balance before and after the transfer and credits the difference. A token that takes a fee on transfer can't make a pool promise more than it really holds.
Settling: one root for every payout
When a pool's 24 hours are up, Basin works out everyone's share (see who earns), puts each (address, amount) into a Merkle tree, and commits the tree's root. That's one transaction whether 3 people earned or 3,000.
/// @notice Commit the computed distribution as a single Merkle root.function settlePool(bytes32 poolId, bytes32 merkleRoot) external onlySettler { Pool storage pool = pools[poolId]; require(pool.funder != address(0), "pool not funded"); require(!pool.settled && !pool.closed, "pool already finalized"); require(merkleRoot != bytes32(0), "empty root"); pool.merkleRoot = merkleRoot; pool.settled = true; pool.settledAt = uint64(block.timestamp); emit PoolSettled(poolId, merkleRoot);}Only the settler can call it, only once per pool, and only on a pool that was funded and isn't closed.
What the contract can't check
Claiming
To claim, you send your amount and a proof (a few hashes Basin gives you) that your line is in the settled list. This is the whole check:
function _claim(bytes32 poolId, address account, uint256 amount, bytes32[] calldata proof) internal { Pool storage pool = pools[poolId]; require(pool.settled && !pool.closed, "pool not claimable"); require(!claimed[poolId][account], "already claimed"); // Matches @openzeppelin/merkle-tree's StandardMerkleTree leaf encoding // for ["address", "uint256"] — the settlement job builds the tree with // that library, so the two can't drift apart. bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount)))); require(MerkleProof.verify(proof, pool.merkleRoot, leaf), "invalid proof"); require(pool.totalClaimed + amount <= pool.totalDeposited, "exceeds pool"); claimed[poolId][account] = true; pool.totalClaimed += amount; _send(pool.token, account, amount); emit Claimed(poolId, account, amount);}- Your line binds your address to your amount, so nobody can claim your share, and you can't claim a different amount.
- It's marked claimed before the tokens move, in the same transaction, so claiming twice is impossible, even with two transactions racing.
- A pool can't pay out more than it holds, even if a bad list promised more.
Basin pays the gas for claims by calling claimFor on your behalf. The tokens still go to you: the proof is for your address, so whoever sends the transaction can't redirect it.
/// @notice Claim on someone's behalf. Funds always go to `account` — the/// leaf binds amount to address — so a relayer can trigger payouts but can/// never redirect them. Lets Basin run "auto-claim" without custody.function claimFor(bytes32 poolId, address account, uint256 amount, bytes32[] calldata proof) external nonReentrant{ _claim(poolId, account, amount, proof);}Refunds and returns
There are three ways money goes back to the person who funded a pool, and it can only ever go to them:
| Function | When |
|---|---|
| refundPool | Basin closes a pool nobody eligible engaged with. Settler only. |
| reclaimStale | A pool still isn't settled 7 days after funding (Basin offline, key lost). The funder calls it. |
| sweepUnclaimed | 30 days after settling, whatever wasn't claimed. Anyone can call it. |
/// @notice Funder escape hatch: reclaim a pool the settler never settled.function reclaimStale(bytes32 poolId) external nonReentrant { Pool storage pool = pools[poolId]; require(msg.sender == pool.funder, "only funder"); require(!pool.settled && !pool.closed, "pool already finalized"); require(block.timestamp >= uint256(pool.fundedAt) + STALE_AFTER, "not stale yet"); pool.closed = true; uint256 amount = pool.totalDeposited; _send(pool.token, pool.funder, amount); emit PoolRefunded(poolId, pool.funder, amount);}/// @notice After the claim window, return everything unclaimed (and rounding/// dust) to the funder. Callable by anyone; funds can only go to the funder.function sweepUnclaimed(bytes32 poolId) external nonReentrant { Pool storage pool = pools[poolId]; require(pool.settled && !pool.closed, "not sweepable"); require(block.timestamp >= uint256(pool.settledAt) + CLAIM_WINDOW, "claim window open"); pool.closed = true; uint256 remaining = pool.totalDeposited - pool.totalClaimed; if (remaining > 0) _send(pool.token, pool.funder, remaining); emit UnclaimedSwept(poolId, pool.funder, remaining);}reclaimStale is the escape hatch: if Basin disappeared tomorrow, every unsettled pool could still be taken back by its funder, straight from the contract.
Keys and permissions
Two keys have any special power, and each can do exactly one kind of thing:
| Who | Can do |
|---|---|
| settler | Settle a pool once, or refund an unsettled pool to its funder. Basin's server holds this key. |
| owner | Replace the settler (if its key leaks). Nothing else: it can't settle, refund, or move funds. |
| funder | Fund a pool, and take it back if it's never settled within 7 days. |
| anyone | Claim for an earner (paid to the earner), and sweep leftovers to the funder after 30 days. |
/// @notice Owner only: replace the settler (e.g. after its key leaks).function setSettler(address newSettler) external onlyOwner { _setSettler(newSettler);}The owner can't give up ownership. That's deliberate: with no owner, a leaked settler key could never be replaced.
/// @notice Disabled: with no owner, a leaked settler key could never be replaced.function renounceOwnership() public view override onlyOwner { revert("renounce disabled");}Tests and audits
The contract has a test suite that runs against it with Foundry. Beyond ordinary tests, it checks that:
- each earner is paid exactly their share, and a second claim fails;
- a proof for one person, or from another pool, doesn't work for anyone else;
- random wrong amounts (1,000 tries each run) are always rejected;
- a bad list can't pay out more than was deposited;
- only the settler can settle or refund, and the owner can't do either;
- the escape hatches, refunds, and sweeps only ever pay the funder.
It also runs invariant tests: thousands of random sequences of funding, settling, claiming, refunding, sweeping, and time passing, with these two rules checked after every single step:
/// Solvency + no leakage: the escrow holds exactly what open pools still/// owe. Never less (insolvent / drained), never more (funds stuck).function invariant_balanceEqualsOutstanding() public view { uint256 outstanding; for (uint256 i; i < handler.poolCount(); i++) { (, , uint256 deposited, uint256 claimedAmt, , , , , bool closed) = escrow.pools(handler.poolIds(i)); assertLe(claimedAmt, deposited, "claimed more than deposited"); if (!closed) outstanding += deposited - claimedAmt; } assertEq(token.balanceOf(address(escrow)), outstanding);}/// Money only ever goes to the funder or the two leaf accounts.function invariant_noThirdPartyReceivesFunds() public view { assertEq(token.balanceOf(address(handler)), 0); assertEq(token.balanceOf(handler.owner()), 0); assertEq( token.balanceOf(handler.funder()) + token.balanceOf(handler.alice()) + token.balanceOf(handler.bob()) + token.balanceOf(address(escrow)), type(uint128).max );}| Compiler | Solidity 0.8.24 |
| Libraries | OpenZeppelin Contracts 5.1.0 |
| License | MIT |
| Source | Verified on RobinScan (exact match) |
| Audit | Not audited by an outside firm yet |
Pool ids
Pools are keyed by a bytes32 id. A post's own pool uses the hash of the post's id (the id in basin.social/post/<id>). Boosts and referral payouts each get their own, so one post can hold many pools.
/** On-chain pool key for a post's own (author) pool. Derived from the post id. */export function poolIdFor(postId: string): `0x${string}` { return keccak256(toHex(postId));} /** On-chain key for a boost: its own pool row id, so a post can hold many pools. */export function boostPoolIdFor(poolRowId: string): `0x${string}` { return keccak256(toHex(`boost:${poolRowId}`));} /** A weekly referral payout pool's id on-chain (one per payout). */export function referralPoolIdFor(poolRowId: string): `0x${string}` { return keccak256(toHex(`referral:${poolRowId}`));}Reading a pool
Everything is public, so you can read any pool straight from the chain. No Basin API is needed. This uses viem:
import { createPublicClient, defineChain, http, keccak256, parseAbi, toHex } from "viem"; const chain = defineChain({ id: 4663, name: "Robinhood Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },});const client = createPublicClient({ chain, transport: http() }); const ESCROW = "0x1fC0FB0dCCe88E3da5A52A045ED1b5e7801C02ED";const abi = parseAbi([ "function pools(bytes32 poolId) view returns (address token, address funder, uint256 totalDeposited, uint256 totalClaimed, bytes32 merkleRoot, uint64 fundedAt, uint64 settledAt, bool settled, bool closed)", "function claimed(bytes32 poolId, address account) view returns (bool)", "function settler() view returns (address)", "function owner() view returns (address)",]); // A post's own pool: the hash of its id.const poolId = keccak256(toHex("POST_ID")); const [token, funder, deposited, claimedSoFar, root, fundedAt, settledAt, settled, closed] = await client.readContract({ address: ESCROW, abi, functionName: "pools", args: [poolId] }); const stillInPool = closed ? 0n : deposited - claimedSoFar; // Has this wallet claimed its share yet?const done = await client.readContract({ address: ESCROW, abi, functionName: "claimed", args: [poolId, "0xYourWallet"],});settler() and owner() show which keys hold the two special powers right now.
Events to index
Every change emits an event, so you can follow pools without trusting anyone's database:
event PoolFunded(bytes32 indexed poolId, address indexed funder, address token, uint256 amount);event PoolSettled(bytes32 indexed poolId, bytes32 merkleRoot);event PoolRefunded(bytes32 indexed poolId, address indexed funder, uint256 amount);event Claimed(bytes32 indexed poolId, address indexed account, uint256 amount);event UnclaimedSwept(bytes32 indexed poolId, address indexed funder, uint256 amount);event SettlerChanged(address indexed previousSettler, address indexed newSettler);For example, every claim a wallet has made. The contract was deployed in block 75,982,716, so there's nothing to read before it. The public RPC times out on wide ranges, so read in chunks of a few thousand blocks.
import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: ESCROW, event: parseAbiItem("event Claimed(bytes32 indexed poolId, address indexed account, uint256 amount)"), args: { account: "0xYourWallet" }, fromBlock: 75982716n, toBlock: 75987716n, // then the next chunk…});Errors
When a call is refused, the transaction reverts with one of these messages:
| Message | Why |
|---|---|
| pool already funded | That pool id has been funded before. Each id is funded once. |
| amount must be > 0 | Funding with nothing. |
| nothing received | The token transfer delivered nothing. |
| use fundPoolNative | Funding with ETH goes through the other function. |
| pool not funded | Settling or refunding a pool that doesn't exist. |
| pool already finalized | The pool was already settled, refunded, or swept. |
| empty root | Settling with an empty payout list. |
| only settler | Someone other than the settler tried to settle or refund. |
| only funder | Someone other than the funder tried to reclaim a stale pool. |
| not stale yet | Reclaiming before 7 days have passed. |
| pool not claimable | The pool isn't settled yet, or it's closed. |
| already claimed | That address has claimed from this pool. |
| invalid proof | The address, amount, or proof doesn't match the settled list. |
| exceeds pool | Paying this claim would take more than the pool holds. |
| not sweepable | The pool isn't settled, or it's already closed. |
| claim window open | Sweeping before 30 days after settlement. |
| renounce disabled | The owner can't give up ownership (see keys and permissions). |
| settler required | The settler can't be set to the zero address. |
Staying safe
- Basin will never ask for your password, seed phrase, or private key.
- Only trust $BASIN at the token address above.
- Pool amounts are set by posters. A pool is a reward, not an investment.
See also the Terms and Privacy Policy.