# Browser আর mobile app থেকে Tokens call করা

> Web page বা mobile app-এ Tokens API key কেন কখনোই রাখবেন না, আর কী করলে কাজ হয়: মাঝখানে নিজের backend। streaming সহ Next.js ও Express-এর চালু উদাহরণ, সাথে mobile-এর পদ্ধতি।

Web page বা mobile app সরাসরি Tokens-কে call করতে পারে না। user-এর device-এ যা-ই পাঠান, user সেটা পড়তে পারে, আর একটা Tokens key মানে আপনার টাকা খরচ করার ক্ষমতা। browser-এ আরেকটা বাধাও আছে: Tokens তার response-এ CORS header পাঠায় না, তাই page-এর JavaScript উত্তরটা পড়তেই পারে না।

সমাধান হলো নিজের একটা ছোট backend। app আপনার backend-কে call করে। backend দেখে নেয় কে চাইছে, তারপর key দিয়ে Tokens-কে call করে, আর উত্তর app-এ ফিরিয়ে দেয়। এই পেজে Next.js আর Express-এ সেই backend দেখানো হয়েছে, streaming পাস করে দেওয়া সহ। তারপর আছে mobile app-এর জন্য একই পদ্ধতি।

## Client code-এ key রাখলে কেন কাজ হয় না

- **key গোপন থাকে না।** browser-এর developer tools-এ প্রতিটা request দেখা যায়। mobile app খুলে তার ভেতরের string পড়া যায়। framework যেসব environment variable client bundle-এ কপি করে দেয়, যেমন Next.js-এ `NEXT_PUBLIC_` দিয়ে শুরু হওয়া নামগুলো, সেগুলো সংজ্ঞাতেই public।
- **key ফাঁস মানে টাকার ক্ষতি।** key যার হাতে যাবে, সে key-এর spend cap বা আপনার ব্যালান্স শেষ না হওয়া পর্যন্ত request চালিয়ে যেতে পারবে।
- **CORS আপনাকে বাঁচায়ও না, কাজেও লাগে না।** gateway browser-এর preflight request-এর উত্তর দেয়, কিন্তু আসল response-এ `Access-Control-Allow-Origin` header থাকে না। তাই browser উত্তরটা আপনার page-কে দেয় না। এটাকে নিরাপত্তার ব্যবস্থা ভাববেন না: request কিন্তু আপনার key সমেত browser থেকে ঠিকই বেরিয়ে যেতে পারে। client code-এ থাকা key-কে ধরে নিন ফাঁস হয়েই গেছে।

কোনো key কখনো client code-এ থেকে থাকলে সেটা [API keys](/dashboard/keys) থেকে revoke করে নতুন একটা key বানান। বিস্তারিত [API keys](/docs/api-keys) পেজে।

## পদ্ধতিটা

```text
Browser or app  --(your user's session)-->  Your backend  --(Tokens key)-->  Tokens
                <------- streamed answer ---------------  <---- stream -----
```

আপনার backend পাঁচটা কাজ করবে:

1. **আপনার user-কে authenticate করবে।** আপনার app-এ যে session বা token আগে থেকেই আছে, সেটাই ব্যবহার করুন। খোলা endpoint মানে খোলা Wallet।
2. **input যাচাই করবে।** message list বা prompt নিন, তার size দেখুন, আর client আর যা-ই পাঠাক, সব উপেক্ষা করুন।
3. **model আর সীমাগুলো server-এ ঠিক করে দেবে।** `model`, `max_tokens` বা tools client ঠিক করবে না। নইলে কোনো user সবচেয়ে দামি model আর সবচেয়ে বড় output বেছে নিতে পারবে।
4. **environment variable থেকে key নিয়ে Tokens-কে call করবে,** আর stream buffer না করে সোজা পাস করে দেবে।
5. **upstream error লুকিয়ে রাখবে।** billing বা limit-এর error আপনার সমস্যা, আপনার user-এর না। `x-tokens-request-id` log করে রাখুন, আর user-কে ছোট একটা কথা বলুন।

এই service-এর জন্য আলাদা একটা key নিন, যেটায় monthly spend cap আর allowed models-এর তালিকা দেওয়া আছে। তাহলে bug বা কোনো খারাপ user থাকলেও খরচ একটা জানা অঙ্কের বেশি হবে না। দেখুন [API keys](/docs/api-keys) আর [production checklist](/docs/production-checklist)।

## Streaming সহ Next.js route handler

এটা App Router-এর একটা route handler। এটা শুধু server-এ চলে, তাই `process.env.TOKENS_API_KEY` কখনো browser-এ যায় না।

