Super Agent · Deep dive

Technical details —Super Agent Portal

Delve into the execution mechanism of the Super Agent portal: the essence is onefork of Agent Portal(CRA React 18), adding an Agent administration layer, distributing credits to agents, game mapping per-agent, 2-level hierarchy tree and Service Worker (PWA). Read the following overview report05-superagent.html.

CRA + Craco 7 TypeScript 4.9 React Router v6 Axios + JWT Workbox PWA

Stack & holistic thinking

Before reading the code, grasp the 6 "strange" features of this app. Much of it comes from one truth:This repo is a clone of Agent Portal, was expanded rather than rewritten from scratch — many of the names in the code are still "Agent".

🧬

App is a fork of Agent Portal

package.jsonstill recorded"name": "kioskgaming-agent", README.mdwrite "Agent Portal", and all type/hooks use the prefixAgent* (AgentProfile, useAgentTransactions…). Super Agent = Agent + agent management layer.

🔌

Useaxios, not fetch

Different from Kiosk Frontend. A singletonclient in services/api.tswith interceptor attaching Bearer token and catching 401.timeout: 15000.

🌐

Real routing using React Router v6

Not a state machine like Kiosk. HaveBrowserRouter, <Routes>, deep-link, ProtectedRoutewrap every route except/login.

🚪

401 = logout, NO refresh

Login paid for everythingrefreshTokenbut appDon't save or use it. Encounter 401 → fire eventauth:unauthorizedAuthProviderdelete localStorage. Expires = re-login.

📱

PWA: the only repo with Service Workers

service-worker.js + Workbox. Register only in production (NODE_ENV==='production'). Supports offline, precache, image cache.

🎨

Tailwind v3 + theme cam

Havetailwind.config.js(HSL CSS vars). The actual UI uses tonescam (orange-500/600), not yellow — yellow is just the theme of this document.

💡
Read in what order?

Newbies should open:src/App.tsx (routes) → src/services/api.ts(all endpoints) →src/hooks/useAuth.tsx (auth) → src/pages/SuperAgentManagementPage.tsxandManagedAgentDetailPage.tsx(2 SA exclusive pages). These 5 files are ~75% "different from Agent".

Directory structuresrc/

Standard CRA tree:pages/ for route component, components/ for UI con, services/api.tscentralize all HTTP. ★ files are exclusive to Super Agent.

src/ ├── App.tsx # ★ Declare router + ProtectedRoute ├── index.tsx # StrictMode + serviceWorkerRegistration.register() ├── pages/ │ ├── SuperAgentManagementPage.tsx # ★ 3 section: agents | shops | players │ ├── ManagedAgentDetailPage.tsx # ★ 4 tab: info/games/shops/players │ ├── DashboardPage.tsx # stat cards + HierarchyMiniTree │ ├── AgentTransactionsPage.tsx # wallet history (shares Agent name) │ ├── /createManagedAgentCashCredits — POST /super-agent/agents/:id/cash-credits.tsx # buy credits from the platform (0x/crypto) │ ├── AgentPaymentReturnPage.tsx # pages returned after payment │ ├── ShopsPage / ShopDetailPage # use /agent/shops (DO NOT mount in routes) │ └── SettingsPage, SendBroadcastPage… # legacy / unconnected route ├── components/ │ ├── auth/ LoginPage.tsx, ProtectedRoute.tsx │ ├── layout/ AppLayout.tsx # sidebar + topbar + bottom nav (mobile) │ ├── credits/ │ │ ├── SuperAgentAgentCreditTrigger.tsx # ★ grant credits SA→Agent │ │ ├── CreditPurchaseTrigger / Modal # buy credits from the platform │ │ └── SellCreditsCashToAgentTrigger.tsx # issue credits to shop (shared) │ └── dashboard/ HierarchyMiniTree.tsx # ★ 2-level tree SA→Agent→Shop ├── services/ api.ts # ★ ALL endpoints (axios) ├── hooks/ useAuth.tsx # AuthContext (login/logout/restore) ├── types/ index.ts # SuperManagedAgent/Shop/Player + Agent* ├── constants/ index.ts # STORAGE_KEYS, API_BASE_URL ├── config/ env.ts # ENV (REACT_APP_*) ├── utils/ # costFormat, agentWalletLedger, sanitizeUser… ├── service-worker.js # ★ Workbox (precache + image SWR) └── serviceWorkerRegistration.ts # register SW (prod only)
🏷️
The name "Agent" everywhere is intentional (fork legacy)

