Configuration
one document, four layers.
TIDE reads its operator settings from one JSON document. You edit it in the in-app operator panel, host it yourself for many installs, or both. The only build setting is your domain.
01Two places, one rule
| Where | What goes there | Change needs |
|---|---|---|
Build setting --dart-define=TIDE_HOST=… | Your domain. Site, docs, terms, privacy, support e-mail and referral links are built from it. | A new build |
| Operator config document | Fee, referrals, cashback, feature switches, names, links, RPC endpoint and API keys | Operator panel → Save and apply, or a new hosted document. No rebuild. |
TIDE has no .env and no other --dart-define values. TIDE_HOST is the only one in the code (lib/core/config/app_endpoints.dart). Keys are never compiled in: RuntimeKeys.assertShipsKeyless() runs at start-up and fails a debug build if one ever is.
02Your domain (TIDE_HOST)
Default: tide.mikodes.com. Pass your own host (no https://) on every run and build:
flutter run -d chrome --dart-define=TIDE_HOST=app.example.com
flutter build web --release --dart-define=TIDE_HOST=app.example.com
| Derived value | Result for app.example.com |
|---|---|
| Site | https://app.example.com |
| Docs link in the app | https://app.example.com/docs |
| Terms / privacy | https://app.example.com/terms, /privacy |
| Support e-mail | support@app.example.com |
| Referral link | https://app.example.com/r/<code> |
If you set Support email, Website, Terms URL or Privacy URL in the operator panel, the app uses those and ignores the derived value. The Android https referral link has its own host in AndroidManifest.xml (Rebranding → Deep links).
03The four layers
Weakest first. The strongest layer that supplies a value wins that value, leaf by leaf (lib/core/config/config_layers.dart).
| Layer | What it is |
|---|---|
| 1. Built-in default | AdminConfigModel.defaults, compiled in. No fee, no payouts, no copy execution. |
| 2. Cached remote document | The last hosted document this device fetched. Used only when a fetch fails. |
| 3. Remote document | The document you host, fetched at start-up. Not wired in the shipped build (how to wire it). |
| 4. Operator panel | What was saved on this device. Stored sparse: only values you changed. |
Operator panel → In force right now lists every value the app is running on and which layer it came from.
04The document
This is the full shape, with the shipped defaults. Operator panel → Copy as JSON gives you the same shape with your values (only the ones you changed).
{
"config_version": 1,
"app_name": "TIDE",
"tagline": "Swap on Solana. See what other wallets did.",
"support_email": "",
"website_url": "",
"terms_url": "",
"privacy_url": "",
"announcement": "",
"min_app_version": "",
"default_slippage_pct": 1.0,
"admin_locked": false,
"flags": {
"swap_enabled": true,
"feed_enabled": true,
"watchlist_enabled": true,
"profiles_enabled": true,
"realised_standings_enabled": true,
"push_enabled": false,
"referral_enabled": false,
"cashback_enabled": false,
"evm_chain_enabled": false,
"copy_execution_enabled": false
},
"fee": {
"fee_enabled": false,
"fee_bps": 0,
"min_fee_usd": 0,
"solana_fee_wallet": "",
"evm_fee_wallet": ""
},
"referral": {
"referral_enabled": false,
"tiers": 0,
"tier1_share_bps": 0,
"tier2_share_bps": 0,
"payout_threshold_usd": 0
},
"cashback": {
"cashback_enabled": false,
"tiers": []
},
"keys": {
"helius_api_key": "",
"zerox_api_key": "",
"solana_rpc_url": "",
"evm_rpc_url": ""
},
"extra_restricted_regions": []
}
An unknown flag key is ignored. A region code that is not two letters is dropped. Every rate is clamped to its code cap on the way in (caps).
05Feature switches
| Key | Default | What it does |
|---|---|---|
swap_enabled | on | Swap screen |
feed_enabled | on | Feed of followed wallets |
watchlist_enabled | on | Following wallets |
profiles_enabled | on | Profile of a followed wallet |
realised_standings_enabled | on | The Realised tab. Off removes the tab and every route into it. |
push_enabled | off | Push registration, if you wire a token provider (push) |
referral_enabled | off | Master switch for the referral programme |
cashback_enabled | off | Master switch for volume cashback |
evm_chain_enabled | off | Not active in this version Base is scaffolded but no quote source is wired, so the switch has no effect. |
copy_execution_enabled | off | Copy execution module (read first) |
06Endpoints and keys
Key in keys | Purpose | Get it at |
|---|---|---|
solana_rpc_url Recommended | Your Solana RPC endpoint. Replaces the public one, and makes SPL balances load on the web build. | Any Solana RPC provider |
helius_api_key | Not active in this version Stored and shown as set, but no network call reads it in 0.1.0. To use Helius, paste the full URL https://mainnet.helius-rpc.com/?api-key=<your-key> into solana_rpc_url instead. | helius.dev |
zerox_api_key | Not active in this version Reserved for the EVM chain, which has no quote source. | |
evm_rpc_url | Not active in this version Same reason. Not shown in the panel. |
Anything you put in a hosted document can be read by anyone who finds its URL, and every app build can read it. Use RPC URLs and keys that are restricted in your provider's dashboard (allowed origins, rate limits). A support dump made with toJsonRedacted() shows keys only as [set].
07Remote document for many installs
The panel changes only the device it runs on. To give every install the same configuration:
- Configure one buildSet everything in the operator panel.
- Copy the documentOperator panel → Copy as JSON.
- Add the lockAdd
"admin_locked": trueto the document, so users cannot change the fee on their own phones. - Host itAny HTTPS static file works, for example
https://<your-domain>/tide-config.json. - Wire a fetcherThis is the one code change in the install. In
lib/main.dart, passfetchRemotetoAdminConfigService:// lib/main.dart — add at the top: import 'dart:convert'; import 'package:http/http.dart' as http; // replace AdminConfigService(store: adminStore) with: AdminConfigService( store: adminStore, fetchRemote: () async { final r = await http.get(Uri.parse('https://<your-domain>/tide-config.json')); if (r.statusCode != 200) return null; return jsonDecode(r.body) as Map<String, dynamic>; }, ), - Rebuild and releaseFrom then on you change the hosted file; the app reads it on each start.
With admin_locked the panel becomes read-only on every install and says why. Without it, the device's own panel values win, which is what you want while you are the only user. The snippet passes flutter analyze against 0.1.0; the URL is yours to fill in. Firestore or Remote Config work the same way: return the document as a map.
08Push notifications and Firebase (optional)
TIDE works without Firebase and ships no Firebase files. Push is a keyless hook: PushNotificationService in lib/core/notifications/push_notification_service.dart accepts a tokenProvider you supply. To use Firebase: create your own project, add the firebase_core and firebase_messaging packages, run flutterfire configure, initialise Firebase in lib/main.dart, and pass tokenProvider: () => FirebaseMessaging.instance.getToken(). Sending pushes needs a server or the Firebase console; TIDE includes none. These steps are from docs/FIREBASE-SETUP.md and were not run for these docs.
If you store config in Firestore, keep it read-only for clients and never store a wallet address next to a referral code (why).
09Where data is stored
| Data | Where |
|---|---|
| Seed phrase and keys | Platform secure storage (iOS Keychain, Android Keystore; browser storage on web), encrypted by TIDE with AES-256-GCM first |
| Operator panel values | On the device (AdminConfigStore) |
| Watchlist, notifications, preferences, copy rules | On the device |
| Payout ledger and traded volume | On the device; export as CSV from the Earn screen |
There is no server and no database. Nothing is uploaded.