Getting started

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.

In one line: a stablecoin under your name, with the float’s yield paid to you instead of to a stablecoin company.

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)

  1. Open the app and connect a wallet.
  2. Enter a name (“Bonk Dollar”) and a symbol (BONKUSD). Symbols are 2–10 characters, A–Z and 0–9, and first come first served.
  3. Set routes. By default all yield goes to your wallet as USDC. Add up to four, and they must add up to 100%.
  4. 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.

Concepts

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:

actionwhowhat moves
mintanyoneUSDC in, the same amount of the dollar out
redeemany holderthe dollar burned, the same amount of USDC out
invest / divestallocatorUSDC swapped for Treasury tokens (or back) at the reserve’s price
harvestanyonean 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.
value = usdc_vault + tbill_vault × tbill_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%).

kindpaysgood for
0 USDCUSDC from the reserve to the walleta team, dev or DAO treasury
1 dollarnewly minted units of your dollar to the walletrewards, 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:

surplus = value − owed
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:

accrued += supply × (yield_index − index_snapshot) / 10¹⁸

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

$TENDER isn’t part of the program in this repo, and nothing here mints or sells it.

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.

Reference

Instructions

instructionsignerdoes
initialize_reserveadminCreates the reserve and its two vaults, once.
update_reserveadminSets roles, fee (≤ 20%), liquidity floor and mint pause. Counts yield under the old fee first.
set_adminadminHands over admin.
create_issuercreatorCreates the issuer, the Token-2022 mint (metadata pointer plus on-chain metadata) and the routes.
set_routesissuer authorityReplaces the routes.
set_issuer_authorityissuer authorityHands over the issuer.
mintuserUSDC in, dollar out, 1:1.
redeemuserDollar burned, USDC out, 1:1.
harvestanyonePays an issuer’s earned yield to its routes. Remaining accounts: one token account per route, in order.
collect_protocol_feeanyonePays the protocol’s share of yield to the treasury’s USDC account.
update_priceprice authorityMoves the Treasury price by ≤ 0.5%, at most once an hour.
invest / divestallocatorSwaps USDC and Treasuries at the reserve price. invest must respect the liquidity floor.

Accounts and addresses

accountseedsmainnet address
programTnd4ZV5367L8r7DDxLH97bA64SkR4Ke1Lf6GZa9VwgJ
reserve["reserve"]9C4Tf22GakAo6STmU5JnrgFbTqJ5mgGKnWd2aEcqZyAa
USDC vault["usdc_vault"]DuorJxY9acRPQbGuBw6LbbGqkHuEXnWWuSeHci47YmNX
USDY vault["tbill_vault"]4AS2bXibt9bkn2o8YTcRyNWbNRpGAUVfgjUcBzKtJRuL
USDC mintEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
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_price is USDC base units per Treasury base unit, × 10⁹. So 1_005_000_000 means $1.005.
  • yield_index is yield per dollar base unit, × 10¹⁸.

Errors

codenamemeaning
6000BadSymbolSymbol must be 2–10 characters, A–Z and 0–9 only
6001BadNameName must be 1–32 characters
6002BadUriMetadata URI is over 200 characters
6003BadRoutesRoutes must be 1–4 wallets whose shares add up to 100%
6004BadRouteAccountsRoute token accounts missing or not matching the routes
6005ZeroAmountAmount must be greater than zero
6006PausedMinting is paused
6007InsufficientLiquidityNot enough liquid USDC right now
6008BelowLiquidityFloorInvest would breach the liquidity floor
6009NothingToHarvestNo yield to pay out yet
6010BadPricePrice must be positive and move ≤ 0.5%
6011PriceTooSoonPrice updated less than an hour ago
6012FeeTooHighProtocol fee above the 20% cap
6013BadParamsInvalid reserve parameters
6014OverflowArithmetic overflow
6015CapReachedThe 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);
Trust

Roles and limits

rolecancan’t
adminset roles, fee, floor and supply cap; pause mintstake USDC, pause redemptions, mint dollars, set a fee above 20%
price authoritymove the Treasury price ±0.5% an hourmove it faster, or touch any funds
allocatorswap USDC ↔ Treasuries at the reserve pricetake value out, or go below the liquidity floor
issuer authoritychange its own routesmint 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

Unaudited. The program has 15 end-to-end tests, and the site’s client passes full runs on a local validator and on devnet. It has not been audited. The reserve has a supply cap so that no more than a set amount of USDC can be at risk while that’s true.
  • 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.