Don't be mistaken:AgentProfile, AgentDashboardData, AgentTransactionsPage, fetchAgentTransactions… all serveSuper Agent. Type SuperManagedAgent/Shop/Playernew is "agent/shop/player that SA manages".

Startup Screen & Register Service Worker

What happens when the browser opens the app. Different from Kiosk Frontend, this is a pure CRA SPA client.

1
index.tsx render <App/> in StrictMode

Mount React 18 createRoot; then callserviceWorkerRegistration.register()at the end of the file.

2
App.tsx builds the Provider + Router tree

<AuthProvider><BrowserRouter><Routes> + <Toaster/> (react-hot-toast).

3
AuthProvider restores the session

Readagent_auth_token & agent_user_datafrom localStorage; if valid →isAuthenticated=true. While reading,isLoading=true.

Navigate by route

/ redirect /dashboard. Not logged in →ProtectedRoutepush back/login(holdstate.fromto go back).

src/index.tsxtsx
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<React.StrictMode><App /></React.StrictMode>);

// SW only actually registers when NODE_ENV === 'production'
serviceWorkerRegistration.register();
🧭
Why is SA a SPA client and Kiosk is a state machine?

Kiosk is a 1-session touch screen, no URL needed. Super Agent is a multi-site administration tool, needs deep-links, back/forward, bookmarks — should use a real router. Each role chooses the appropriate model.

Hierarchy SuperAgent → Agent → Shop → Player

This is the central concept. Super Agent standstopB2B chain: buy credits from platform, create & Manage many Agents, each Agent manages many Shops, each Shop has many Players.

Platform Super Agent Agent Shop Player
👑

