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

# Signing requests

> Create Ed25519 signatures for sensitive Anchorage Digital API requests.

export const RequestSigner = ({defaultMethod = "GET", defaultPath = "/v2/apikey", defaultBody = "", idempotencyField = null, compact = false, title = "Request signer", subtitle = "Generate fresh Api-Timestamp and Api-Signature headers in your browser.", autoFillPlayground = true} = {}) => {
  const STORAGE_KEY = "anchorage-request-signer-v1";
  const SIGNATURE_TTL_SECONDS = 60;
  const REFERENCE = {
    seed: "0101010101010101010101010101010101010101010101010101010101010101",
    publicKey: "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
    timestamp: "1577880000",
    method: "POST",
    path: "/v2/transfers?foo=bar&baz=bang",
    body: ""
  };
  const hexToBytes = hex => {
    const clean = (hex || "").replace(/^0x/i, "").replace(/\s+/g, "");
    if (!(/^[0-9a-fA-F]*$/).test(clean) || clean.length % 2 !== 0) {
      throw new Error("Signing key seed must be valid hex");
    }
    const out = new Uint8Array(clean.length / 2);
    for (let i = 0; i < out.length; i++) {
      out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
    }
    return out;
  };
  const bytesToHex = bytes => Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
  const buildEffectiveBody = (raw, injectField, injectValue) => {
    const trimmed = (raw || "").trim();
    if (!trimmed && !injectField) return {
      effective: "",
      kind: "empty"
    };
    let parsed;
    try {
      parsed = trimmed ? JSON.parse(trimmed) : {};
    } catch {
      return {
        effective: raw,
        kind: "raw",
        injectionSkipped: !!injectField && !!injectValue
      };
    }
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
      return {
        effective: JSON.stringify(parsed),
        kind: "json",
        injectionSkipped: !!injectField && !!injectValue
      };
    }
    if (injectField && injectValue) {
      parsed = {
        ...parsed,
        [injectField]: injectValue
      };
    }
    return {
      effective: JSON.stringify(parsed),
      kind: "json",
      injectionSkipped: false
    };
  };
  const setReactFieldValue = (el, value) => {
    if (!el) return false;
    let proto;
    if (el.tagName === "TEXTAREA") proto = HTMLTextAreaElement.prototype; else if (el.tagName === "SELECT") proto = HTMLSelectElement.prototype; else proto = HTMLInputElement.prototype;
    const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
    if (!setter) return false;
    setter.call(el, value);
    el.dispatchEvent(new Event("input", {
      bubbles: true
    }));
    el.dispatchEvent(new Event("change", {
      bubbles: true
    }));
    return true;
  };
  const findPlaygroundInput = name => {
    if (typeof document === "undefined") return null;
    const target = name.toLowerCase();
    const isOurs = el => !!el.closest("[data-anchorage-signer]");
    const fields = document.querySelectorAll("input, textarea, select");
    for (const el of fields) {
      if (isOurs(el)) continue;
      const ph = (el.getAttribute("placeholder") || "").trim().toLowerCase();
      if (!ph) continue;
      if (ph === target || ph === `enter ${target}` || ph === `select ${target}` || ph === `enter your ${target}`) {
        return el;
      }
    }
    for (const el of fields) {
      if (isOurs(el)) continue;
      const candidates = [el.getAttribute("name"), el.getAttribute("aria-label"), el.getAttribute("data-key"), el.id].filter(Boolean).map(s => s.toLowerCase());
      if (candidates.some(c => c === target)) return el;
    }
    for (const el of fields) {
      if (isOurs(el) || !el.id) continue;
      const lbl = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
      if (lbl && lbl.textContent.trim().toLowerCase() === target) return el;
    }
    const labelish = document.querySelectorAll("label, span, code, div, p, dt, b, strong");
    for (const el of labelish) {
      if (isOurs(el)) continue;
      const text = (el.textContent || "").trim();
      if (text.toLowerCase() !== target) continue;
      let cursor = el.parentElement;
      for (let depth = 0; depth < 6 && cursor; depth++) {
        const candidate = cursor.querySelector("input, textarea, select");
        if (candidate && !isOurs(candidate)) return candidate;
        cursor = cursor.parentElement;
      }
    }
    return null;
  };
  const isPlaygroundMounted = () => typeof document !== "undefined" && !!document.querySelector(".text-playground-input, [aria-label^='Enter '], [aria-label^='Select ']");
  const openPlaygroundIfNeeded = () => {
    if (typeof document === "undefined") return false;
    if (isPlaygroundMounted()) return false;
    const btn = document.querySelector(".tryit-button") || Array.from(document.querySelectorAll("button")).find(b => (/^try it$/i).test((b.textContent || "").trim()));
    if (btn) {
      btn.click();
      return true;
    }
    return false;
  };
  const fillPlaygroundFromSigned = (signedPayload, accessKeyValue) => {
    if (typeof document === "undefined") return {
      status: "missing",
      filled: 0,
      total: 0
    };
    document.querySelectorAll("details:not([open])").forEach(d => {
      if (!d.closest("[data-anchorage-signer]")) d.open = true;
    });
    const fields = [];
    if (accessKeyValue) fields.push(["Api-Access-Key", accessKeyValue]);
    fields.push(["Api-Timestamp", signedPayload.timestamp]);
    fields.push(["Api-Signature", signedPayload.signature]);
    if (signedPayload.bodyKind === "json" && signedPayload.body) {
      try {
        const parsed = JSON.parse(signedPayload.body);
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
          for (const [k, v] of Object.entries(parsed)) {
            if (v === null || typeof v === "object") continue;
            fields.push([k, String(v)]);
          }
        }
      } catch {}
    }
    let filled = 0;
    for (const [name, value] of fields) {
      const input = findPlaygroundInput(name);
      if (input && setReactFieldValue(input, value)) filled++;
    }
    if (fields.length === 0) return {
      status: "missing",
      filled: 0,
      total: 0
    };
    if (filled === 0) return {
      status: "missing",
      filled: 0,
      total: fields.length
    };
    if (filled < fields.length) return {
      status: "partial",
      filled,
      total: fields.length
    };
    return {
      status: "filled",
      filled,
      total: fields.length
    };
  };
  const generateUuid = () => {
    if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
      return crypto.randomUUID();
    }
    const bytes = new Uint8Array(16);
    crypto.getRandomValues(bytes);
    bytes[6] = bytes[6] & 0x0f | 0x40;
    bytes[8] = bytes[8] & 0x3f | 0x80;
    const hex = bytesToHex(bytes);
    return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
  };
  const loadSession = () => {
    if (typeof window === "undefined") return {};
    try {
      const raw = window.sessionStorage.getItem(STORAGE_KEY);
      return raw ? JSON.parse(raw) : {};
    } catch {
      return {};
    }
  };
  const saveSession = next => {
    if (typeof window === "undefined") return;
    try {
      window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(next));
    } catch {}
  };
  const [accessKey, setAccessKey] = useState("");
  const [seedHex, setSeedHex] = useState("");
  const [method, setMethod] = useState(defaultMethod);
  const [path, setPath] = useState(defaultPath);
  const [body, setBody] = useState(defaultBody);
  const [idempotencyKey, setIdempotencyKey] = useState("");
  const [hydrated, setHydrated] = useState(false);
  const [edModule, setEdModule] = useState(null);
  const [loadError, setLoadError] = useState(null);
  const [signed, setSigned] = useState(null);
  const [signError, setSignError] = useState(null);
  const [signing, setSigning] = useState(false);
  const [selfTest, setSelfTest] = useState({
    status: "pending",
    detail: ""
  });
  const [copied, setCopied] = useState(null);
  const [showSeed, setShowSeed] = useState(false);
  const [playgroundFill, setPlaygroundFill] = useState(null);
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    const saved = loadSession();
    if (saved.accessKey) setAccessKey(saved.accessKey);
    if (saved.seedHex) setSeedHex(saved.seedHex);
    setHydrated(true);
  }, []);
  useEffect(() => {
    if (!hydrated) return;
    saveSession({
      accessKey,
      seedHex
    });
  }, [hydrated, accessKey, seedHex]);
  useEffect(() => {
    if (!hydrated) return;
    if (typeof window === "undefined") return;
    let cancelled = false;
    const finish = (mod, err) => {
      if (cancelled) return;
      if (err) setLoadError(err); else setEdModule(mod);
    };
    const ready = window.__anchorageEd25519;
    if (ready) {
      ready.then(mod => finish(mod, null)).catch(e => finish(null, e.message || String(e)));
      return () => {
        cancelled = true;
      };
    }
    const promise = new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.type = "module";
      const cdn = "https://esm.sh/" + "@noble/ed25519@2.1.0";
      script.textContent = 'import * as ed from "' + cdn + '";' + "window.__anchorageEd25519Ready && window.__anchorageEd25519Ready(ed);";
      script.onerror = () => reject(new Error("Could not load Ed25519 module"));
      window.__anchorageEd25519Ready = mod => resolve(mod);
      document.head.appendChild(script);
      setTimeout(() => reject(new Error("Ed25519 module load timed out")), 15000);
    });
    window.__anchorageEd25519 = promise;
    promise.then(mod => finish(mod, null)).catch(e => finish(null, e.message || String(e)));
    return () => {
      cancelled = true;
    };
  }, [hydrated]);
  useEffect(() => {
    if (!signed) return;
    setNow(Date.now());
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, [signed]);
  useEffect(() => {
    if (!edModule) return;
    let cancelled = false;
    (async () => {
      try {
        const seedBytes = hexToBytes(REFERENCE.seed);
        const message = new TextEncoder().encode(REFERENCE.timestamp + REFERENCE.method + REFERENCE.path + REFERENCE.body);
        const pub = await edModule.getPublicKeyAsync(seedBytes);
        const sig = await edModule.signAsync(message, seedBytes);
        const verified = await edModule.verifyAsync(sig, message, pub);
        if (cancelled) return;
        const pubMatches = bytesToHex(pub) === REFERENCE.publicKey;
        const ok = verified && pubMatches;
        setSelfTest({
          status: ok ? "pass" : "fail",
          detail: ok ? "Ed25519 is loaded. Sign\u2013verify round-trip and public-key derivation both succeed." : verified ? "Signature verified, but public key did not match the documented test vector." : "Sign\u2013verify round-trip failed. The Ed25519 module may be corrupted."
        });
      } catch (err) {
        if (cancelled) return;
        setSelfTest({
          status: "fail",
          detail: err.message || String(err)
        });
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [edModule]);
  useEffect(() => {
    setSigned(null);
    setSignError(null);
    setPlaygroundFill(null);
  }, [method, path, body, idempotencyKey, seedHex]);
  const handleSign = async () => {
    if (!edModule || signing) return;
    setSigning(true);
    setSignError(null);
    try {
      const seedBytes = hexToBytes(seedHex);
      if (seedBytes.length !== 32) {
        throw new Error(`Signing key seed must be 32 bytes (64 hex chars), got ${seedBytes.length}`);
      }
      const ts = Math.floor(Date.now() / 1000);
      const m = (method || "GET").toUpperCase();
      const p = path || "";
      const {effective, kind, injectionSkipped} = buildEffectiveBody(body, idempotencyField, idempotencyKey);
      const messageStr = String(ts) + m + p + effective;
      const message = new TextEncoder().encode(messageStr);
      const sig = await edModule.signAsync(message, seedBytes);
      const pub = await edModule.getPublicKeyAsync(seedBytes);
      const signedAt = Date.now();
      const payload = {
        timestamp: String(ts),
        signedAt,
        signature: bytesToHex(sig),
        publicKey: bytesToHex(pub),
        message: messageStr,
        body: effective,
        bodyKind: kind,
        injectionSkipped
      };
      setSigned(payload);
      setNow(signedAt);
      if (autoFillPlayground) {
        const opened = openPlaygroundIfNeeded();
        const runFill = () => {
          const result = fillPlaygroundFromSigned(payload, accessKey);
          setPlaygroundFill(prev => prev && prev.filled >= result.filled ? prev : result);
        };
        const firstDelay = opened ? 350 : 0;
        setTimeout(runFill, firstDelay);
        setTimeout(runFill, firstDelay + 400);
        setTimeout(runFill, firstDelay + 1000);
      } else {
        setPlaygroundFill({
          status: "off",
          filled: 0,
          total: 0
        });
      }
    } catch (err) {
      setSignError(err.message || String(err));
    } finally {
      setSigning(false);
    }
  };
  const generateSeed = () => {
    const buf = new Uint8Array(32);
    crypto.getRandomValues(buf);
    setSeedHex(bytesToHex(buf));
    setShowSeed(true);
  };
  const handleGenerateUuid = () => {
    setIdempotencyKey(generateUuid());
  };
  const forgetKeys = () => {
    setAccessKey("");
    setSeedHex("");
    setSigned(null);
    if (typeof window !== "undefined") {
      try {
        window.sessionStorage.removeItem(STORAGE_KEY);
      } catch {}
    }
  };
  const copy = async (label, value) => {
    if (!value) return;
    try {
      await navigator.clipboard.writeText(value);
      setCopied(label);
      setTimeout(() => setCopied(c => c === label ? null : c), 1400);
    } catch {}
  };
  const ageSeconds = signed ? Math.max(0, Math.floor((now - signed.signedAt) / 1000)) : 0;
  const remainingSeconds = signed ? Math.max(0, SIGNATURE_TTL_SECONDS - ageSeconds) : 0;
  const isExpired = signed && remainingSeconds === 0;
  const freshnessTone = !signed ? "idle" : isExpired ? "expired" : remainingSeconds <= 15 ? "warning" : "fresh";
  const canSign = !!(edModule && seedHex && !signing);
  const headerRows = [{
    label: "Api-Access-Key",
    value: accessKey,
    placeholder: "Enter your access key above"
  }, {
    label: "Api-Timestamp",
    value: signed && !isExpired ? signed.timestamp : "",
    placeholder: signed && isExpired ? "Signature expired \u2014 sign again" : "Click Sign request"
  }, {
    label: "Api-Signature",
    value: signed && !isExpired ? signed.signature : "",
    placeholder: signed && isExpired ? "Signature expired \u2014 sign again" : signError || "Click Sign request"
  }];
  const renderField = (label, right, children) => <label className="block">
      <div className="mb-1 flex items-center justify-between gap-2">
        <span className="text-xs font-semibold text-zinc-700 dark:text-zinc-300">{label}</span>
        {right}
      </div>
      {children}
    </label>;
  const renderHeaderRow = (label, value, placeholder, onCopy, isCopied) => <div key={label} className="flex items-center gap-3 px-3 py-2 bg-white dark:bg-zinc-900">
      <code className="shrink-0 text-[11px] font-semibold text-zinc-500 dark:text-zinc-400 w-32">
        {label}
      </code>
      <code className="flex-1 truncate text-xs font-mono text-zinc-900 dark:text-zinc-100">
        {value || <span className="text-zinc-400 dark:text-zinc-600 italic">{placeholder}</span>}
      </code>
      <button type="button" onClick={onCopy} disabled={!value} className={"shrink-0 rounded-md border px-2 py-1 text-[11px] font-medium transition " + (isCopied ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : value ? "border-zinc-950/15 dark:border-white/15 bg-white dark:bg-transparent text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5" : "border-zinc-950/10 dark:border-white/10 text-zinc-400 dark:text-zinc-600 cursor-not-allowed")}>
        {isCopied ? "Copied" : "Copy"}
      </button>
    </div>;
  const renderSelfTestBadge = (status, hasLoadError) => {
    const cls = "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-white text-[10px] font-bold ";
    if (hasLoadError || status === "fail") {
      return <span className={cls + "bg-rose-500"} aria-label="Self-test failed">!</span>;
    }
    if (status === "pass") {
      return <span className={cls + "bg-emerald-500"} aria-label="Self-test passed">OK</span>;
    }
    return <span className={cls + "bg-zinc-400 dark:bg-zinc-600 animate-pulse"} aria-label="Self-test pending">
        ...
      </span>;
  };
  const freshnessClasses = {
    idle: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400",
    fresh: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
    warning: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
    expired: "bg-rose-500/10 text-rose-700 dark:text-rose-400"
  };
  const freshnessDot = {
    idle: "bg-zinc-400",
    fresh: "bg-emerald-500 animate-pulse",
    warning: "bg-amber-500 animate-pulse",
    expired: "bg-rose-500"
  };
  const freshnessLabel = !signed ? "Not signed yet" : isExpired ? "Expired \u2014 sign again" : `Valid for ${remainingSeconds}s`;
  return <div data-anchorage-signer="" className="not-prose my-6 rounded-2xl border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-zinc-900 shadow-sm overflow-hidden">
      <div className="flex items-start justify-between gap-4 px-5 py-4 border-b border-zinc-950/10 dark:border-white/10 bg-zinc-50 dark:bg-zinc-950/40">
        <div>
          {!compact && <div className="text-xs font-bold tracking-wider uppercase text-[#5580F6]">
              Live tool
            </div>}
          <h3 className="mt-1 text-base font-semibold text-zinc-950 dark:text-white">
            {title}
          </h3>
          <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">{subtitle}</p>
        </div>
        <span className={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium tabular-nums " + freshnessClasses[freshnessTone]}>
          <span className={"h-1.5 w-1.5 rounded-full " + freshnessDot[freshnessTone]} />
          {freshnessLabel}
        </span>
      </div>

      <div className={"px-5 " + (compact ? "py-3 space-y-4" : "py-4 space-y-5")}>
        <div className="flex items-start gap-3 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2.5 text-xs text-amber-900 dark:text-amber-200">
          <svg className="h-4 w-4 shrink-0 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
            <line x1="12" y1="9" x2="12" y2="13" />
            <line x1="12" y1="17" x2="12.01" y2="17" />
          </svg>
          <div className="flex-1 leading-relaxed">
            Your signing key never leaves this browser. Signing happens locally with{" "}
            <code className="text-[11px]">@noble/ed25519</code> and is held only in this tab's{" "}
            <code className="text-[11px]">sessionStorage</code>, which clears when you close the tab.
          </div>
          <button type="button" onClick={forgetKeys} className="shrink-0 rounded-md border border-amber-500/40 bg-white/60 dark:bg-transparent px-2 py-1 text-[11px] font-medium text-amber-900 dark:text-amber-200 hover:bg-white dark:hover:bg-white/5 transition">
            Forget keys
          </button>
        </div>

        <div className="grid gap-4">
          {renderField("API Access Key", null, <input type="text" autoComplete="off" spellCheck={false} value={accessKey} onChange={e => setAccessKey(e.target.value.trim())} placeholder="paste your Api-Access-Key" className="w-full rounded-md border border-zinc-950/15 dark:border-white/15 bg-white dark:bg-zinc-950/50 px-3 py-2 text-sm font-mono text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-[#5580F6]/40 focus:border-[#5580F6]/60" />)}

          {renderField("Ed25519 signing key seed (hex)", <div className="flex items-center gap-2">
              <button type="button" onClick={() => setShowSeed(s => !s)} className="text-[11px] font-medium text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200">
                {showSeed ? "Hide" : "Show"}
              </button>
              <button type="button" onClick={generateSeed} className="rounded-md border border-zinc-950/15 dark:border-white/15 px-2 py-0.5 text-[11px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5 transition">
                Generate test keypair
              </button>
            </div>, <input type={showSeed ? "text" : "password"} autoComplete="off" spellCheck={false} value={seedHex} onChange={e => setSeedHex(e.target.value.trim())} placeholder="64 hex characters" className="w-full rounded-md border border-zinc-950/15 dark:border-white/15 bg-white dark:bg-zinc-950/50 px-3 py-2 text-sm font-mono text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-[#5580F6]/40 focus:border-[#5580F6]/60" />)}

          {idempotencyField && renderField(<span>
                Idempotency key{" "}
                <code className="ml-1 text-[10px] font-mono text-zinc-500 dark:text-zinc-500">
                  body.{idempotencyField}
                </code>
              </span>, <button type="button" onClick={handleGenerateUuid} className="rounded-md border border-zinc-950/15 dark:border-white/15 px-2 py-0.5 text-[11px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5 transition">
                Generate UUID
              </button>, <>
                <input type="text" spellCheck={false} value={idempotencyKey} onChange={e => setIdempotencyKey(e.target.value.trim())} placeholder="e.g. e763a50d-aa82-4ec7-b5a3-89ad0462d248" className="w-full rounded-md border border-zinc-950/15 dark:border-white/15 bg-white dark:bg-zinc-950/50 px-3 py-2 text-sm font-mono text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-[#5580F6]/40 focus:border-[#5580F6]/60" />
                <p className="mt-1.5 text-[11px] text-zinc-500 dark:text-zinc-500">
                  Auto-injected as <code>{idempotencyField}</code> in the JSON body before signing.
                  Reuse the same UUID if you retry a failed request to avoid duplicate operations.
                </p>
              </>)}
        </div>

        <details className="group rounded-lg border border-zinc-950/10 dark:border-white/10 bg-zinc-50/60 dark:bg-zinc-950/30" open={compact}>
          <summary className="cursor-pointer list-none px-3 py-2 text-xs font-semibold text-zinc-700 dark:text-zinc-300 flex items-center justify-between">
            <span>{compact ? "Request being signed" : "Customize the request being signed"}</span>
            <svg className="h-3.5 w-3.5 transition-transform group-open:rotate-180 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <polyline points="6 9 12 15 18 9" />
            </svg>
          </summary>
          <div className="px-3 pb-3 pt-1 grid gap-3">
            <div className="grid grid-cols-[auto_1fr] gap-2">
              <select value={method} onChange={e => setMethod(e.target.value)} className="rounded-md border border-zinc-950/15 dark:border-white/15 bg-white dark:bg-zinc-950/50 px-2 py-2 text-sm font-mono text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-[#5580F6]/40">
                {["GET", "POST", "PUT", "PATCH", "DELETE"].map(m => <option key={m} value={m}>
                    {m}
                  </option>)}
              </select>
              <input type="text" spellCheck={false} value={path} onChange={e => setPath(e.target.value)} placeholder="/v2/transfers?foo=bar" className="rounded-md border border-zinc-950/15 dark:border-white/15 bg-white dark:bg-zinc-950/50 px-3 py-2 text-sm font-mono text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-[#5580F6]/40 focus:border-[#5580F6]/60" />
            </div>
            <textarea spellCheck={false} value={body} onChange={e => setBody(e.target.value)} placeholder="Request body, e.g. {&quot;tradingPair&quot;:&quot;BTC-USD&quot;,&quot;quantity&quot;:&quot;1&quot;,&quot;currency&quot;:&quot;BTC&quot;,&quot;side&quot;:&quot;BUY&quot;}" rows={3} className="w-full rounded-md border border-zinc-950/15 dark:border-white/15 bg-white dark:bg-zinc-950/50 px-3 py-2 text-xs font-mono text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-[#5580F6]/40 focus:border-[#5580F6]/60 resize-y" />
            <p className="text-[11px] text-zinc-500 dark:text-zinc-500">
              Signed over <code>timestamp + METHOD + path + JSON.stringify(body)</code>. JSON bodies are
              auto-canonicalized to match the bytes the server receives.
            </p>
          </div>
        </details>

        <div className="flex items-center justify-between gap-3">
          <button type="button" onClick={handleSign} disabled={!canSign} className="inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-semibold transition" style={canSign ? {
    backgroundColor: "#5580F6",
    color: "#ffffff",
    boxShadow: "0 1px 3px rgba(0,0,0,0.18)"
  } : {
    backgroundColor: "#e4e5e7",
    color: "#9ca3af",
    cursor: "not-allowed"
  }}>
            {signing ? <>
                <svg className="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden="true">
                  <circle cx="12" cy="12" r="9" opacity="0.25" />
                  <path d="M21 12a9 9 0 0 1-9 9" />
                </svg>
                Signing...
              </> : signed && !isExpired ? "Re-sign request" : "Sign request"}
          </button>
          {!edModule && !loadError && <span className="text-[11px] text-zinc-500 dark:text-zinc-500">Loading Ed25519...</span>}
          {!seedHex && edModule && <span className="text-[11px] text-zinc-500 dark:text-zinc-500">
              Add a signing key seed to enable signing
            </span>}
          {signError && <span className="text-[11px] text-rose-600 dark:text-rose-400 truncate max-w-md">
              {signError}
            </span>}
        </div>

        {signed && <>
            <div className="space-y-2">
              <div className="flex items-center justify-between">
                <div className="text-xs font-semibold text-zinc-700 dark:text-zinc-300">
                  Headers to copy into the API playground
                </div>
                {isExpired && <span className="text-[11px] font-medium text-rose-600 dark:text-rose-400">
                    Timestamp older than {SIGNATURE_TTL_SECONDS}s, click Re-sign request
                  </span>}
              </div>
              <div className={"rounded-lg border overflow-hidden divide-y " + (isExpired ? "border-rose-500/30 divide-rose-500/20 opacity-60" : "border-zinc-950/10 dark:border-white/10 divide-zinc-950/10 dark:divide-white/10")}>
                {headerRows.map(row => renderHeaderRow(row.label, row.value, row.placeholder, () => copy(row.label, row.value), copied === row.label))}
              </div>
              {autoFillPlayground && playgroundFill && !isExpired && <div className={"flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-[11px] " + (playgroundFill.status === "filled" ? "border-emerald-500/30 bg-emerald-500/5 text-emerald-700 dark:text-emerald-400" : playgroundFill.status === "partial" ? "border-amber-500/30 bg-amber-500/5 text-amber-800 dark:text-amber-300" : "border-zinc-950/10 dark:border-white/10 bg-zinc-50/60 dark:bg-zinc-950/30 text-zinc-600 dark:text-zinc-400")}>
                  <span className={"h-1.5 w-1.5 rounded-full " + (playgroundFill.status === "filled" ? "bg-emerald-500" : playgroundFill.status === "partial" ? "bg-amber-500" : "bg-zinc-400")} />
                  <span className="flex-1">
                    {playgroundFill.status === "filled" ? `Auto-filled ${playgroundFill.filled} field${playgroundFill.filled === 1 ? "" : "s"} in the playground on this page.` : playgroundFill.status === "partial" ? `Auto-filled ${playgroundFill.filled} of ${playgroundFill.total} playground fields. Use the Copy buttons for the rest.` : "Couldn't find the playground on this page. Use the Copy buttons to paste the headers manually."}
                  </span>
                </div>}
            </div>

            <div className="rounded-lg border border-zinc-950/10 dark:border-white/10 bg-zinc-50/60 dark:bg-zinc-950/30 px-3 py-2.5 space-y-2">
              <div className="flex items-center justify-between">
                <span className="text-[11px] font-semibold text-zinc-500 dark:text-zinc-400">
                  Body to send {signed.body && signed.bodyKind === "json" && idempotencyField ? "(with idempotency key injected)" : ""}
                </span>
                <button type="button" onClick={() => copy("body", signed.body)} disabled={!signed.body} className={"rounded-md border px-2 py-0.5 text-[11px] font-medium transition " + (copied === "body" ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : signed.body ? "border-zinc-950/15 dark:border-white/15 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5" : "border-zinc-950/10 dark:border-white/10 text-zinc-400 dark:text-zinc-600 cursor-not-allowed")}>
                  {copied === "body" ? "Copied" : "Copy"}
                </button>
              </div>
              {signed.body ? <code className="block break-all text-[11px] font-mono text-zinc-800 dark:text-zinc-200">
                  {signed.body}
                </code> : <code className="block text-[11px] font-mono text-zinc-400 dark:text-zinc-600 italic">
                  (no body)
                </code>}
              {signed.injectionSkipped && <p className="text-[11px] text-amber-600 dark:text-amber-400">
                  Could not inject the idempotency key — the body is not a JSON object. Add it
                  manually as <code>{idempotencyField}</code> before sending.
                </p>}
              <p className="text-[11px] text-zinc-500 dark:text-zinc-500">
                Paste this exact body into the playground. Any whitespace difference invalidates the
                signature.
              </p>
            </div>
          </>}

        {!signed && <div className="rounded-lg border border-dashed border-zinc-950/15 dark:border-white/15 px-3 py-4 text-center text-xs text-zinc-500 dark:text-zinc-500">
            Click <strong className="font-semibold text-zinc-700 dark:text-zinc-300">Sign request</strong>{" "}
            to generate fresh <code className="text-[11px]">Api-Timestamp</code> and{" "}
            <code className="text-[11px]">Api-Signature</code> headers. Headers stay valid for{" "}
            {SIGNATURE_TTL_SECONDS} seconds.
          </div>}

        <div className="flex items-center gap-2 rounded-lg border border-zinc-950/10 dark:border-white/10 bg-zinc-50/60 dark:bg-zinc-950/30 px-3 py-2">
          {renderSelfTestBadge(selfTest.status, !!loadError)}
          <span className="text-xs text-zinc-600 dark:text-zinc-400">
            {loadError ? `Could not load Ed25519 library: ${loadError}` : selfTest.status === "pending" ? "Verifying in-browser Ed25519 against the published reference\u2026" : selfTest.detail}
          </span>
        </div>
      </div>
    </div>;
};

Certain Anchorage endpoints require an [Ed25519 signature](https://en.wikipedia.org/wiki/EdDSA#Ed25519) in the request headers alongside the API key.
Signatures are optional unless explicitly required, but are encouraged for all requests for maximum security. The `Api-Signature` is valid for 60 seconds.

## Signature input

Create the signature by concatenating these values:

```text theme={null}
timestamp + uppercase HTTP method + request path including query + request body
```

For requests without a body, omit the body from the signature input. For
requests with a JSON body, sign the **exact bytes sent over the wire** —
that is, `JSON.stringify(body)` with no extra whitespace. Any difference
(stray spaces, trailing newlines, key reordering) will produce a signature
the server rejects.

| Value          | Notes                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------ |
| `timestamp`    | Seconds since the Unix Epoch in UTC. It must be within one minute of the API service time. |
| `method`       | Uppercase HTTP method, such as `GET`, `POST`, or `DELETE`.                                 |
| `request path` | Path and query string, such as `/v2/transfers?foo=bar&baz=bang`.                           |
| `body`         | `JSON.stringify(body)` for requests that include one. Empty for `GET`.                     |

## Sign a request

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    package com.anchorage.api.client;

    import okio.Buffer;
    import org.apache.commons.codec.DecoderException;
    import org.apache.commons.codec.binary.Hex;
    import org.bouncycastle.crypto.CryptoException;
    import org.bouncycastle.crypto.Signer;
    import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
    import org.bouncycastle.crypto.signers.Ed25519Signer;
    import org.json.JSONObject;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.http.client.SimpleClientHttpRequestFactory;
    import org.springframework.web.client.RestTemplate;

    import java.nio.charset.StandardCharsets;
    import java.time.Instant;

    /**
     * A sample java API client to connect to Anchorage API v2
     *
     * Required dependencies:
     *   org.json:json:20210307
     *   org.bouncycastle:bcpkix-jdk15on:1.69
     *   commons-codec:commons-codec:1.15
     *   com.squareup.okio:okio:1.9.0
     */
    public class RestClientWithSigning {

        private RestTemplate restTemplate;

        public static void main(String[] args){
            RestClientWithSigning api = new RestClientWithSigning();
            api.init();
            api.apiCall();
        }

        private void init(){
            SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
            factory.setConnectTimeout(2000);
            factory.setReadTimeout(2000);
            restTemplate = new RestTemplate(factory);
        }

        private void apiCall() {
            try {
                String url = "https://api.anchorage-staging.com/v2/trading/quote";
                String body = createRequestBody();

                HttpEntity<String> request = new HttpEntity<>(body, createHeaders("/v2/trading/quote", HttpMethod.POST, body));
                ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
                if (responseEntity != null && HttpStatus.CREATED == responseEntity.getStatusCode()) {
                    System.out.println(String.format("Getting data from ( %s ) response: %s", url, responseEntity.getBody()));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private String createRequestBody(){
            JSONObject body = new JSONObject();
            body.put("currency", "ETH");
            body.put("quantity", "1.2");
            body.put("side", "BUY");
            body.put("tradingPair", "ETH-USD");
            return body.toString();
        }

        private HttpHeaders createHeaders(String requestPath, HttpMethod httpmethod, String body) throws DecoderException, CryptoException {
            String api_key = "your API Key";
            String signing_key_hex = "Your signing key";

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Api-Access-Key", api_key);

            String timestamp = String.valueOf(Instant.now().getEpochSecond());
            byte[] toSign = decodeMessage(timestamp, httpmethod, requestPath, body);

            String signature = bc_sign(Hex.decodeHex(signing_key_hex), toSign);
            headers.add("Api-Signature", signature);
            headers.add("Api-Timestamp", timestamp);
            return headers;
        }

        private byte[] decodeMessage(String timestamp, HttpMethod httpmethod, String url_path, String body){
            Buffer buffer = new Buffer();
            buffer.write(timestamp.getBytes(StandardCharsets.UTF_8))
                    .write(httpmethod.name().getBytes(StandardCharsets.UTF_8))
                    .write(url_path.getBytes(StandardCharsets.UTF_8))
                    .write(body.getBytes(StandardCharsets.UTF_8));
            return buffer.readByteArray();
        }

        private String bc_sign(byte[] signing_key, byte[] toSign) throws CryptoException {
            Ed25519PrivateKeyParameters privateKeyParameters = new Ed25519PrivateKeyParameters(signing_key);
            Signer signer = new Ed25519Signer();
            signer.init(true, privateKeyParameters);
            signer.update(toSign, 0, toSign.length);
            return Hex.encodeHexString(signer.generateSignature());
        }
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using System;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Text;
    using System.Threading.Tasks;
    using Sodium;

    // Dependencies: Sodium.Core 1.3.1
    // Replace privateKey, publicKey, and Api-Access-Key with your keys

    namespace WebAPIClient
    {
        class Program
        {
            private static readonly HttpClient client = new HttpClient();

            static async Task Main(string[] args)
            {
                await ProcessAnchorageAPISign();
            }

            private static async Task<Object> ProcessAnchorageAPISign()
            {
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                string privateKey = "your private key here";
                string publicKey = "your public key here";
                string signingKey = $"{privateKey}{publicKey}";

                string timestamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
                string path = "/v2/trading/quote/accept";
                string method = "POST";
                string payload = "{\"quoteID\":\"1b1748b0-a62a-46c7-802c-7c61ac222424\",\"side\":\"BUY\"}";
                string msgToSign = $"{timestamp}{method}{path}{payload}";
                var signature = ComputeSignature(Utilities.HexToBinary(signingKey), Encoding.UTF8.GetBytes(msgToSign));

                client.DefaultRequestHeaders.Add("Api-Access-Key", "your api access key");
                client.DefaultRequestHeaders.Add("Api-Timestamp", timestamp);
                client.DefaultRequestHeaders.Add("Api-Signature", signature);

                Uri u = new Uri("https://api.anchorage-staging.com/v2/trading/quote/accept");
                HttpContent c = new StringContent(payload, Encoding.UTF8, "application/json");
                HttpResponseMessage result = await client.PostAsync(u, c);

                var response = await result.Content.ReadAsStringAsync();
                Console.WriteLine(response);
                return response;
            }

            static string ComputeSignature(byte[] privateKey, byte[] toSign)
            {
                byte[] signature = PublicKeyAuth.SignDetached(toSign, privateKey);
                return Utilities.BinaryToHex(signature);
            }
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    require "ed25519"
    require "net/http"
    require "time"

    def hex_to_bin(s)
      [s].pack('H*')
    end

    def bin_to_hex(s)
      s.unpack('H*').first
    end

    private_key_seed_hex = '0101010101010101010101010101010101010101010101010101010101010101'
    public_key_hex = '8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c'
    key_pair_hex = private_key_seed_hex + public_key_hex

    key_pair = hex_to_bin(key_pair_hex)
    signing_key = Ed25519::SigningKey.from_keypair(key_pair)

    timestamp = '1577880000' # Time.now.to_i.to_s

    req = Net::HTTP::Post.new('/v2/transfers?foo=bar&baz=bang')
    req.body = '{"source": {"id": "1c920f4241b78a1d483a29f3c24b6c4c", "type": "VAULT"},
    "assetType": "ETH", "destination": {"id": "55e89d4a644d736b01533a2ea9b32a20", "type": "VAULT"}, "amount": "1000.00000000"}'

    signature = signing_key.sign(timestamp + req.method + req.path + req.body)

    req['Api-Access-Key'] = 'YOUR_ACCESS_KEY'
    req['Api-Timestamp'] = timestamp
    req['Api-Signature'] = bin_to_hex(signature)

    puts bin_to_hex(signature)
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import time
    import requests
    from nacl import signing


    class AnchorageAuth(requests.auth.AuthBase):
        ACCESS_KEY_HEADER = "Api-Access-Key"
        SIGNATURE_HEADER = "Api-Signature"
        TIMESTAMP_HEADER = "Api-Timestamp"

        def __init__(self, access_key: str, signing_key_seed: bytes):
            self.access_key = access_key
            self.signing_key = signing.SigningKey(signing_key_seed)

        def __call__(self, request: requests.PreparedRequest):
            request.headers[self.ACCESS_KEY_HEADER] = self.access_key

            timestamp = str(int(time.time()))
            method = request.method.upper() if request.method else "GET"
            body = request.body or b""
            if isinstance(body, str):
                body = body.encode("utf-8")

            message = b"".join([
                timestamp.encode("utf-8"),
                method.encode("utf-8"),
                request.path_url.encode("utf-8"),
                body,
            ])

            request.headers[self.SIGNATURE_HEADER] = self.signing_key.sign(message).signature.hex()
            request.headers[self.TIMESTAMP_HEADER] = timestamp
            return request
    ```
  </Tab>

  <Tab title="Node.js">
    ```js theme={null}
    const { sign } = require("@noble/ed25519");
    const axios = require("axios");

    class AnchorageClient {
      constructor(accessKey, signingKeySeed) {
        this.accessKey = accessKey;
        this.signingKeySeed = signingKeySeed; // 32-byte hex seed
        this.basePath = "https://api.anchorage-staging.com";
      }

      async sendSignedRequest(method, endpoint, body) {
        const timestamp = Math.floor(Date.now() / 1000);
        const serializedBody = body ? JSON.stringify(body) : "";
        const signatureInput = `${timestamp}${method}${endpoint}${serializedBody}`;
        const messageHex = Buffer.from(signatureInput, "utf8").toString("hex");
        const signature = await sign(messageHex, this.signingKeySeed);
        const signatureHex = Buffer.from(signature).toString("hex");

        return axios({
          method,
          url: this.basePath + endpoint,
          data: body,
          headers: {
            "Api-Access-Key": this.accessKey,
            "Api-Signature": signatureHex,
            "Api-Timestamp": String(timestamp),
            "Content-Type": "application/json",
          },
        }).then((res) => res.data);
      }
    }

    // Example: request and accept a quote
    const client = new AnchorageClient("YOUR_ACCESS_KEY", "YOUR_SIGNING_KEY_SEED");

    const quote = await client.sendSignedRequest("POST", "/v2/trading/quote", {
      tradingPair: "BTC-USD",
      quantity: "1",
      currency: "BTC",
      side: "BUY",
    });

    await client.sendSignedRequest("POST", "/v2/trading/quote/accept", {
      quoteID: quote.data.quoteID,
      side: "BUY",
      allowedSlippage: "0.001",
    });
    ```
  </Tab>
</Tabs>

<Note>
  `JSON.stringify(body)` produces the same compact bytes the server signs
  against, so you don't need to pre-serialize the request manually. The live
  signer above applies the same canonicalization to anything you paste into
  the body field.
</Note>
