What Tender is
Tender lets a community issue its own dollar on Solana. A memecoin, an app, a DAO or a creator names it, and anyone can then mint it one for one with USDC and redeem it the same way.
Every Tender dollar stands on one shared reserve of USDC and tokenized Treasuries. What that reserve earns goes where each issuer routes it.
What you get as an issuer
- A Token-2022 mint with your name and symbol stored on-chain, so wallets and explorers show it correctly.
- 1:1 mint and redeem against USDC, for anyone, with no fee.
- Yield on every dollar of yours that’s outstanding, paid to up to four routes you choose.
- No reserve to run. The program holds the USDC and Treasuries for every issuer together.
What holders get
A dollar that is always worth one USDC at the program: put one in, get one out. The program is the only mint authority, the mint has no freeze authority, and redemptions can’t be paused.
Quickstart
Issue a dollar (in the app)
- Open the app and connect a wallet.
- Enter a name (“Bonk Dollar”) and a symbol (
BONKUSD). Symbols are 2–10 characters, A–Z and 0–9, and first come first served. - Set routes. By default all yield goes to your wallet as USDC. Add up to four, and they must add up to 100%.
- Press Issue dollar and approve one transaction. It creates the issuer account, the mint and its metadata.
Then share the mint page: anyone can mint your dollar from there.
Run it yourself
The repo has the program (programs/tender, Rust and Anchor) and the site (app/, Vite and TypeScript). You need Rust, the Solana CLI 2.2 and Node 20+.
# build and test the program (15 end-to-end tests in LiteSVM)
cargo build-sbf --manifest-path programs/tender/Cargo.toml
cargo test -p tender --test integration
# deploy (devnet shown; use -u mainnet-beta for mainnet)
solana program deploy target/deploy/tender.so --program-id keys/tender-program-keypair.json
# create the reserve (makes test USDC and Treasury mints on devnet)
cd app && npm install
RPC_URL=https://api.devnet.solana.com npx tsx scripts/init-reserve.ts
# run the site
cp .env.example .env && npm run dev
To check the site’s client against the real program, start a local validator with the program loaded and run the end-to-end script:
solana-test-validator --reset \
--bpf-program Tnd4ZV5367L8r7DDxLH97bA64SkR4Ke1Lf6GZa9VwgJ ../target/deploy/tender.so
npm run e2e
Wallets and signing in
There are no accounts and no sign-up. Connecting a wallet only tells the site your address so it can show balances and build transactions. Phantom, Solflare and Backpack work out of the box.
Every action is one transaction you approve in your wallet. The site never holds keys. Minting or redeeming also creates your token account for that dollar if you don’t have one yet, which costs a little SOL in rent the first time.
Harvesting is open to anyone. A wallet that harvests pays the transaction fee, and the yield still goes only to the dollar’s routes.
How Tender works
There are three kinds of account:
- The reserve (one). It holds two vaults: liquid USDC and a tokenized Treasury. It tracks how many Tender dollars exist across every issuer, and how much yield has been counted.
- Issuers (one per dollar). Each has a name, a symbol, a mint, a supply and up to four routes.
- Mints (one per issuer). These are Token-2022 mints whose mint authority is the issuer account, which only the program can sign for.
Money moves through four paths:
| action | who | what moves |
|---|---|---|
| mint | anyone | USDC in, the same amount of the dollar out |
| redeem | any holder | the dollar burned, the same amount of USDC out |
| invest / divest | allocator | USDC swapped for Treasury tokens (or back) at the reserve’s price |
| harvest | anyone | an issuer’s earned yield, paid to its routes |
The reserve
All dollars share one reserve. It holds:
- Liquid USDC. Redemptions are paid from this.
- A tokenized Treasury, such as a T-bill token. It earns interest and is valued at the reserve’s price.
owed = total_supply + unclaimed_yield
If the reserve is worth more than it owes, the difference is yield. If it’s worth less (say the Treasury price dipped), no yield is counted until the shortfall is made up. Dollars stay redeemable at par as long as there’s liquid USDC.
The liquidity floor
The allocator moves USDC into Treasuries to earn yield, and back out to meet redemptions. The program refuses any invest that would leave less liquid USDC than min_liquid_bps of total supply (20% by default). If redemptions drain the USDC, the allocator divests Treasuries back into USDC.
Why one shared reserve
A small community can’t buy Treasuries on its own, and a thin reserve couldn’t handle a run of redemptions. Pooled, every issuer gets the same yield per dollar, and every redemption draws on the whole pile.
Yield routes
Each issuer has one to four routes. A route is a wallet, a share in basis points, and a kind. The shares must add up to 10,000 (100%).
| kind | pays | good for |
|---|---|---|
0 USDC | USDC from the reserve to the wallet | a team, dev or DAO treasury |
1 dollar | newly minted units of your dollar to the wallet | rewards, airdrops or a buyback wallet |
A dollar route doesn’t dilute anyone. The new dollars are backed by the yield itself, which is already sitting in the reserve.
Only the issuer’s authority (the wallet that created it, unless handed over with set_issuer_authority) can change routes. Yield already earned is paid under whatever routes are set when it’s harvested.
Conversion at par
mint takes n USDC and mints n of the dollar. redeem burns n and pays n USDC. Both use 6 decimals, like USDC, so the amounts are identical down to the smallest unit.
- No fee and no spread. The protocol earns only from yield.
- No oracle on the way in or out. The Treasury price affects only how much yield is counted.
- Redemptions are never paused. The admin can pause new mints. The only thing that can stop a redemption is the reserve not having enough liquid USDC at that moment, which the floor and the allocator exist to prevent.
- A dollar redeems only through its own issuer. BONKUSD burns against the BONKUSD issuer, but the USDC comes from the shared vault.
How yield is counted
Every instruction that touches the reserve first runs accrue:
fee = surplus × protocol_fee_bps / 10,000
yield_index += (surplus − fee) × 10¹⁸ / total_supply
unclaimed_yield += surplus
yield_index is a running total of yield per dollar outstanding. Each issuer stores the index it last saw. When its supply is about to change, it settles:
So an issuer earns in proportion to how many dollars it had outstanding, and for how long. A dollar minted after yield came in doesn’t share in that yield, because the mint settles everyone before the new supply is added.
Worked example
AUSD has 1,000 outstanding and BUSD has 3,000. The reserve holds 2,000 USDC and 2,000 Treasury tokens at $1.00. The Treasury price goes to $1.005, so the reserve is worth 10 USDC more than it owes. With a 10% protocol fee, 9 goes to issuers: 2.25 to AUSD and 6.75 to BUSD. If AUSD routes 60% as USDC and 40% as AUSD, a harvest sends 1.35 USDC to the first wallet and mints 0.90 AUSD to the second. This exact case is one of the program’s tests.
Rounding
Every division rounds in the reserve’s favour. Issuers together can never be paid more than was counted, and the tests check after every step that the reserve covers every dollar plus every unpaid bit of yield.
The $TENDER token
The program sends its protocol fee, a share of yield capped at 20% by code, to a treasury wallet that the admin sets. The plan is for that fee, and control of the reserve’s settings (fee, liquidity floor, allocator, price authority), to pass to $TENDER holders through governance once there is one.
Tender dollars never need $TENDER. Minting, redeeming, issuing and harvesting work without it.
Instructions
| instruction | signer | does |
|---|---|---|
initialize_reserve | admin | Creates the reserve and its two vaults, once. |
update_reserve | admin | Sets roles, fee (≤ 20%), liquidity floor and mint pause. Counts yield under the old fee first. |
set_admin | admin | Hands over admin. |
create_issuer | creator | Creates the issuer, the Token-2022 mint (metadata pointer plus on-chain metadata) and the routes. |
set_routes | issuer authority | Replaces the routes. |
set_issuer_authority | issuer authority | Hands over the issuer. |
mint | user | USDC in, dollar out, 1:1. |
redeem | user | Dollar burned, USDC out, 1:1. |
harvest | anyone | Pays an issuer’s earned yield to its routes. Remaining accounts: one token account per route, in order. |
collect_protocol_fee | anyone | Pays the protocol’s share of yield to the treasury’s USDC account. |
update_price | price authority | Moves the Treasury price by ≤ 0.5%, at most once an hour. |
invest / divest | allocator | Swaps USDC and Treasuries at the reserve price. invest must respect the liquidity floor. |
Accounts and addresses
| account | seeds | mainnet address |
|---|---|---|
| program | Tnd4ZV5367L8r7DDxLH97bA64SkR4Ke1Lf6GZa9VwgJ | |
| reserve | ["reserve"] | 9C4Tf22GakAo6STmU5JnrgFbTqJ5mgGKnWd2aEcqZyAa |
| USDC vault | ["usdc_vault"] | DuorJxY9acRPQbGuBw6LbbGqkHuEXnWWuSeHci47YmNX |
| USDY vault | ["tbill_vault"] | 4AS2bXibt9bkn2o8YTcRyNWbNRpGAUVfgjUcBzKtJRuL |
| USDC mint | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | |
| USDY mint (Ondo) | A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6 | |
| issuer | ["issuer", symbol] | one per dollar |
| dollar mint | ["mint", issuer] | one per dollar |
Units
- Amounts are base units with 6 decimals (1 USDC = 1,000,000).
tbill_priceis USDC base units per Treasury base unit, × 10⁹. So 1_005_000_000 means $1.005.yield_indexis yield per dollar base unit, × 10¹⁸.
Errors
| code | name | meaning |
|---|---|---|
| 6000 | BadSymbol | Symbol must be 2–10 characters, A–Z and 0–9 only |
| 6001 | BadName | Name must be 1–32 characters |
| 6002 | BadUri | Metadata URI is over 200 characters |
| 6003 | BadRoutes | Routes must be 1–4 wallets whose shares add up to 100% |
| 6004 | BadRouteAccounts | Route token accounts missing or not matching the routes |
| 6005 | ZeroAmount | Amount must be greater than zero |
| 6006 | Paused | Minting is paused |
| 6007 | InsufficientLiquidity | Not enough liquid USDC right now |
| 6008 | BelowLiquidityFloor | Invest would breach the liquidity floor |
| 6009 | NothingToHarvest | No yield to pay out yet |
| 6010 | BadPrice | Price must be positive and move ≤ 0.5% |
| 6011 | PriceTooSoon | Price updated less than an hour ago |
| 6012 | FeeTooHigh | Protocol fee above the 20% cap |
| 6013 | BadParams | Invalid reserve parameters |
| 6014 | Overflow | Arithmetic overflow |
| 6015 | CapReached | The mint would take total supply over the reserve’s cap |
TypeScript client
app/src/chain/tender.ts has no Anchor dependency. It builds every instruction, decodes both account types and mirrors the yield math, so a UI can show what a harvest will pay before anyone sends one.
import { Connection } from "@solana/web3.js";
import { Tender, parseUsd, harvestable } from "./chain/tender";
const t = new Tender(new Connection(RPC));
const reserve = await t.fetchReserve();
// mint 100 BONKUSD (creates the user's token accounts if needed)
const ixs = t.convert("mint", user, reserve.usdcMint, "BONKUSD", parseUsd("100"));
// what would a harvest pay right now?
const issuer = await t.fetchIssuer("BONKUSD");
const { usdc, tbill } = await t.vaultBalances();
const ready = harvestable(reserve, issuer, usdc, tbill);
Roles and limits
| role | can | can’t |
|---|---|---|
| admin | set roles, fee, floor and supply cap; pause mints | take USDC, pause redemptions, mint dollars, set a fee above 20% |
| price authority | move the Treasury price ±0.5% an hour | move it faster, or touch any funds |
| allocator | swap USDC ↔ Treasuries at the reserve price | take value out, or go below the liquidity floor |
| issuer authority | change its own routes | mint its dollar without USDC, or touch the reserve |
The admin, price authority and allocator run the reserve from the app’s Admin tab, which appears only for those wallets. It suggests each price update from USDY’s live market price on Jupiter.
The upgrade authority outranks every row above. Whoever holds it can replace the program, so holders are trusting that wallet with everything in the reserve. Moving it, and the admin role, to a multisig (for example Squads) is the next step for safety.
Risks
- Treasury token risk. The reserve’s Treasury token is Ondo’s USDY. Ondo can freeze USDY accounts, and USDY isn’t offered to US persons. If USDY loses value or the vault is frozen, the reserve can be worth less than it owes. Yield stops first. Beyond that, the last redeemers bear the loss.
- Price authority. The USDY price comes from a trusted signer, bounded to 0.5% an hour; the Admin tab suggests USDY’s market price from Jupiter. An overstated price counts yield that isn’t there.
- Price steps. Each update adds yield in one step. Someone could mint just before a step, take a share of it for their own dollar, and redeem after. Updating at least daily keeps each step too small to be worth it.
- Allocator. It trades at the reserve’s price. If that price is stale it could trade against the reserve, up to the 0.5%-an-hour band.
- Liquidity. Redemptions need liquid USDC. A run larger than the floor waits on the allocator to divest.
- USDC. Every dollar is ultimately a claim on USDC, with all of USDC’s own risks. Circle can freeze USDC accounts, the reserve’s vault included.
- Regulation. Issuing dollar-like tokens and passing on Treasury yield may be regulated where you or your users are. Get legal advice.