> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitrobot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# BitRobot Connect examples

> Backend implementations, page templates, and tests for a BitRobot Connect integration, in seven languages

Copy-ready implementations of the routes from the [BitRobot Connect guide](/bitrobot-connect).

## Backend implementations

Each example implements `GET /bitrobot/connect` and `POST /bitrobot/connect/confirm`, stores `request_id` and `nonce` in the server-side session, and reads `BITROBOT_API_KEY`, `BITROBOT_SUBNET_ID`, and `BITROBOT_API_BASE` from the environment. Replace the login guard (`requireLogin`, `current_user`, and so on) with your own. If your API is stateless, use the [stateless hold](#stateless-hold) instead of the session.

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  // routes/bitrobot-connect.js
  const express = require("express");
  const crypto = require("node:crypto");
  const { requireLogin } = require("../middleware/auth"); // your existing login guard

  const router = express.Router();

  const BITROBOT_API_BASE = process.env.BITROBOT_API_BASE || "https://api.bitrobot.ai";
  const BITROBOT_API_KEY = process.env.BITROBOT_API_KEY;
  const BITROBOT_SUBNET_ID = process.env.BITROBOT_SUBNET_ID;

  // Landing page. Needs a server-side session (for example express-session) mounted before this router.
  router.get("/bitrobot/connect", requireLogin, (req, res) => {
    const { request_id: requestId, nonce } = req.query;
    if (typeof requestId !== "string" || typeof nonce !== "string") {
      return res.status(400).send("Missing request_id or nonce");
    }

    req.session.bitrobotConnect = { requestId, nonce };
    res.render("bitrobot-connect", { walletAddress: req.user.walletAddress });
  });

  // Confirm handler. The Connect button on the landing page posts here.
  router.post("/bitrobot/connect/confirm", requireLogin, async (req, res) => {
    const pending = req.session.bitrobotConnect;
    if (!pending) {
      return res.status(400).send("No connection in progress");
    }

    const response = await fetch(
      `${BITROBOT_API_BASE}/subnets/${BITROBOT_SUBNET_ID}/connect/confirm`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${BITROBOT_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": crypto.randomUUID(),
        },
        body: JSON.stringify({
          request_id: pending.requestId,
          nonce: pending.nonce,
          wallet_address: req.user.walletAddress,
          subnet_user_id: String(req.user.id),
        }),
      }
    );
    const body = await response.json();

    // Branch on auth_code, not the status. Never log or render auth_code.
    const returnUrl = safeReturnUrl(body.return_url);
    if (!body.auth_code || !returnUrl) {
      return res
        .status(400)
        .render("bitrobot-connect-error", { error: body.error, reason: body.reason });
    }

    delete req.session.bitrobotConnect;

    // Redirect to return_url verbatim, fragment included.
    res.redirect(303, returnUrl);
  });

  // Allow http(s) URLs only. Return the string as given.
  function safeReturnUrl(raw) {
    if (!raw) return null;
    try {
      const url = new URL(raw);
      return url.protocol === "https:" || url.protocol === "http:" ? raw : null;
    } catch {
      return null;
    }
  }

  module.exports = router;
  ```

  ```python Python (FastAPI) theme={null}
  # app/bitrobot_connect.py
  import os
  import uuid
  from urllib.parse import urlsplit

  import httpx
  from fastapi import APIRouter, Depends, HTTPException, Request
  from fastapi.responses import HTMLResponse, RedirectResponse
  from fastapi.templating import Jinja2Templates

  from app.auth import current_user  # your existing login dependency

  # Needs SessionMiddleware on the app:
  #   app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"])
  router = APIRouter()
  templates = Jinja2Templates(directory="templates")

  BITROBOT_API_BASE = os.environ.get("BITROBOT_API_BASE", "https://api.bitrobot.ai")
  BITROBOT_API_KEY = os.environ["BITROBOT_API_KEY"]
  BITROBOT_SUBNET_ID = os.environ["BITROBOT_SUBNET_ID"]


  # Landing page.
  @router.get("/bitrobot/connect", response_class=HTMLResponse)
  async def landing(request: Request, request_id: str, nonce: str, user=Depends(current_user)):
      request.session["bitrobot_connect"] = {"request_id": request_id, "nonce": nonce}
      return templates.TemplateResponse(
          "bitrobot_connect.html",
          {"request": request, "wallet_address": user.wallet_address},
      )


  # Confirm handler. The Connect button on the landing page posts here.
  @router.post("/bitrobot/connect/confirm")
  async def confirm(request: Request, user=Depends(current_user)):
      pending = request.session.get("bitrobot_connect")
      if not pending:
          raise HTTPException(status_code=400, detail="No connection in progress")

      async with httpx.AsyncClient(timeout=10) as client:
          response = await client.post(
              f"{BITROBOT_API_BASE}/subnets/{BITROBOT_SUBNET_ID}/connect/confirm",
              headers={
                  "Authorization": f"Bearer {BITROBOT_API_KEY}",
                  "Idempotency-Key": str(uuid.uuid4()),
              },
              json={
                  "request_id": pending["request_id"],
                  "nonce": pending["nonce"],
                  "wallet_address": user.wallet_address,
                  "subnet_user_id": str(user.id),
              },
          )
      body = response.json()

      # Branch on auth_code, not the status. Never log or render auth_code.
      return_url = safe_return_url(body.get("return_url"))
      if not body.get("auth_code") or return_url is None:
          return templates.TemplateResponse(
              "bitrobot_connect_error.html",
              {"request": request, "error": body.get("error"), "reason": body.get("reason")},
              status_code=400,
          )

      request.session.pop("bitrobot_connect", None)

      # Redirect to return_url verbatim, fragment included.
      return RedirectResponse(return_url, status_code=303)


  # Allow http(s) URLs only. Return the string as given.
  def safe_return_url(raw):
      if not raw:
          return None
      scheme = urlsplit(raw).scheme.lower()
      return raw if scheme in ("http", "https") else None
  ```

  ```ruby Ruby (Sinatra) theme={null}
  # bitrobot_connect.rb
  require "sinatra"
  require "net/http"
  require "json"
  require "securerandom"

  enable :sessions

  BITROBOT_API_BASE = ENV.fetch("BITROBOT_API_BASE", "https://api.bitrobot.ai")
  BITROBOT_API_KEY = ENV.fetch("BITROBOT_API_KEY")
  BITROBOT_SUBNET_ID = ENV.fetch("BITROBOT_SUBNET_ID")

  # Landing page.
  get "/bitrobot/connect" do
    require_login! # your existing login guard
    if params[:request_id].to_s.empty? || params[:nonce].to_s.empty?
      halt 400, "Missing request_id or nonce"
    end

    session[:bitrobot_connect] = { "request_id" => params[:request_id], "nonce" => params[:nonce] }
    erb :bitrobot_connect, locals: { wallet_address: current_user.wallet_address }
  end

  # Confirm handler. The Connect button on the landing page posts here.
  post "/bitrobot/connect/confirm" do
    require_login!
    pending = session[:bitrobot_connect]
    halt 400, "No connection in progress" unless pending

    uri = URI("#{BITROBOT_API_BASE}/subnets/#{BITROBOT_SUBNET_ID}/connect/confirm")
    http_request = Net::HTTP::Post.new(uri)
    http_request["Authorization"] = "Bearer #{BITROBOT_API_KEY}"
    http_request["Content-Type"] = "application/json"
    http_request["Idempotency-Key"] = SecureRandom.uuid
    http_request.body = JSON.generate(
      request_id: pending["request_id"],
      nonce: pending["nonce"],
      wallet_address: current_user.wallet_address,
      subnet_user_id: current_user.id.to_s
    )

    response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(http_request) }
    body = JSON.parse(response.body)

    # Branch on auth_code, not the status. Never log or render auth_code.
    return_url = safe_return_url(body["return_url"])
    if body["auth_code"].to_s.empty? || return_url.nil?
      halt 400, erb(:bitrobot_connect_error, locals: { error: body["error"], reason: body["reason"] })
    end

    session.delete(:bitrobot_connect)

    # Redirect to return_url verbatim, fragment included.
    redirect return_url, 303
  end

  # Allow http(s) URLs only. Return the string as given.
  def safe_return_url(raw)
    return nil if raw.to_s.empty?

    uri = URI.parse(raw)
    uri.is_a?(URI::HTTP) ? raw : nil
  rescue URI::InvalidURIError
    nil
  end
  ```

  ```go Go (net/http) theme={null}
  // bitrobot_connect.go
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"html/template"
  	"net/http"
  	"net/url"
  	"os"

  	"github.com/google/uuid"
  	"github.com/gorilla/sessions" // any server-side session store works
  )

  var (
  	bitrobotAPIBase  = envOr("BITROBOT_API_BASE", "https://api.bitrobot.ai")
  	bitrobotAPIKey   = os.Getenv("BITROBOT_API_KEY")
  	bitrobotSubnetID = os.Getenv("BITROBOT_SUBNET_ID")
  	sessionStore     = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
  	templates        = template.Must(template.ParseGlob("templates/*.html"))
  )

  type confirmResponse struct {
  	Status    string `json:"status"`
  	ReturnURL string `json:"return_url"`
  	AuthCode  string `json:"auth_code"` // Check its presence only. Never log or render it.
  	Error     string `json:"error"`
  	Reason    string `json:"reason"`
  }

  // Landing page.
  func connectLanding(w http.ResponseWriter, r *http.Request) {
  	user, ok := currentUser(r) // your existing login guard
  	if !ok {
  		redirectToLogin(w, r)
  		return
  	}
  	requestID, nonce := r.URL.Query().Get("request_id"), r.URL.Query().Get("nonce")
  	if requestID == "" || nonce == "" {
  		http.Error(w, "Missing request_id or nonce", http.StatusBadRequest)
  		return
  	}

  	session, _ := sessionStore.Get(r, "app")
  	session.Values["bitrobot_request_id"] = requestID
  	session.Values["bitrobot_nonce"] = nonce
  	if err := session.Save(r, w); err != nil {
  		http.Error(w, "Could not save session", http.StatusInternalServerError)
  		return
  	}
  	templates.ExecuteTemplate(w, "bitrobot_connect.html", map[string]string{
  		"WalletAddress": user.WalletAddress,
  	})
  }

  // Confirm handler. The Connect button on the landing page posts here.
  func connectConfirm(w http.ResponseWriter, r *http.Request) {
  	user, ok := currentUser(r)
  	if !ok {
  		redirectToLogin(w, r)
  		return
  	}
  	session, _ := sessionStore.Get(r, "app")
  	requestID, _ := session.Values["bitrobot_request_id"].(string)
  	nonce, _ := session.Values["bitrobot_nonce"].(string)
  	if requestID == "" || nonce == "" {
  		http.Error(w, "No connection in progress", http.StatusBadRequest)
  		return
  	}

  	payload, _ := json.Marshal(map[string]string{
  		"request_id":     requestID,
  		"nonce":          nonce,
  		"wallet_address": user.WalletAddress,
  		"subnet_user_id": user.ID,
  	})
  	req, _ := http.NewRequest(
  		http.MethodPost,
  		fmt.Sprintf("%s/subnets/%s/connect/confirm", bitrobotAPIBase, bitrobotSubnetID),
  		bytes.NewReader(payload),
  	)
  	req.Header.Set("Authorization", "Bearer "+bitrobotAPIKey)
  	req.Header.Set("Content-Type", "application/json")
  	req.Header.Set("Idempotency-Key", uuid.NewString())

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		http.Error(w, "BitRobot is unreachable, please try again", http.StatusBadGateway)
  		return
  	}
  	defer resp.Body.Close()

  	var body confirmResponse
  	_ = json.NewDecoder(resp.Body).Decode(&body)

  	// Branch on auth_code, not the status.
  	returnURL := safeReturnURL(body.ReturnURL)
  	if body.AuthCode == "" || returnURL == "" {
  		w.WriteHeader(http.StatusBadRequest)
  		templates.ExecuteTemplate(w, "bitrobot_connect_error.html", map[string]string{
  			"Error": body.Error, "Reason": body.Reason,
  		})
  		return
  	}

  	delete(session.Values, "bitrobot_request_id")
  	delete(session.Values, "bitrobot_nonce")
  	_ = session.Save(r, w)

  	// Redirect to return_url verbatim, fragment included.
  	http.Redirect(w, r, returnURL, http.StatusSeeOther)
  }

  // Allow http(s) URLs only. Return the string as given.
  func safeReturnURL(raw string) string {
  	parsed, err := url.Parse(raw)
  	if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
  		return ""
  	}
  	return raw
  }

  func envOr(key, fallback string) string {
  	if v := os.Getenv(key); v != "" {
  		return v
  	}
  	return fallback
  }

  func main() {
  	http.HandleFunc("GET /bitrobot/connect", connectLanding)
  	http.HandleFunc("POST /bitrobot/connect/confirm", connectConfirm)
  	http.ListenAndServe(":8080", nil)
  }
  ```

  ```php PHP (Laravel) theme={null}
  <?php
  // app/Http/Controllers/BitRobotConnectController.php
  //
  // routes/web.php:
  //   Route::get('/bitrobot/connect', [BitRobotConnectController::class, 'landing'])->middleware('auth');
  //   Route::post('/bitrobot/connect/confirm', [BitRobotConnectController::class, 'confirm'])->middleware('auth');
  //
  // config/services.php:
  //   'bitrobot' => [
  //       'api_base' => env('BITROBOT_API_BASE', 'https://api.bitrobot.ai'),
  //       'api_key' => env('BITROBOT_API_KEY'),
  //       'subnet_id' => env('BITROBOT_SUBNET_ID'),
  //   ],

  namespace App\Http\Controllers;

  use Illuminate\Http\Request;
  use Illuminate\Support\Facades\Http;
  use Illuminate\Support\Str;

  class BitRobotConnectController extends Controller
  {
      // Landing page.
      public function landing(Request $request)
      {
          $validated = $request->validate([
              'request_id' => 'required|string',
              'nonce' => 'required|string',
          ]);

          $request->session()->put('bitrobot_connect', $validated);

          return view('bitrobot.connect', [
              'walletAddress' => $request->user()->wallet_address,
          ]);
      }

      // Confirm handler. The Connect button on the landing page posts here.
      public function confirm(Request $request)
      {
          $pending = $request->session()->get('bitrobot_connect');
          abort_unless($pending, 400, 'No connection in progress');

          $baseUrl = config('services.bitrobot.api_base');
          $subnetId = config('services.bitrobot.subnet_id');

          $response = Http::withToken(config('services.bitrobot.api_key'))
              ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
              ->post("{$baseUrl}/subnets/{$subnetId}/connect/confirm", [
                  'request_id' => $pending['request_id'],
                  'nonce' => $pending['nonce'],
                  'wallet_address' => $request->user()->wallet_address,
                  'subnet_user_id' => (string) $request->user()->id,
              ]);

          // Branch on auth_code, not the status. Never log or render auth_code.
          $returnUrl = $this->safeReturnUrl($response->json('return_url'));
          if (!$response->json('auth_code') || $returnUrl === null) {
              return response()->view('bitrobot.connect-error', [
                  'error' => $response->json('error'),
                  'reason' => $response->json('reason'),
              ], 400);
          }

          $request->session()->forget('bitrobot_connect');

          // Redirect to return_url verbatim, fragment included.
          return redirect()->away($returnUrl, 303);
      }

      // Allow http(s) URLs only. Return the string as given.
      private function safeReturnUrl(?string $raw): ?string
      {
          if (!$raw) {
              return null;
          }

          $scheme = strtolower((string) parse_url($raw, PHP_URL_SCHEME));

          return in_array($scheme, ['http', 'https'], true) ? $raw : null;
      }
  }
  ```

  ```java Java (Spring Boot) theme={null}
  // src/main/java/com/example/bitrobot/BitRobotConnectController.java
  package com.example.bitrobot;

  import java.net.URI;
  import java.util.Map;
  import java.util.UUID;

  import jakarta.servlet.http.HttpSession;

  import org.springframework.core.ParameterizedTypeReference;
  import org.springframework.http.HttpHeaders;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.MediaType;
  import org.springframework.http.ResponseEntity;
  import org.springframework.security.core.annotation.AuthenticationPrincipal;
  import org.springframework.stereotype.Controller;
  import org.springframework.ui.Model;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.PostMapping;
  import org.springframework.web.bind.annotation.RequestParam;
  import org.springframework.web.client.HttpStatusCodeException;
  import org.springframework.web.client.RestClient;
  import org.springframework.web.server.ResponseStatusException;

  // Both routes sit behind your existing Spring Security login requirement.
  // AppUser is your own principal type; it must expose the user's subnet wallet address.
  @Controller
  public class BitRobotConnectController {

      private static final ParameterizedTypeReference<Map<String, Object>> JSON_OBJECT =
              new ParameterizedTypeReference<>() {};

      private static final String BITROBOT_API_BASE =
              System.getenv().getOrDefault("BITROBOT_API_BASE", "https://api.bitrobot.ai");
      private static final String BITROBOT_API_KEY = System.getenv("BITROBOT_API_KEY");
      private static final String BITROBOT_SUBNET_ID = System.getenv("BITROBOT_SUBNET_ID");

      private final RestClient bitrobot = RestClient.builder()
              .baseUrl(BITROBOT_API_BASE)
              .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BITROBOT_API_KEY)
              .build();

      // Landing page.
      @GetMapping("/bitrobot/connect")
      public String landing(@RequestParam("request_id") String requestId,
                            @RequestParam("nonce") String nonce,
                            @AuthenticationPrincipal AppUser user,
                            HttpSession session,
                            Model model) {
          session.setAttribute("bitrobotRequestId", requestId);
          session.setAttribute("bitrobotNonce", nonce);
          model.addAttribute("walletAddress", user.walletAddress());
          return "bitrobot-connect";
      }

      // Confirm handler. The Connect button on the landing page posts here.
      @PostMapping("/bitrobot/connect/confirm")
      public Object confirm(@AuthenticationPrincipal AppUser user, HttpSession session, Model model) {
          String requestId = (String) session.getAttribute("bitrobotRequestId");
          String nonce = (String) session.getAttribute("bitrobotNonce");
          if (requestId == null || nonce == null) {
              throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No connection in progress");
          }

          Map<String, Object> body;
          try {
              body = bitrobot.post()
                      .uri("/subnets/{subnetId}/connect/confirm", BITROBOT_SUBNET_ID)
                      .contentType(MediaType.APPLICATION_JSON)
                      .header("Idempotency-Key", UUID.randomUUID().toString())
                      .body(Map.of(
                              "request_id", requestId,
                              "nonce", nonce,
                              "wallet_address", user.walletAddress(),
                              "subnet_user_id", String.valueOf(user.id())))
                      .retrieve()
                      .body(JSON_OBJECT);
          } catch (HttpStatusCodeException e) {
              // Read the body of a 4xx response too.
              body = e.getResponseBodyAs(JSON_OBJECT);
          }

          // Branch on auth_code, not the status. Never log or render auth_code.
          Object authCode = body == null ? null : body.get("auth_code");
          String returnUrl = safeReturnUrl(body == null ? null : body.get("return_url"));
          if (authCode == null || returnUrl == null) {
              model.addAttribute("error", body == null ? null : body.get("error"));
              model.addAttribute("reason", body == null ? null : body.get("reason"));
              return "bitrobot-connect-error";
          }

          session.removeAttribute("bitrobotRequestId");
          session.removeAttribute("bitrobotNonce");

          // Redirect to return_url verbatim, fragment included, with a 303.
          return ResponseEntity.status(HttpStatus.SEE_OTHER).header(HttpHeaders.LOCATION, returnUrl).build();
      }

      // Allow http(s) URLs only. Return the string as given.
      private static String safeReturnUrl(Object raw) {
          if (!(raw instanceof String value) || value.isBlank()) {
              return null;
          }
          try {
              String scheme = URI.create(value).getScheme();
              return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme) ? value : null;
          } catch (IllegalArgumentException e) {
              return null;
          }
      }
  }
  ```

  ```csharp C# (ASP.NET Core minimal API) theme={null}
  // Program.cs
  using System.Net.Http.Headers;
  using System.Net.Http.Json;
  using System.Text.Json.Serialization;

  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddDistributedMemoryCache();
  builder.Services.AddSession();
  builder.Services.AddHttpClient("bitrobot", client =>
  {
      client.BaseAddress = new Uri(
          Environment.GetEnvironmentVariable("BITROBOT_API_BASE") ?? "https://api.bitrobot.ai");
      client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
          "Bearer", Environment.GetEnvironmentVariable("BITROBOT_API_KEY"));
  });
  // Plus your existing authentication/authorization registration.

  var app = builder.Build();
  app.UseSession();

  var subnetId = Environment.GetEnvironmentVariable("BITROBOT_SUBNET_ID");

  // Landing page.
  app.MapGet("/bitrobot/connect", (HttpContext http, string request_id, string nonce) =>
  {
      var user = http.CurrentUser(); // your existing helper for the signed-in user

      http.Session.SetString("bitrobot_request_id", request_id);
      http.Session.SetString("bitrobot_nonce", nonce);
      return Results.Content(ConfirmPage(user.WalletAddress), "text/html");
  }).RequireAuthorization();

  // Confirm handler. The Connect button on the landing page posts here.
  app.MapPost("/bitrobot/connect/confirm", async (HttpContext http, IHttpClientFactory clients) =>
  {
      var user = http.CurrentUser();
      var requestId = http.Session.GetString("bitrobot_request_id");
      var nonce = http.Session.GetString("bitrobot_nonce");
      if (requestId is null || nonce is null)
      {
          return Results.BadRequest("No connection in progress");
      }

      var client = clients.CreateClient("bitrobot");
      using var message = new HttpRequestMessage(HttpMethod.Post, $"/subnets/{subnetId}/connect/confirm")
      {
          Content = JsonContent.Create(new
          {
              request_id = requestId,
              nonce,
              wallet_address = user.WalletAddress,
              subnet_user_id = user.Id.ToString(),
          }),
      };
      message.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());

      var response = await client.SendAsync(message);
      var body = await response.Content.ReadFromJsonAsync<ConfirmResponse>();

      // Branch on auth_code, not the status. Never log or render auth_code.
      var returnUrl = SafeReturnUrl(body?.ReturnUrl);
      if (body?.AuthCode is null || returnUrl is null)
      {
          return Results.Content(ErrorPage(body?.Error, body?.Reason), "text/html", statusCode: 400);
      }

      http.Session.Remove("bitrobot_request_id");
      http.Session.Remove("bitrobot_nonce");

      // Redirect to return_url verbatim, fragment included, with a 303.
      http.Response.Headers.Location = returnUrl;
      return Results.StatusCode(StatusCodes.Status303SeeOther);
  }).RequireAuthorization();

  app.Run();

  // Allow http(s) URLs only. Return the string as given.
  static string? SafeReturnUrl(string? raw) =>
      Uri.TryCreate(raw, UriKind.Absolute, out var uri)
      && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
          ? raw
          : null;

  // Render the connect page and the error page from the HTML templates below.
  static string ConfirmPage(string walletAddress) =>
      File.ReadAllText("Views/bitrobot-connect.html").Replace("{{WALLET_ADDRESS}}", walletAddress);

  static string ErrorPage(string? error, string? reason) =>
      File.ReadAllText("Views/bitrobot-connect-error.html")
          .Replace("{{ERROR}}", error ?? "Something went wrong")
          .Replace("{{REASON}}", reason ?? "unknown");

  record ConfirmResponse(
      [property: JsonPropertyName("status")] string? Status,
      [property: JsonPropertyName("return_url")] string? ReturnUrl,
      [property: JsonPropertyName("auth_code")] string? AuthCode,
      [property: JsonPropertyName("error")] string? Error,
      [property: JsonPropertyName("reason")] string? Reason);
  ```
</CodeGroup>

## Stateless hold

If your API is stateless, store `request_id` and `nonce` in a table instead of the session. Keep one row per user, replace it on each new start, and check expiry on read.

```sql theme={null}
CREATE TABLE bitrobot_connect_attempts (
  id          TEXT PRIMARY KEY,
  user_id     TEXT      NOT NULL REFERENCES users (id),
  request_id  TEXT      NOT NULL,
  nonce       TEXT      NOT NULL,
  expires_at  TIMESTAMP NOT NULL,
  created_at  TIMESTAMP NOT NULL,
  updated_at  TIMESTAMP NOT NULL
);