```typescript title="app/api/chat/route.ts"
import { getSessionUserId } from "@/lib/session"; // your own auth

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const TOKENS_URL = "https://tokens.bd/v1/chat/completions";
const MODEL = "deepseek/deepseek-v4.1-flash";

type ChatMessage = { role: "user" | "assistant"; content: string };

function parseMessages(value: unknown): ChatMessage[] | null {
  if (!Array.isArray(value) || value.length === 0 || value.length > 40) return null;
  const messages: ChatMessage[] = [];
  for (const item of value) {
    if (typeof item !== "object" || item === null) return null;
    const { role, content } = item as Record<string, unknown>;
    if (role !== "user" && role !== "assistant") return null;
    if (typeof content !== "string" || content.length > 20_000) return null;
    messages.push({ role, content });
  }
  return messages;
}

export async function POST(req: Request): Promise<Response> {
  const userId = await getSessionUserId(req);
  if (!userId) return Response.json({ error: "unauthorized" }, { status: 401 });

  const payload: unknown = await req.json().catch(() => null);
  const messages = parseMessages((payload as { messages?: unknown } | null)?.messages);
  if (!messages) return Response.json({ error: "invalid_request" }, { status: 400 });

  let upstream: Response;
  try {
    upstream = await fetch(TOKENS_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TOKENS_API_KEY}`,
        "Content-Type": "application/json",
        "x-request-id": crypto.randomUUID(),
      },
      body: JSON.stringify({ model: MODEL, messages, stream: true, max_tokens: 1024 }),
      signal: req.signal, // client left: cancel the upstream request too
    });
  } catch {
    return Response.json({ error: "unavailable" }, { status: 502 });
  }

  if (!upstream.ok || !upstream.body) {
    console.error("tokens call failed", {
      userId,
      status: upstream.status,
      requestId: upstream.headers.get("x-tokens-request-id"),
    });
    await upstream.body?.cancel();
    const headers = new Headers();
    const retryAfter = upstream.headers.get("retry-after");
    if (upstream.status === 429 && retryAfter) headers.set("Retry-After", retryAfter);
    return Response.json(
      { error: upstream.status === 429 ? "busy" : "unavailable" },
      { status: upstream.status === 429 ? 429 : 502, headers }
    );
  }

  return new Response(upstream.body, {
    headers: {
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      "X-Accel-Buffering": "no",
    },
  });
}
```

`getSessionUserId` বলতে আপনার app যা দিয়ে user চেনে সেটাই বোঝানো হয়েছে (Auth.js, Clerk, Supabase Auth বা আপনার নিজের cookie)। শুধু এই লাইনটাই আপনি বদলাবেন। আপনার host যদি route কতক্ষণ চলতে পারবে তার সীমা বেঁধে দেয়, তাহলে এই route-এর জন্য সীমাটা বাড়িয়ে নিন, কারণ reasoning model কয়েক মিনিটও নিতে পারে।

### Browser-এ stream পড়া

উত্তরটা সেই একই Server-Sent Events text, যা Tokens পাঠায়। তাই client `data:` লাইনগুলো পড়ে। কোনো chunk লাইনের মাঝখানে শেষ হতে পারে, তাই অসমাপ্ত অংশটা একটা buffer-এ রেখে দিন।

```typescript title="chat-client.ts"
export async function streamChat(
  messages: { role: "user" | "assistant"; content: string }[],
  onText: (text: string) => void,
  signal?: AbortSignal
): Promise<void> {
  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages }),
    signal,
  });
  if (!res.ok || !res.body) throw new Error(`Chat failed with status ${res.status}`);

  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) return;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";
    for (const line of lines) {
      if (!line.startsWith("data:")) continue;
      const data = line.slice(5).trim();
      if (data === "[DONE]") return;
      const delta = JSON.parse(data).choices?.[0]?.delta?.content;
      if (delta) onText(delta);
    }
  }
}
```

"stop" বাটন থেকে একটা `AbortSignal` পাঠান। abort করলে আপনার backend-এর request বাতিল হয়, তাতে Tokens-এর request-ও বাতিল হয়, আর সেই পর্যন্ত যতটুকু তৈরি হয়েছে Tokens শুধু ততটুকুর বিল করে।

## Express দিয়ে Node.js

একই backend, এবার সাধারণ একটা Express app হিসেবে। এখানে Node.js 18 বা তার পরের version-এর global `fetch` আর ES modules ব্যবহার হয়েছে।

```javascript title="server.js"
import express from "express";
import { randomUUID } from "node:crypto";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { requireUser } from "./auth.js"; // your own auth middleware

const TOKENS_URL = "https://tokens.bd/v1/chat/completions";
const MODEL = "deepseek/deepseek-v4.1-flash";

const app = express();
app.use(express.json({ limit: "100kb" }));