What SA can do (via /super-agent/*)

  • Create / Suspend / Unsuspend / Terminate Agent
  • Fixcost_rateby Agent
  • Grant credits to Agent (debit SA wallet)
  • Configure game provider per-Agent
  • Move Shop to another Agent, Move Player to another Shop
  • See all agents/shops/players + statistics
🔁

"Similar" to the pair Agent→Shop

The relationship SA→Agent is a copy of the relationship Agent→Shop one level up: same formulacash = credits × cost_rate, same active/suspended/terminated lifecycle, same "parent wallet loaded to child wallet" model.

GrantType in codeData source
Super Agent (me)AgentProfilelogin + /super-agent/dashboard
Agent (con)SuperManagedAgentGET /super-agent/agents
Shop (nephew)SuperManagedShopGET /super-agent/shops(attachedagent)
Player (great-grandson)SuperManagedPlayerGET /super-agent/players(attachedshop_name, agent_name)
📐
"Flat reference" relationship

The API does not return nested trees. SA fetches 3 flat lists and then automatically joins them at the client:shop.agent?.idconnect shop with agent,player.shop_id/player.agent_idConnect player to shop/agent.HierarchyMiniTreegroup together byuseMemo.

Routing & ProtectedRoute

React Router v6 in App.tsx. All routes (except/login) wrapped inProtectedRoute; Route mismatch redirects back/dashboard.

PathComponentNote
/loginLoginPagePublic; has Turnstile captcha (depending on env)
/Navigate/dashboard
/dashboardDashboardPageStat cards + HierarchyMiniTree
/agentsSuperAgentManagementPage section="agents"SA onlylist + create agent
/agents/:agentIdManagedAgentDetailPageSA onlydetails, 4 tabs
/shopsSuperAgentManagementPage section="shops"All shops + move
/playersSuperAgentManagementPage section="players"All players + move
/agents/newCreateManagedAgentPageCreate managed agent
/online-transactionsOnlineBalanceTransactionsPageLedger USD + crypto withdrawal
/cash-transactionsCashTransactionsLedgerPageCash transactions
/payment/returnAgentPaymentReturnPageCallback mua credit online
/transactionsAgentTransactionsPageHistory of SA wallet
/payment/returnAgentPaymentReturnPageThe page returns to the payment gateway
*Navigate/dashboard
src/components/auth/ProtectedRoute.tsxtsx
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <PageLoadingIndicator variant="section" />;
if (!isAuthenticated)
  return <Navigate to="/login" replace state={{ from: location }} />;
return <>{children}</>;
⚠️
"SA only" is just UI distinction, not route authorization

ProtectedRoutejust check "logged in",Are notrole check. The fact that a route is "SA exclusive" is guaranteed bybackend via prefix /super-agent/*+ SA tokens. Frontend does not have role-based guards.

services/api.ts— API call center

A single file contains all backend call functions, exported for each operation. Component imports the function to use, automatically manages loading/error.

PATTERNAxios singleton + request/response interceptor
  • Base URL: REACT_APP_API_BASE_URL || 'http://localhost:3001/api' (prefix /apiavailable in the base)
  • Request:readagent_auth_tokenfrom localStorage → addAuthorization: Bearer <token>
  • Response 401: window.dispatchEvent(new Event('auth:unauthorized'))then reject — DO NOT refresh
  • Timeout: 15000ms
  • Envelope:standard response{ success, data?, message? }
src/services/api.ts (shortened)ts
const client = axios.create({ baseURL: API_BASE_URL, timeout: 15000 });

client.interceptors.request.use((config) => {
  const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

client.interceptors.response.use((r) => r, (error) => {
  if (error.response?.status === 401)
    window.dispatchEvent(new Event('auth:unauthorized'));
  return Promise.reject(error);
});
🚨
The app still calls multiple endpoints/agent/*(Not/super-agent/*)

Because it is a fork, legacy features still call the old prefix:fetchShops()/agent/shops, fetchAgentPlayers()/agent/players, broadcasts/in-app-popups → /agent/broadcasts, /agent/in-app-popups/*, low-balance → /agent/settings/low-balance-warning, issue credits to the shop →POST /agent/credits/distribute. The backend must accept SA tokens for both prefixes. When debugging 403/404, always check the actual prefix of the function.

Login flow & JWT

One-step login with email + password (with optional captcha). LocalStorage storage token. No OTP code like Kiosk, no refresh token.

1
Submit form login

loginWithEmailPassword(email, password, captchaToken?)POST /super-agent-auth/login. IfREACT_APP_TURNSTILE_ENABLEDTo enable it, a Cloudflare Turnstile token is required.

2
Read profiles flexibly

Takeres.data.agent or res.data.superAgent(backend returns different keys depending on version), standardizedtoAgentProfile()(remove passwordHash).

3
Save session

login(profile, accessToken) ghi agent_auth_token + agent_user_datago to localStorage; shootagent-auth-token-changed. refreshToken is ignored.

4
Navigation

navigate(from)— return to the previously blocked page (vialocation.state.from) or/dashboard.

🔑

Save & restore

localStorage: agent_auth_token, agent_user_data. Khi reload, AuthProviderread again; JSON error → selflogout().

🚪

Logout = pure client

logout()just delete localStorage + reset state,do not call the API. Also triggered automatically when receiving eventsauth:unauthorized (401).

🐛
2 easy points about auth

(1)There is no refresh token → when the access token expires, the user will be kicked out to login immediately (all running requests fail 401).(2) STORAGE_KEYSusecommon namewith Agent Portal (agent_auth_token). If you run SA and Agent app on the same origin (same host:port) they willoverwrite each other's tokens. The app avoids this by using another port (default SA3003).

State management

Minimalist: only 1 Context (Auth). All business data is local state in each page, loaded byPromise.all when mount.

What's up?
  • AuthContext (useAuth): user, token, isAuthenticated, isLoading
  • Local stateper page: agents/shops/players, loading, modal, search/filter
  • react-hook-form for form login
  • react-hot-toastfor notifications (instead of notificationService)
  • useMemo for filter/group (filteredAgents, shopsByAgent…)
Do not have
  • Redux / Zustand / Jotai
  • React Query / SWR (every fetch is handwritten)
  • Cross-page cache — each page fetches itself
  • Optimistic update — always call again after mutateload()
Parallel data loading sample (SuperAgentManagementPage)tsx
const [agentsRes, shopsRes, playersRes] = await Promise.all([
  fetchManagedAgents(),   // GET /super-agent/agents
  fetchManagedShops(),    // GET /super-agent/shops
  fetchManagedPlayers()    // GET /super-agent/players
]);
setAgents(agentsRes.data ?? []); /* …setShops, setPlayers */

SuperAgentManagementPage — 3 section in 1 component

One component used for 3 routes (/agents, /shops, /players) via prop section. Note: these are 3 separate routes, not tabs— move section = change URL, component re-mount & fetch all 3 lists again.

sectionMain actionSearch / Filter
agentsCreate agent (createManagedAgent), Suspend/Unsuspend, Terminate, link to detailsSearch name/email/phone → filteredAgents
shopsMove shop to another agentSearch + filter by agent → filteredShops
playersMove player to another shopSearch + filter by shop → filteredPlayers

Create Agent — client-side validation

SuperAgentManagementPage.tsxtsx
const PASSWORD_MIN = 8;
const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;

// % input (0–100) → decimal rate (0–1), rounded to 4 digits
const toRate = (percent: string) =>
  Math.min(1, Number((Number(percent) / 100).toFixed(4)));

createManagedAgent({ name, email, phone, password,
  cost_rate: toRate(form.costPercent),
  can_create_shop_popup, can_manage_shop_popup_permission,
  default_shop_can_create_player_popup });
📞

Phone & popup permissions

Phone input usedreact-phone-number-input(US default). 3 popup/broadcast authorization checkboxes for agents:can_create_shop_popup, can_manage_shop_popup_permission, default_shop_can_create_player_popup.

⌨️

Terminate requires typing confirmation

window.prompt('Type "terminate"…'); must type the correct letterterminatejust called the API. This is an anti-mistake mechanism, not a beautiful modal.

Move Shop / Move Player

Move Shop

PATCH /super-agent/shops/{id}/move body { target_agent_id }. Dropdown disableThe shop's current agent does not "move into itself".

Move Player

PATCH /super-agent/players/{id}/move body { target_shop_id }. Dropdown appearsAll shops have the name of the owner agentto have context; disable the current shop.

🔧
The actual HTTP verb is PATCH (not PUT/POST)

The overview document says PUT/POST for suspend/terminate/move, but the code is usedPATCHfor suspend/unsuspend/terminate/rates/move. Separately create agent & grant credits for usePOST; game-mappings usedPUT. Always take the verb fromapi.ts.

ManagedAgentDetailPage— 4 tabs controlled by URL

Route /agents/:agentId. Tab saved in query?tab=(hold when F5). Different from the management page (using routes), this is a real tab in the same URL.

info · games · shops · players
ManagedAgentDetailPage.tsxtsx
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = isAgentDetailTab(searchParams.get('tab'))
  ? searchParams.get('tab') : 'info';

// info is default → remove param 'tab' from URL for brevity
const setActiveTab = (t) => setSearchParams((p) => {
  const n = new URLSearchParams(p);
  t === 'info' ? n.delete('tab') : n.set('tab', t);
  return n;
}, { replace: true });
ℹ️

Tab Info

Status badge, credit_balance, Credit button (SuperAgentAgentCreditTrigger), Suspend/Terminate, and fixcost rateinline. The hint "must be higher than SA's rate" displays the wordsuperAgentProfile.costRate.

🎮

Tab Games

Parallel downloadfetchManagedAgentGameMappings + fetchPublicGameProviders. Select All or Selected. See section 10.

🏪

Tab Shops

Filter clients:shops.filter(s => s.agent?.id === agentId). Show total/active + shop table.

👤

Tab Players

Filter clients:players.filter(p => p.agent_id === agentId). Show total/active + player table.

🐢
There is no "1 agent" endpoint — fetch all 3 lists and then filter

load()callfetchManagedAgents/Shops/Players(all) already.find()/.filter() by agentIdin client. For large systems, this is where bandwidth is wasted; If optimization is needed, suggest additional backendGET /super-agent/agents/:id.

Configure Game Provider for each Agent

SA limits the set of games each agent is allowed to use. Inherited logic: the agent only sees games in the SA's "pool"; save empty array = "inherit all".

1
Download mapping + game list

Promise.all([fetchManagedAgentGameMappings(agentId), fetchPublicGameProviders()]). Mapping paidgame_ids(of agent) +super_agent_game_ids (pool SA).

2
Filter by SA pool

visibleGameProviders: IfparentGamePool.length > 0only shows games belonging to that pool; if pool is empty → show all providersstatus !== 'maintenance'.

3
Select mode

game_idsempty → modeAll(inherit pool SA); has element → modeSelectedwith checkboxes.

4
Save

putManagedAgentGameMappings(agentId, payload)withpayload = mode==='all' ? [] : selectedGameIds.

MethodEndpointPurpose
GET/super-agent/agents/{id}/game-mappingsPay{ game_ids, super_agent_game_ids }
PUT/super-agent/agents/{id}/game-mappingsBody { game_ids }[]= inherit SA
GET/game-providersPublic category, response{ data: { items[] } }
🎯
Category endpoint is/game-providers

Not/public/game-providersas an overview document. The data is insidedata.items[](each item:{ id, name, code, status? }), is filtered outstatus === 'maintenance'before rendering.

Credits Distribution

Two cash flows: (1) SAmuacredits from platform; (2) SAgrantcredits down Agent. Leveling down to Shop reuses the Agent Portal component.

Platform → mua → SA wallet → distribute → Agent wallet → distribute → Shop Player

SA → Agent: SuperAgentAgentCreditTrigger

Component is located in the Info tab of the agent details. Open modal → download SA balance (fetchDashboard) → enter amount → preview cash → submit.

components/credits/SuperAgentAgentCreditTrigger.tsxtsx
// Preview: cash that the agent must pay to SA
const effectiveCostRate = Math.min(1, Math.max(0, Number(agentCostRate ?? 0)));
const cashToCollect = credits * effectiveCostRate;
// eg: 100 credits × 0.95 = $95.00 (agent pays SA)

await distributeCreditsToManagedAgent(agentId, amount);
// POST /super-agent/agents/{id}/cash-credits — sell credits to agent (cash)
💰

SA buys credits from the platform

purchaseCredits(amount, paymentMethod?)POST /super-agent/credits/purchase. /createManagedAgentCashCredits — POST /super-agent/agents/:id/cash-creditssupports 0x/crypto (LinkMePay) payment methodpaymentUrlto redirect.

🏪

Grant credits to Shop (shared use)

distributeCredits(shopId, amount)POST /agent/credits/distribute body { shop_id, amount }— this is the endpointby Agent, not super-agent.

🔗
Endpoint distribute is/credits/distribute

createManagedAgentCashCreditscallPOST /super-agent/agents/{id}/cash-credits. WithdrawCreditsFromAgentTriggerrevoke credits. Recipe:cash = credits × agent_cost_rate.

HierarchyMiniTree— 2-level tree + statistics

Unlike Agent Portal (1 level Agent→Shop), SA sees 2 levels:SA → Agents → Shops, with statistics on deposits/withdrawal/users by date range.

components/dashboard/HierarchyMiniTree.tsxtsx
type Props = { superAgentName: string;
  agents: SuperManagedAgent[]; shops: SuperManagedShop[] };

// Group shops by agent (Map<agentId, shops[]>), sort by name
const shopsByAgent = useMemo(() => { /* ... */ }, [shops]);

// Statistics by period (default 1 most recent month)
fetchSuperAgentShopHierarchyStats({ from, to });
// GET /super-agent/shops/stats?from=&to=
🌳

Interact

Root SA expand/collapse; Each agent expands separately to view the shop as a card grid;StatusDoteach node (active/suspended/terminated); shop card link to/agents/{agentId}.

📊

Statistical

Date range from/to (default 30 days). Summary row total deposits/withdrawal/new users/total users; Each shop has its own statsstats.shops[] map by id.

📅
Endpoint stats are/super-agent/shops/stats

Not/super-agent/shop-hierarchy-stats. Response: { range, summary, shops[] }; Every shop has itagent_id, agent_name, total_users, new_users, total_deposit, total_withdrawal.

Service Worker (Workbox)

The only repo in the ecosystem that has SW — suitable for the "regional manager" role needed on tablet/mobile.Only runs in production.

src/service-worker.jsjs
clientsClaim();                          // SW just claimed right away, no waiting
precacheAndRoute(self.__WB_MANIFEST);   // precache asset build-time

// SPA: navigate request → return index.html to React Router
registerRoute(isNavigate,
  createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'));

// .png image from the same origin → StaleWhileRevalidate, maximum 100 entries
registerRoute(isPng, new StaleWhileRevalidate({
  cacheName: 'images',
  plugins: [new ExpirationPlugin({ maxEntries: 100 })] }));

Precache

__WB_MANIFESTinjected by Workbox at build time; Cache JS/CSS/static fonts.

SPA fallback

Navigate (not file, not start/_) → index.html.

SKIP_WAITING

Listen to messagesSKIP_WAITINGself.skipWaiting()for quick updates.

🧪
Dev doesn't see SW working

serviceWorkerRegistration.register()run only whenprocess.env.NODE_ENV === 'production'. Runnpm start(dev) willAre notRegister for SW — if you want to test PWA, you mustnpm run buildthen serve directorybuild/.

Config & environment variable

Stay focusedsrc/config/env.ts + src/constants/index.ts. Every variable must have a prefixREACT_APP_(CRA convention) can only be used on the client.

VariableDefaultMeaning
REACT_APP_API_BASE_URLhttp://localhost:3001/apiBase backend URL (included/api) — shares backend with admin
REACT_APP_APP_NAMEKiosk Gaming — AgentApp name (legacy fork, still says "Agent")
REACT_APP_TURNSTILE_ENABLEDfalseTurn on Cloudflare Turnstile at the login screen
REACT_APP_TURNSTILE_SITE_KEYSite key Turnstile
PORT3003Avoid touching admin (3000) — important to avoid clashing localStorage tokens
📄
Available.env.example

Copy .env.example.env. Backend needs to be setAGENT_FRONTEND_URLpoint to this app (eghttp://localhost:3003) to redirect the payment back to the correct one/payment/return. CORS: add hostname to the tabledomains(type admin) orDISABLE_CORS=true when dev.

Summary API Endpoints (according to real code)

Excerpted fromservices/api.ts. Note the double prefix:/super-agent/*(SA feature) mixed/agent/*(inherited from fork).

MethodEndpointJawPurpose
POST/super-agent-auth/loginloginWithEmailPasswordLogin SA
GET/super-agent/dashboardfetchDashboardBalance + shop/agent count
GET/super-agent/agentsfetchManagedAgentsList of agents
POST/super-agent/agentscreateManagedAgentCreate agents
PATCH/super-agent/agents/{id}/suspendsuspendManagedAgentSuspend
PATCH/super-agent/agents/{id}/unsuspendunsuspendManagedAgentRemove suspension
PATCH/super-agent/agents/{id}/terminateterminateManagedAgentTermination ({confirm:true})
PATCH/super-agent/agents/{id}/ratesupdateManagedAgentRateEdit cost_rate (plural:rates)
POST/super-agent/agents/{id}/cash-creditscreateManagedAgentCashCreditsSell ​​SA credits→Agent (cash)
POST/super-agent/agents/{id}/credits/withdrawwithdrawCreditsFromManagedAgentRecover credits
GET/super-agent/agents/{id}/game-mappingsfetchManagedAgentGameMappingsGet the agent's game
PUT/super-agent/agents/{id}/game-mappingsputManagedAgentGameMappingsSave game ({game_ids})
GET/super-agent/shopsfetchManagedShopsAll shops
PATCH/super-agent/shops/{id}/movemoveManagedShopMove shop ({target_agent_id})
GET/super-agent/shops/statsfetchSuperAgentShopHierarchyStatsStatistical hierarchy
GET/super-agent/playersfetchManagedPlayersAll players
PATCH/super-agent/players/{id}/movemoveManagedPlayerMove player ({target_shop_id})
POST/super-agent/credits/purchasepurchaseCreditsBuy credits from the platform
GET/super-agent/transactionsfetchAgentTransactionsHistory of SA wallet
GET/game-providersfetchPublicGameProvidersPublic game directory
POST/agent/credits/distribute /agent/*distributeCreditsGrant credits to shop (inheritance)
GET/agent/shops, /agent/players /agent/*fetchShops, fetchAgentPlayersUsed by ShopsPage/ShopDetailPage
PATCH/agent/settings/low-balance-warning /agent/*patchAgentLowBalanceWarningLow balance warning
GET/agent/broadcasts, /agent/in-app-popups/* /agent/*broadcasts / popupsNotifications (inherited)

Cookbook for new devs

Common tasks and how to do them correctly according to project conventions.

HOW-TOAdd a new backend endpoint
  1. Add export functionsrc/services/api.ts, callclient.get/post/patch/put(...)(self-attached token).
  2. Declare the response type insrc/types/index.ts (by envelope { success, data?, message? }).
  3. Component imports functions, self-managementloading/errorequaluseState; error reporttoast.
HOW-TOAdd a new route
  1. Create internal pagesrc/pages/, wrap the content in<AppLayout>.
  2. Declare<Route> in App.tsx, wrap<ProtectedRoute>If you need to log in.
  3. Add the linkNAV_ITEMS in components/layout/AppLayout.tsx(putprimary:trueif you want to appear at the bottom nav mobile).
HOW-TOAdd tab to ManagedAgentDetailPage
  1. Add values ​​to the arrayAGENT_TABS+ updateisAgentDetailTab().
  2. Add tab button (callsetActiveTab) and render block{activeTab === '...' && (...)}.
  3. If the tab needs to be fetched separately, useuseEffectdependent[activeTab, agentId]+ flagcancelledto avoid racing.
HOW-TOWork with cost rate
  1. Backend saveddecimal rate 0–1(0.05 = 5%). UI input %.
  2. % → rate: toRate() / percentInputToRate() (clamp 0–100, /100, toFixed(4)).
  3. rate → %: toPercent() (×100, toFixed(2)) orformatCostRate() (Intl.NumberFormat style:'percent').

Common traps (read before fixing)

TrapDetails & how to handle
Thought this was a separate appTo befork of Agent Portal: package.jsonname "kioskgaming-agent", README says "Agent". Type/hook prefixAgent*serving SA.
Clash localStorage tokenSTORAGE_KEYS = agent_auth_token/agent_user_dataUsed together with Agent app. Running at the same origin will override each other — separate ports (SA3003).
Expect token refreshLogin returns refreshToken but app DOES NOT save/use. 401 = logout now. There is no renewal mechanism.
Sai HTTP verbsuspend/unsuspend/terminate/rates/move usePATCH; create & credits uses POST; game-mappings uses PUT. Get the verb fromapi.ts, don't trust the dashboard.
Wrong endpoint nameReality:/super-agent/shops/stats, /super-agent/agents/{id}/rates, .../credits/distribute, game category/game-providers.
Dual prefix /agent vs /super-agentMany legacy features still call/agent/*(shops, players, broadcasts, popups, credits→shop, settings). The backend must receive SA tokens for both.
Agent details load slowlyManagedAgentDetailPagefetch all 3 lists and then filter clients. Do not haveGET /super-agent/agents/:id.
Section ≠ tab/agents /shops /playersare 3 different routes (same component), NOT tabs. Only agent details have a tab (?tab=).
SW not running when devSW only registers in production. If you want to test PWA, you mustnpm run build + serve build/.
Cost rate unitBackend = decimal 0–1; UI = %. Always usetoRate/toPercent. Enter 5 → save 0.0500.
Login profile keyReadres.data.agentORres.data.superAgent— The backend returns a different key depending on the version.
Captcha enabled/disabled according to envTurnstile is only required whenREACT_APP_TURNSTILE_ENABLED='true'& There is a site key. Forgetting to configure will block login when enabled.