-- One row per user.
CREATE UNIQUE INDEX index_bitrobot_connect_attempts_on_user_id
  ON bitrobot_connect_attempts (user_id);
```

```ruby theme={null}
# app/models/bitrobot_connect_attempt.rb
class BitrobotConnectAttempt < ApplicationRecord
  TTL = 15.minutes

  belongs_to :user

  validates :request_id, presence: true
  validates :nonce, presence: true

  scope :active, -> { where(expires_at: Time.current..) }

  # Replaces whatever the user had in flight.
  def self.start!(user:, request_id:, nonce:)
    attempt = find_or_initialize_by(user_id: user.id)
    attempt.update!(request_id: request_id, nonce: nonce, expires_at: TTL.from_now)
    attempt
  end

  # The live hold for this user, or nil once it has lapsed.
  def self.active_for(user)
    active.find_by(user_id: user.id)
  end
end
```

```ruby theme={null}
# In your controller. safe_return_url is the helper from the Ruby (Sinatra) example above.
def confirm
  attempt = BitrobotConnectAttempt.active_for(current_user)
  return render_error(:not_found, "no_attempt") if attempt.nil?

  body = Bitrobot::Client.new.confirm_wallet_connection(
    request_id: attempt.request_id,
    nonce: attempt.nonce,
    wallet_address: current_user.wallet_address,
    subnet_user_id: current_user.id.to_s
  )

  # Branch on auth_code, not the status. Never log or render auth_code.
  return_url = safe_return_url(body["return_url"])
  if body["auth_code"].to_s.empty? || return_url.nil?
    return render_error(:bad_request, body["reason"])
  end

  attempt.destroy

  # Redirect to return_url verbatim, fragment included.
  redirect_to return_url, allow_other_host: true, status: :see_other