function validMessages(messages) {
  return (
    Array.isArray(messages) &&
    messages.length > 0 &&
    messages.length <= 40 &&
    messages.every(
      (m) =>
        (m?.role === "user" || m?.role === "assistant") &&
        typeof m.content === "string" &&
        m.content.length <= 20_000
    )
  );
}

app.post("/api/chat", requireUser, async (req, res) => {
  const messages = req.body?.messages;
  if (!validMessages(messages)) return res.status(400).json({ error: "invalid_request" });

  // The client closed the connection before the answer finished: stop the upstream request.
  const controller = new AbortController();
  res.on("close", () => {
    if (!res.writableEnded) controller.abort();
  });

  let upstream;
  try {
    upstream = await fetch(TOKENS_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TOKENS_API_KEY}`,
        "Content-Type": "application/json",
        "x-request-id": randomUUID(),
      },
      body: JSON.stringify({ model: MODEL, messages, stream: true, max_tokens: 1024 }),
      signal: controller.signal,
    });
  } catch {
    return res.status(502).json({ error: "unavailable" });
  }

  if (!upstream.ok || !upstream.body) {
    console.error("tokens call failed", {
      userId: req.user.id,
      status: upstream.status,
      requestId: upstream.headers.get("x-tokens-request-id"),
    });
    await upstream.body?.cancel();
    const retryAfter = upstream.headers.get("retry-after");
    if (upstream.status === 429 && retryAfter) res.set("Retry-After", retryAfter);
    return res
      .status(upstream.status === 429 ? 429 : 502)
      .json({ error: upstream.status === 429 ? "busy" : "unavailable" });
  }

  res.status(200).set({
    "Content-Type": "text/event-stream; charset=utf-8",
    "Cache-Control": "no-cache, no-transform",
    "X-Accel-Buffering": "no",
  });
  res.flushHeaders();

  try {
    await pipeline(Readable.fromWeb(upstream.body), res);
  } catch {
    // The client left or the upstream stream broke. Nothing more to send.
  }
});

app.listen(3001, () => console.log("Listening on http://localhost:3001"));
```

`requireUser` হলো আপনার authentication middleware, সেটা `req.user` সেট করে। আগের ধাপের browser code এই server-এর সাথে কোনো বদল ছাড়াই চলে।

Express nginx-এর পেছনে চালালে এই route-এর জন্য `proxy_buffering off;` দিন। না দিলে stream টুকরো টুকরো না এসে একসাথে আসবে। `text/event-stream` response compress করবেন না। আরও দেখুন [troubleshooting](/docs/troubleshooting)।

## Mobile app

mobile app-এও একই কাজ: সে কথা বলে আপনার backend-এর সাথে, Tokens-এর সাথে কখনো নয়। app আপনার user-এর নিজের session token পাঠায়। আপনার backend সেটা যাচাই করে, প্রতি user-এর সীমা প্রয়োগ করে, আর key দিয়ে Tokens-কে call করে।

mobile-এর সবচেয়ে সহজ নকশা হলো non-streaming call। সব platform-এর default HTTP client response body একটু একটু করে দেয় না, আর সাধারণ একটা request আগে ঠিকমতো চালু করা সহজ। তাই backend-এ আরেকটা route যোগ করুন, যেটা পুরো উত্তরটা JSON হিসেবে ফেরত দেয়:

```typescript title="app/api/ask/route.ts"
import { getSessionUserId } from "@/lib/session"; // your own auth

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST(req: Request): Promise<Response> {
  const userId = await getSessionUserId(req);
  if (!userId) return Response.json({ error: "unauthorized" }, { status: 401 });

  const payload = (await req.json().catch(() => null)) as { prompt?: unknown } | null;
  const prompt = payload?.prompt;
  if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 20_000) {
    return Response.json({ error: "invalid_request" }, { status: 400 });
  }

  let upstream: Response;
  try {
    upstream = await fetch("https://tokens.bd/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TOKENS_API_KEY}`,
        "Content-Type": "application/json",
        "x-request-id": crypto.randomUUID(),
      },
      body: JSON.stringify({
        model: "deepseek/deepseek-v4.1-flash",
        messages: [{ role: "user", content: prompt }],
        max_tokens: 800,
      }),
      signal: AbortSignal.timeout(120_000),
    });
  } catch {
    return Response.json({ error: "unavailable" }, { status: 502 });
  }

  if (!upstream.ok) {
    console.error("tokens call failed", {
      userId,
      status: upstream.status,
      requestId: upstream.headers.get("x-tokens-request-id"),
    });
    return Response.json({ error: "unavailable" }, { status: upstream.status === 429 ? 429 : 502 });
  }

  const data = (await upstream.json()) as { choices?: { message?: { content?: string } }[] };
  return Response.json({ text: data.choices?.[0]?.message?.content ?? "" });
}
```

app এই route-কে নিজের session token দিয়ে call করে:

:::code-tabs

```swift title="Swift (iOS)"
import Foundation

