Skip to content

Quickstart (agent-native)

This is the shortest path to linked agent feedback: ratings that cannot be re-pointed at another Langfuse session, with your Langfuse write key staying on your server only.

Package: @uservane/vercel-ai. Server helper: @uservane/vercel-ai/server. Optional round-trip: @uservane/langfuse-push.

Terminal window
npm install @uservane/vercel-ai

Use the publishable key in the browser (uv_pk_...). Prop name is apiKey.

// app/providers.tsx (client component)
"use client";
import { UserVaneProvider } from "@uservane/vercel-ai";
import type { ReactNode } from "react";
export function Providers({ children }: { children: ReactNode }) {
return (
<UserVaneProvider
apiKey={process.env.NEXT_PUBLIC_USERVANE_KEY!}
surveySlug="post-task"
>
{children}
</UserVaneProvider>
);
}

By default the provider renders <InlineFeedback /> under your children (0 CLS slot, dismiss-on-next-action). Pass renderInline={false} and drive UI yourself with isTaskOpen / active from useUserVane().

Identify when you know the user:

"use client";
import { useUserVane } from "@uservane/vercel-ai";
import { useEffect } from "react";
export function Identify({ userId }: { userId: string }) {
const { identify } = useUserVane();
useEffect(() => {
identify({ userId, traits: { plan: "pro" } });
}, [identify, userId]);
return null;
}

Without userId, the SDK mints an anonymous device/conversation key so the ask does not go dark. Suppression is weaker across devices than an identified user.

2. Resolve the task when the outcome is real

Section titled “2. Resolve the task when the outcome is real”

Call resolveTask() only when the user’s outcome is confirmed. Not when the agent “submitted a request.” Not when a tool returned. For money, refund, or complaint flows, resolution is the confirmed outcome (refund applied, ticket resolved), not “handed to a human.”

"use client";
import { useUserVane } from "@uservane/vercel-ai";
export function onRefundConfirmed() {
// inside a client component:
const { resolveTask } = useUserVane();
resolveTask({ outcome: "refunded" });
}

Until resolveTask fires, the task is open and the ask does not show (blocking-capable default). Mechanical triggers stay off unless you set nonBlocking on the provider.

Headless hosts: construct AgentController with headlessMode: true and supply setHeadlessContract({ isTaskOpen, onDismiss }), or the SDK refuses to show.

3. Server: mint a session-bound token (secret key)

Section titled “3. Server: mint a session-bound token (secret key)”

The secret key (uv_sk_...) is server-only. Never put it in a client bundle or NEXT_PUBLIC_*.

On the server that runs your model, choose the same sessionId you pass to Langfuse propagateAttributes, mint tokens with that id, and thread showToken + sessionId to the client.

// app/api/chat/route.ts (server)
import { mintFeedbackToken } from "@uservane/vercel-ai/server";
import { propagateAttributes } from "@langfuse/tracing";
export async function POST(req: Request) {
const { userId, messages } = await req.json();
const sessionId = crypto.randomUUID();
return propagateAttributes({ sessionId }, async () => {
// ... generateText / streamText with your model ...
const { tokens } = await mintFeedbackToken({
secretKey: process.env.USERVANE_SECRET_KEY!,
respondentId: userId,
sessionId,
surveyId: "surv_post_task", // optional: limit to one survey id
});
const showToken = tokens["surv_post_task"];
// Stream showToken + sessionId + surveyId to the client (data part / metadata).
return Response.json({ sessionId, surveyId: "surv_post_task", showToken });
});
}

mintFeedbackToken calls POST /v1/sdk/tokens with Authorization: Bearer <secret>. Empty tokens means gated (suppression, quiet period, cap), not a transport error: do not show.

Client: bind before resolve:

"use client";
import { useUserVane } from "@uservane/vercel-ai";
import { useEffect } from "react";
export function BindSession(props: {
surveyId: string;
showToken: string;
sessionId: string;
}) {
const { bind } = useUserVane();
useEffect(() => {
if (!props.showToken) return;
bind({
surveyId: props.surveyId,
showToken: props.showToken,
sessionId: props.sessionId,
});
}, [bind, props.surveyId, props.showToken, props.sessionId]);
return null;
}

controller.bind({ surveyId, showToken, sessionId }) installs the server-minted token. Submit then carries the bound sessionId. Ingestion rejects a client-altered session id that does not match the token. Without bind, bootstrap still works; those responses store as unlinked.

4. Optional: Langfuse round-trip on your server

Section titled “4. Optional: Langfuse round-trip on your server”

Run pushPendingScores on a cron or route you host. It holds your UserVane secret key and your Langfuse keys. UserVane never receives the Langfuse key.

// scripts/push-uservane-scores.ts (server / cron)
import { pushPendingScores } from "@uservane/langfuse-push";
const summary = await pushPendingScores({
uservane: {
secretKey: process.env.USERVANE_SECRET_KEY!,
},
langfuse: {
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!,
},
// scoreName defaults to "uservane.satisfaction"
});
console.log(summary); // { delivered, failed }

That polls GET /v1/sdk/pending-scores, writes a session-level Langfuse score, and acks via POST /v1/sdk/pending-scores/ack. Unlinked rows never enter the queue.

Dashboard-only install is complete without this step.

StepWhereKey
UserVaneProvider + InlineFeedbackBrowserPublishable
resolveTask at true outcomeBrowserPublishable
mintFeedbackTokenYour serverSecret
bind({ surveyId, showToken, sessionId })Browser(token from server)
pushPendingScoresYour server / cronSecret + Langfuse