end
```

## Pages you render

Two pages: the connect page the user lands on, and an error page.

### Connect page

Show the wallet address in full and one button that POSTs to your confirm handler. Include your framework's CSRF token in the form.

<CodeGroup>
  ```html Plain HTML theme={null}
  <!-- templates/bitrobot-connect.html: substitute the wallet address with your template engine -->
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>Connect to BitRobot</title>
    </head>
    <body>
      <main>
        <h1>Connect your wallet to BitRobot</h1>
        <p>
          BitRobot will link the wallet you use here to your BitRobot account, so the
          Subnet Points and Bolts we grant you reach it.
        </p>
        <p>Wallet: <code style="word-break: break-all">{{WALLET_ADDRESS}}</code></p>
        <form method="post" action="/bitrobot/connect/confirm">
          <!-- Include your framework's CSRF token here -->
          <button type="submit">Connect</button>
        </form>
      </main>
    </body>
  </html>
  ```

  ```tsx React theme={null}
  // BitRobotConnectPage.tsx: for an SPA. Your confirm handler returns JSON: return_url on success,
  // error on failure, and never auth_code.
  import { useState } from "react";

  type Props = {
    walletAddress: string;
    csrfToken?: string;
  };

  // Allow http(s) URLs only. Return the string as given.
  function safeReturnUrl(raw: unknown): string | null {
    if (typeof raw !== "string" || !raw) return null;
    try {
      const url = new URL(raw);
      return url.protocol === "https:" || url.protocol === "http:" ? raw : null;
    } catch {
      return null;
    }
  }

  export function BitRobotConnectPage({ walletAddress, csrfToken }: Props) {
    const [submitting, setSubmitting] = useState(false);
    const [error, setError] = useState<string | null>(null);

    async function connect(event: React.FormEvent) {
      event.preventDefault();
      setSubmitting(true);
      setError(null);

      const response = await fetch("/bitrobot/connect/confirm", {
        method: "POST",
        headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
      });
      const body = await response.json();
      const href = safeReturnUrl(body.return_url);
      if (!href) {
        setSubmitting(false);
        setError(
          typeof body?.error === "string"
            ? body.error
            : "Could not connect to BitRobot. Please try again."
        );
        return;
      }

      window.location.replace(href);
    }

    return (
      <main>
        <h1>Connect your wallet to BitRobot</h1>
        <p>
          BitRobot will link the wallet you use here to your BitRobot account, so the
          Subnet Points and Bolts we grant you reach it.
        </p>
        <p>
          Wallet: <code style={{ wordBreak: "break-all" }}>{walletAddress}</code>
        </p>
        {error ? <p role="alert">{error}</p> : null}
        <form onSubmit={connect}>
          <button type="submit" disabled={submitting}>
            {submitting ? "Connecting…" : "Connect"}
          </button>
        </form>
      </main>
    );
  }
  ```
</CodeGroup>

### Error page

```html Plain HTML theme={null}
<!-- templates/bitrobot-connect-error.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Couldn't connect to BitRobot</title>
  </head>
  <body>
    <main>
      <h1>Couldn't connect to BitRobot</h1>
      <p>{{ERROR}}</p>
      <p><small>Reason: <code>{{REASON}}</code></small></p>
      <p><a href="https://app.bitrobot.ai/profile">Go back to BitRobot</a> and press Connect again.</p>
    </main>
  </body>