struct AskReply: Decodable { let text: String }

func ask(_ prompt: String, sessionToken: String) async throws -> String {
    var request = URLRequest(url: URL(string: "https://api.example.com/api/ask")!)
    request.httpMethod = "POST"
    request.setValue("Bearer \(sessionToken)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["prompt": prompt])
    request.timeoutInterval = 130

    let (data, response) = try await URLSession.shared.data(for: request)
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(AskReply.self, from: data).text
}
```

```typescript title="React Native"
export async function ask(prompt: string, sessionToken: string): Promise<string> {
  const res = await fetch("https://api.example.com/api/ask", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${sessionToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ prompt }),
  });
  if (!res.ok) throw new Error(`Request failed with status ${res.status}`);
  const data = (await res.json()) as { text: string };
  return data.text;
}
```

:::

`https://api.example.com`-এর জায়গায় নিজের backend-এর ঠিকানা বসান। app-এর হাতে থাকে আপনার user-এর স্বল্পমেয়াদি session, Tokens key নয়। তাই ফোন চুরি হোক বা app decompile করা হোক, আপনার একজন user-এর session যাবে, পুরো অ্যাকাউন্ট নয়।

mobile-এ streaming চাইলে আগে দেখে নিন আপনার HTTP client response body আসার সাথে সাথে পড়তে দেয় কি না। না দিলে non-streaming route-ই রাখুন আর একটা loading অবস্থা দেখান।

## Backend-কেও সুরক্ষিত রাখুন

key server-এ নিয়ে গেলে লক্ষ্যবস্তুও সরে যায়। আপনার endpoint যে-ই call করতে পারে, সে-ই এর ভেতর দিয়ে খরচ করতে পারে।

- **প্রতি user-এর জন্য rate limit দিন,** শুধু IP ধরে নয়, আর একসাথে কয়টা request চলবে তারও সীমা রাখুন। Tokens request per minute আর concurrency-র সীমা আপনার পুরো অ্যাকাউন্টের জন্য ধরে, তাই একজন গোলমেলে user সবার ভাগ খেয়ে ফেলতে পারে। দেখুন [rate limits](/docs/rate-limits)।
- **যা নেবেন তার size-এর সীমা বাঁধুন,** উদাহরণগুলোর মতোই: message-এর সংখ্যা, প্রতি message-এ কত অক্ষর, আর `max_tokens`।
- **model-এর তালিকা server-এ রাখুন।** user model বেছে নিতে পারলে সেটা আপনার নিজের তালিকা থেকে বাছতে দিন।
- **upstream error-এর লেখা দেখাবেন না।** ছোট একটা code ফেরত দিন, বিস্তারিত আপনার log-এ রাখুন।
- **spend cap দেওয়া আলাদা একটা key ব্যবহার করুন।** backend অপব্যবহার হলেও cap খরচ থামিয়ে দেবে।
- **`Origin` header-কে authentication ভাববেন না।** যেকোনো script সেটা বসিয়ে দিতে পারে।

## Troubleshooting

**browser console-এ CORS error দেখাচ্ছে।** আপনার page সরাসরি Tokens-কে call করছে। ওপরের মতো করে নিজের route-কে call করুন।

**উত্তর একসাথে এসে পড়ছে।** আপনার backend আর user-এর মাঝে কিছু একটা stream buffer করছে: proxy, compression-এর কোনো স্তর, অথবা এমন HTTP client যে পুরো body-র জন্য অপেক্ষা করে। দেখুন [troubleshooting](/docs/troubleshooting), আর backend-টা `curl -N` দিয়ে পরীক্ষা করুন।

**backend 502 ফেরত দিচ্ছে আর log-এ 401 বা 403 দেখাচ্ছে।** key ভুল, revoke করা, বা কোনো বিধিনিষেধ দেওয়া। দেখুন [API keys](/docs/api-keys)।

**backend 502 ফেরত দিচ্ছে আর log-এ 402 দেখাচ্ছে।** ব্যালান্স বা plan শেষ। [billing](/dashboard/billing) থেকে টাকা যোগ করুন। loop-এ retry করবেন না, দেখুন [errors](/docs/errors)।

**আপনার user 429 পাচ্ছে।** আপনার অ্যাকাউন্ট per-minute বা concurrency-র সীমায় পৌঁছেছে, অথবা কোনো usage window শেষ। কারণটা log-এর code-এ আছে। দেখুন [rate limits](/docs/rate-limits)।

---
Page: https://tokens.bd/bn/docs/browser-and-mobile
