---
title: "Faucets"
description: "How a BTR testnet deployment hands out pool assets: mintable twins versus official issuer tokens, the registration gate, the one-claim-per-token-per-day cap, and what claim reverts on."
audience: both
type: guide
status: live
lang: en
publish: true
---
# Faucets

A faucet exists only on a testnet deployment, and only for that instance's mintable mock tokens.
Which instances are testnets: [deployed instances](/docs/2-1-contract-addresses). Testnet assets
have no value, and a faucet hands out **pool assets, never gas**
([Pool Legs §5](/docs/4-4-pool-legs#5-decimals-and-gas)).

---

## 1. What the faucet is

`TestnetFaucet` is a rate-limited dispenser, not a minter. The deploy ceremony mints to the
deployer, approves the faucet, calls `fund(token, amount)` with `FAUCET_PREFUND_CLAIMS` days of cap,
then `setCap(token, cap)`. `claim` is a `safeTransfer` out of that pre-funded balance. Minting itself
is `minter`-only on `TestnetERC20`, and there is no setter for the minter, so an empty faucet is
refilled by `fund()` and by nothing else.

## 2. Issuer tokens versus mintable twins

A testnet roster mixes two kinds of token. Some are the official issuer deployments, declared in the
deploy artifact's `nativeTokens` and never minted by BTR; the rest are BTR mocks,
`new TestnetERC20(sym + ".b", sym + ".b", 18)`.

| | Official issuer token | Mintable twin |
|---|---|---|
| Minted by BTR | no | yes (`TestnetERC20`) |
| Faucet-claimable | no | yes, subject to an on-chain cap |
| Seeded to | `unmintableSeedUsdPerLeg` | `seedUsdPerLeg`, and topped up by the liquidity keeper |

The ceremony classifies a token by staticcalling `minter()`: an issuer token reverts and is treated
as un-mintable float. That is also why the [hub](/docs/glossary#hub) leg is a twin rather than the
issuer token ([Pool Legs §2](/docs/4-4-pool-legs#2-why-every-core-has-a-base-leg)). Per-instance
addresses and decimals for both kinds: [Asset Registry](/docs/2-2-asset-registry).

### 2.1. `.b` is display metadata, never a key

The `.b` lives in the ERC-20 `name()`/`symbol()` and nowhere else. The roster key is dot-free
(`USDCB`), with an explicit `display: 'USDC.b'` override for rendering. The SDK's
`canonicalTokenSymbol` **strips a trailing `.b`**, so `getToken('USDC.b')` resolves to the official
6-decimal issuer token and `getToken('USDCB')` to the 18-decimal twin. Feeding a rendered symbol back
into a lookup silently swaps one asset for the other; carry the key, not the label.

### 2.2. A twin owns no feed of its own

Twins are absent from the feed roster and quote off a feed the keeper already services, because a
dedicated feed would have nothing pushing it and would fail closed at TTL
([Oracle Keeper](/docs/3-1-5-oracle-keeper)). Which feed each twin rides, and the ref band it carries:
[Asset Registry §6](/docs/2-2-asset-registry#6-faucet-twins).

Consequences of the shared feed:

1. A twin sits a few basis points off its issuer token permanently, and quotes exactly 1.0 forever against the asset whose feed it borrows, because the two oracle configs are byte-identical. That is a shared-feed artifact, not a depeg.
2. The twin's ref band measures against a mirror of the same feed, so a depeg of the borrowed feed's asset is inherited rather than caught by that band.
3. The liquidity keeper refuses two assets sharing one feed id, and the SDK's offline mark table has no row for the twins: a front end falling back to offline marks misprices them badly when a live feed read fails.

## 3. Claiming

1. Open `/faucet`. `?token=USDC` deep-links one asset to the top of the list.
2. First visit only: press **Enable faucet**. The app checks your mainnet balances on Ethereum, BSC, Base and Arbitrum against a 5 USD minimum (marked at fixed rates, ETH 3000 and BNB 600), then sends `register()`, which sets `whitelisted[msg.sender] = true`.
3. Press **Claim** on a row. The app sends `claim(address token)` and re-reads whitelist and remaining allowance from the receipt.

The button is live only when the faucet is deployed, you are whitelisted, and
`remaining(user, token) > 0`. When you are disconnected or on another network the same click connects
or switches chain instead. Reads are pinned to the deployment's chain id regardless of the wallet's
current network.

## 4. What `claim` enforces

`claim` reverts:

- `NotWhitelisted` if you never registered;
- `NoCap` if `dailyCap[token] == 0`;
- `CapExhausted` if the day's allowance is spent.

Otherwise it resets the day bucket if the day rolled, transfers the **full** remaining amount, and writes `claimedToday = cap`.

- One claim per token per day, all-or-nothing: the write is `claimedToday = cap`, not an increment, so there is no partial second claim.
- The day is `block.timestamp / 1 days`, a UTC-midnight roll rather than 24 hours since your last claim. Claim at 23:55 UTC and you can claim again five minutes later.

## 5. Caps

The cap is derived, not typed: a leg's USD depth target divided by `FAUCET_CAP_DIVISOR` (2% of the
target per day), converted at the ceremony mark into the token's own decimals.

| Parameter | Value | Source |
|---|---|---|
| `FAUCET_CAP_DIVISOR` | 50 | deploy script |
| `FAUCET_PREFUND_CLAIMS` | 200 | deploy script |
| `seedUsdPerLeg` | 50,000 USD | the instance's risk-params file |
| `unmintableSeedUsdPerLeg` | 400 USD | the instance's risk-params file |
| Enable-gate minimum | 5 USD across chains 1 / 56 / 8453 / 42161 | front end |

At a 50,000 USD seed that is roughly 1,000 USD per mintable leg per day, and 200 days of cap are
placed in the faucet at deploy.

`dailyCap(token)` read on chain is the enforced limit. The per-token daily figures in the app's
config are a display fallback used only when that read is missing or zero, and several of them are
stale relative to the derived cap.

## 6. Limits

- The 5 USD check is client-side only. `register()` is permissionless on chain. The real backstops are the daily caps and the owner's `setWhitelisted(user, false)` revoke.
- Issuer tokens are never claimable: excluded from the list and skipped by the deploy's funding step. Where the chain's gas token is one of them, a brand-new wallet cannot bootstrap gas from the faucet and must arrive already funded.
- An uncapped token looks identical to an exhausted one. With `dailyCap == 0`, `remaining()` returns 0 and the row reads *Done today*; a claim in that state reverts `NoCap`. Read `dailyCap` directly to tell the two apart; a token listed out of band should have its cap verified on chain rather than assumed.
- An empty faucet reverts inside `safeTransfer` with no dedicated error. Only `fund()` refills it.

---

## 7. Related documentation

- [Pool Legs](/docs/4-4-pool-legs): what a listing is, the base leg, and why the hub is a twin
- [Asset Registry](/docs/2-2-asset-registry): per-asset addresses, feeds, ERC-20 oddities
- [Contract Addresses](/docs/2-1-contract-addresses): the `TestnetFaucet` address per chain