</html>
```

## Returning from login

Store the destination, query string included, before redirecting to login, and return the user to it after sign-in.

```tsx theme={null}
// The guard: remember the destination, including its query string.
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { ready, authenticated } = useAuth();
  const location = useLocation();

  if (!ready) return <Loading />;
  if (!authenticated) {
    return (
      <Navigate to="/login" replace state={{ from: location.pathname + location.search }} />
    );
  }
  return children;
}
```

```tsx theme={null}
// The login page: accept an in-app path only.
// One leading slash, not followed by another slash or a backslash.
const isInAppPath = (v: unknown): v is string =>
  typeof v === "string" && /^\/(?![/\\])/.test(v);

const from = (location.state as { from?: unknown } | null)?.from;
const destination = isInAppPath(from) ? from : "/dashboard";

// After a successful sign-in:
navigate(destination, { replace: true });
```

For server-rendered apps, store `request.fullpath` before the login redirect and redirect to it afterwards. Accept an in-app path only:

```ruby theme={null}
RETURN_PATH_FORMAT = %r{\A/(?!/|\\|%2[fF]|%5[cC])[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*\z}
```

## Tests

RSpec examples against a confirm handler that returns JSON. `park_attempt` stores a hold for `user`; `confirm_url`, `success_body`, `callback_url`, and `auth_code` are fixtures. Translate the assertions to your framework.

**1. The handler ignores a `request_id` and `nonce` supplied by the caller.**

```ruby theme={null}
it "ignores a request_id and nonce supplied by the caller" do
  park_attempt # holds request_id + nonce for `user`
  stub = stub_request(:post, confirm_url)
    .with(body: hash_including("request_id" => request_id, "nonce" => nonce))
    .to_return(status: 200, body: success_body)

  post "/api/v1/bitrobot_connect/confirm",
       params: { request_id: "01JZ8ZP9K3QF7XN2M4RB6TVWCE", nonce: "attacker-nonce" },
       headers: auth_headers_for(user)

  expect(stub).to have_been_requested # the held ids, not the posted ones
  expect(response).to have_http_status(:ok)
end
```

**2. One user's hold is unusable by another.**

```ruby theme={null}
it "never reads another user's hold" do
  park_attempt(for_user: create(:user))

  post "/api/v1/bitrobot_connect/confirm", headers: auth_headers_for(user)

  expect(response).to have_http_status(:not_found)
  expect(response.parsed_body.dig("error", "code")).to eq("no_attempt")
  expect(a_request(:post, confirm_url)).not_to have_been_made
end
```

**3. A lapsed hold is treated as no hold.**

```ruby theme={null}
it "treats a lapsed hold as no hold at all" do
  park_attempt(expires_at: 1.minute.ago)

  post "/api/v1/bitrobot_connect/confirm", headers: auth_headers_for(user)

  expect(response).to have_http_status(:not_found)
  expect(a_request(:post, confirm_url)).not_to have_been_made
end
```

**4. `return_url` passes through byte for byte, fragment included.**

```ruby theme={null}
it "passes BitRobot's return_url through with its fragment byte-for-byte" do
  park_attempt
  stub_request(:post, confirm_url).to_return(status: 200, body: success_body)

  post "/api/v1/bitrobot_connect/confirm", headers: auth_headers_for(user)

  expect(response.parsed_body["return_url"]).to eq(callback_url)
  expect(URI.parse(response.parsed_body["return_url"]).fragment).to eq("ac=#{auth_code}")
end
```

**5. The authorization code appears only inside `return_url`, and never in logs.**

```ruby theme={null}
it "never renders the auth code as a field of its own, nor logs it" do
  park_attempt
  stub_request(:post, confirm_url).to_return(status: 200, body: success_body)
  logged = []
  allow(Rails.logger).to receive(:warn) { |msg| logged << msg.to_s }
  allow(Rails.logger).to receive(:error) { |msg| logged << msg.to_s }

  post "/api/v1/bitrobot_connect/confirm", headers: auth_headers_for(user)

  expect(response.parsed_body).not_to have_key("auth_code")
  expect(response.body.scan(auth_code).length).to eq(1) # only inside return_url
  expect(logged.join("\n")).not_to include(auth_code)
end
```

Also cover:

* A `409 already_confirmed` with `auth_code` is treated as success.
* A `200` without `auth_code` is treated as a failure.
* A non-`http(s)` `return_url` is refused.
* The hold is cleared after a successful confirm and kept after a failed one.
