BrandGhost
Vercel Eve Tutorial for C# Developers: Build an ASP.NET Core Agent Client

Vercel Eve Tutorial for C# Developers: Build an ASP.NET Core Agent Client

This Vercel Eve tutorial builds a small ASP.NET Core application that asks a separately hosted agent to explain a fictional equipment inspection. The interesting part is not the pump. It is connecting two runtimes without losing track of transport ownership, response consumption, or the difference between receiving JSON and receiving a valid application result.

The application will accept only one synthetic equipment identifier. It will request a structured summary, preserve the agent session identifier in its response, and report downstream failures instead of manufacturing an empty successful answer. There are no maintenance commands, notifications, or production records in this example.

I maintain NexusLabs.Eve, the independent .NET client used in these examples. It is not an official Vercel SDK.

These examples were reviewed against public, tagged source. They have not been compiled or executed for this article. Model access, server startup, and end-to-end execution still need verification in your own validation environment.

Vercel Eve Tutorial: Establish the Version and Service Boundary

For this Vercel Eve tutorial, use .NET 10 with C# 14 and NexusLabs.Eve version 0.1.0-alpha-0014, whose published package targets .NET 10 (package metadata). The reviewed public client source is tagged v0.1.0-alpha.14, and its compatibility reference is Eve 0.63.0 (pinned compatibility documentation).

Do not substitute a floating server version just because the installation command permits it. The client declares accepted agent-info schemas 1 through 4, which is a bounded protocol contract rather than unrestricted future compatibility (protocol source). Keeping the server and client pins together makes the tutorial's API references coherent.

There are two projects. The Eve project contains the agent's instructions and configuration. The ASP.NET Core project receives an application request and calls that agent over HTTP. The .NET client does not move Eve's Node runtime into your web process (client scope).

That separation is useful, but it has a cost. A single-process application has fewer deployment boundaries; a remote agent makes its authoring and execution ownership explicit but introduces network failure and version coordination. This walkthrough demonstrates the second shape without arguing that every model request needs it.

Create a directory named EquipmentWeb and save this complete project file as EquipmentWeb.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="NexusLabs.Eve" Version="0.1.0-alpha-0014" />
  </ItemGroup>
</Project>

The web SDK supplies the ASP.NET Core application environment. The explicit language and package versions document the assumptions under which the following files are written. There is no additional application framework, generated adapter package, or hidden helper library.

Think of the web project as a translation boundary. An HTTP caller asks for a summary of one known item; the adapter expresses that request to the agent; the endpoint returns a small application-owned response. The caller does not need to understand the agent's event stream to use that endpoint. The cost is that the adapter must define what counts as an acceptable answer rather than forwarding every downstream payload indiscriminately.

Keep the TypeScript project in a separate directory named EquipmentAgent. The remaining examples identify their destination files individually. Neither project should receive the other's source files.

Create a Public, Synthetic Read-Only Agent

The public client fixture pins Eve 0.63.0, AI SDK 7.0.105, just-bash 3.1.0, and Node 24.x (fixture manifest). This Vercel Eve tutorial uses those dependency pins as its baseline, not the fixture's execution results as proof that our newly authored agent works.

Save this complete file as EquipmentAgent/package.json:

{
  "name": "synthetic-equipment-agent",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "eve build",
    "start": "eve start"
  },
  "dependencies": {
    "ai": "7.0.105",
    "eve": "0.63.0",
    "just-bash": "3.1.0"
  },
  "engines": {
    "node": "24.x"
  }
}

Here, private prevents accidental npm publication; it does not describe confidential data. All scenario content below is independently authored and fictional. No real equipment service or external account is called to retrieve an inspection.

Eve 0.63.0 uses agent/instructions.md for required instructions and supports agent/agent.ts for optional model configuration (pinned authoring layout). Create the agent subdirectory and save the following complete file as EquipmentAgent/agent/agent.ts:

import { defineAgent } from "eve";

export default defineAgent({
  model: "openai/gpt-5.6-luna-fast",
});

The model identifier follows the pinned upstream example, not a claim that this model is available to every account (configuration example). Provider or gateway credentials must be configured separately using the server's documented mechanism. Do not copy credentials into these files.

Save the following paragraph, without the quotation marker, as the entire contents of EquipmentAgent/agent/instructions.md:

You explain synthetic equipment inspection notes. The only equipment is pump-demo-01. Its fictional inspection note says: a protective cover was reported loose. The recommendation is: ask a qualified technician to assess the equipment. Always identify these observations as fictional. Do not infer operating safety, perform actions, access external records, or invent additional equipment. When a structured result is requested, return equipmentId as pump-demo-01, mocked as true, observation as the fictional inspection note, and recommendation as the technician recommendation.

This is intentionally an instructions-only read-only sample. Adding a database lookup tool would introduce another contract before the HTTP integration is understood. The tradeoff is that the data is fixed, not retrieved from a live inventory. That is appropriate for learning the boundary, but not evidence of a maintenance system.

The manifest provides build and start commands for a validation environment. Dependency restoration and a model-access configuration are prerequisites before using them. Start the pinned server there and use its reported listening address; this article does not assert an unverified default port or a successful startup transcript.

For application architecture background, building agent-backed ASP.NET Core applications illustrates the broader separation between an application endpoint and an agent integration. The protocol used here remains Eve's, not that article's SDK.

Request Typed Output Without Inventing a Success Result

The typed result in this Vercel Eve tutorial is EquipmentSummary. It has four fields: an equipment identifier, a marker that the data is mocked, the observation, and the recommendation. These names must agree across the instructions, JSON Schema, and C# serialization metadata.

The client accepts a JSON Schema through EveTurnOptions.OutputSchema on the message turn, with the server remaining authoritative for validation (structured-output contract). A C# record is therefore not a substitute for sending a schema, and deserialization alone is not a safety guarantee.

Save this complete file as EquipmentWeb/EquipmentAgent.cs:

using System.Text.Json;
using System.Text.Json.Serialization;
using NexusLabs.Eve;

namespace EquipmentWeb;

public sealed record EquipmentSummary(
    string EquipmentId,
    bool Mocked,
    string Observation,
    string Recommendation);

public sealed record EquipmentAnswer(
    string SessionId,
    EquipmentSummary Summary);

[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(EquipmentSummary))]
[JsonSerializable(typeof(EquipmentAnswer))]
internal partial class EquipmentJsonContext : JsonSerializerContext
{
}

public sealed class EquipmentAgent(
    IHttpClientFactory transportFactory,
    IConfiguration configuration)
{
    public async Task<EquipmentAnswer> ReadAsync(
        CancellationToken cancellationToken)
    {
        string host = configuration["Eve:Host"]
            ?? throw new InvalidOperationException("Eve:Host is required.");

        using HttpClient transport = transportFactory.CreateClient("eve");
        EveClient client = new(transport, new EveClientOptions(host));
        EveSession session = client.CreateSession();

        using JsonDocument schema = JsonDocument.Parse("""
            {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "equipmentId": {
                  "type": "string",
                  "enum": ["pump-demo-01"]
                },
                "mocked": {
                  "type": "boolean",
                  "enum": [true]
                },
                "observation": { "type": "string", "minLength": 1 },
                "recommendation": { "type": "string", "minLength": 1 }
              },
              "required": [
                "equipmentId", "mocked", "observation", "recommendation"
              ]
            }
            """);

        EveMessageResponse response = await session.SendAsync(
            EveMessageContent.FromText(
                "Explain the fictional inspection for pump-demo-01. " +
                "Return the requested structured summary."),
            new EveTurnOptions { OutputSchema = schema.RootElement.Clone() },
            cancellationToken);

        EveTurnOutcome outcome =
            await response.GetOutcomeAsync(cancellationToken);

        if (outcome.Status == EveTurnStatus.Failed)
        {
            throw new InvalidOperationException(
                $"Agent session {outcome.SessionId} failed; " +
                $"received {outcome.Events.Count} events.");
        }

        if (outcome.InputRequests.Count != 0)
        {
            throw new InvalidOperationException(
                $"Agent session {outcome.SessionId} requested unexpected human input.");
        }

        EquipmentSummary summary = outcome.DeserializeData(
            EquipmentJsonContext.Default.EquipmentSummary)
            ?? throw new InvalidOperationException(
                $"Agent session {outcome.SessionId} emitted no typed result.");

        if (summary.EquipmentId != "pump-demo-01" ||
            !summary.Mocked ||
            string.IsNullOrWhiteSpace(summary.Observation) ||
            string.IsNullOrWhiteSpace(summary.Recommendation))
        {
            throw new InvalidOperationException(
                $"Agent session {outcome.SessionId} returned invalid demo data.");
        }

        return new EquipmentAnswer(outcome.SessionId, summary);
    }
}

The source-generated context supplies EquipmentJsonContext.Default.EquipmentSummary to the exact DeserializeData API exposed by the tagged outcome class (outcome source). The configured camel-case property naming policy maps JSON equipmentId to the record's EquipmentId contract without asking the agent to emit .NET property casing.

The schema constrains equipmentId and mocked to this scenario, rather than merely declaring their primitive types. The application then checks those values again. That duplication is deliberate: the application's acceptance rule should remain visible even when the downstream contract is violated.

There are three distinct checks here. The schema describes the requested wire shape. Source-generated metadata describes how JSON becomes a .NET value. The final conditional describes what this particular application accepts. Keeping them separate prevents a common misunderstanding: a deserialized object can still be inappropriate for the requested operation. A nonempty recommendation is also not evidence that a real machine is safe. The fictional-data marker belongs in the returned value so downstream consumers can preserve that distinction.

The EquipmentAnswer wrapper intentionally keeps the session identifier outside the summary. The identifier describes the agent interaction, while the summary describes the fictional inspection. This organization lets an application associate diagnostics with the request without asking the model to invent or echo the server's identity. It also avoids placing transport metadata into the equipment description itself.

A streamed session.failed produces EveTurnStatus.Failed in the aggregated outcome instead of necessarily throwing a transport exception (failure documentation). Checking status before reading typed data avoids presenting an earlier partial result from a failed turn as success.

The status enum also includes Waiting, so this example does not equate a conversation waiting for another message with a terminal failure (status source). It requires an actual structured value and rejects any observed human-input request in this deliberately input-free scenario. It does not implement an approval workflow.

Missing data is not replaced with a default record. The deserializer returns a default value when no structured result was emitted, which makes the explicit null check important (deserialization implementation). Malformed JSON or incompatible values remain exceptions, not fictional successful summaries.

Vercel Eve Tutorial: Wire the ASP.NET Core Endpoint

The adapter keeps its HttpClient alive through the awaited outcome aggregation, matching the caller-owned transport requirement (getting-started ownership contract). Disposing transport immediately after SendAsync would put an active response at risk.

The named-client registration keeps HTTP policy in application startup. For the underlying lifecycle concepts, see IHttpClientFactory named clients and DI patterns. We do not store a request-specific transport in a singleton or return an unconsumed response beyond its owning scope.

Save this complete file as EquipmentWeb/Program.cs:

using EquipmentWeb;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("eve", transport =>
{
    transport.Timeout = TimeSpan.FromSeconds(90);
});
builder.Services.AddScoped<EquipmentAgent>();
builder.Services.AddProblemDetails();
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(
        0, EquipmentJsonContext.Default);
});

WebApplication app = builder.Build();
app.UseExceptionHandler();

app.MapPost(
    "/equipment/{equipmentId}/summary",
    async Task<IResult> (
        string equipmentId,
        EquipmentAgent agent,
        CancellationToken cancellationToken) =>
    {
        if (equipmentId != "pump-demo-01")
        {
            return Results.Problem(
                statusCode: StatusCodes.Status400BadRequest,
                title: "Only pump-demo-01 is supported by this synthetic demo.");
        }

        EquipmentAnswer answer = await agent.ReadAsync(cancellationToken);
        return Results.Json(
            answer, EquipmentJsonContext.Default.EquipmentAnswer);
    });

app.Run();

Configure Eve:Host before starting the web application. For a local validation environment, the .NET configuration argument can be passed as --Eve:Host=http://localhost:PORT, replacing PORT with the actual agent listener. It is a server-controlled setting, not a URL supplied by an incoming user request.

After both services are running in that environment, send an empty-body POST to /equipment/pump-demo-01/summary on the web application's reported address. The route supplies the complete application input, so there is no request DTO or JSON body to create. Inspect the returned summary object rather than assuming an assistant's conversational text contains the expected fields. A successful response should remain clearly labeled as mock data.

The endpoint uses POST because asking the agent starts work, even though the sample's equipment data is read-only. The input route permits only the fictional identifier. The application does not accept arbitrary prompts, arbitrary equipment names, or arbitrary destination hosts.

Minimal APIs keep this example compact; controllers provide another organizational option when application structure warrants them. Minimal APIs versus controllers provides that architectural context without changing the adapter's Eve contract.

Unhandled downstream failures reach the exception middleware rather than a 200 response containing an invented summary. This teaching example intentionally does not map every upstream status to a public error taxonomy. Production error handling should preserve diagnostic evidence internally while withholding raw downstream bodies from users.

The 90-second HttpClient timeout is an illustrative per-HTTP-operation setting, not an overall endpoint or agent-turn deadline; with ResponseHeadersRead, subsequent stream consumption needs a separate cancellation deadline. This example passes the inbound request cancellation token but does not configure an overall timed deadline. Cancelling that token controls local consumption; it does not itself stop Eve's durable turn (local cancellation contract). Server cancellation is a separate concern outside this walkthrough.

Read the Result Once and Verify the Boundary

In this Vercel Eve tutorial, one response has one consumer. GetOutcomeAsync aggregates the response stream, while direct enumeration is an alternative single-use path, not an additional pass over the same response (response implementation).

That ownership rule affects logging too. A logger should not independently enumerate the response before the adapter reads it. If you need to inspect collected events afterward, use the outcome already obtained rather than trying to consume the network stream again.

The stream protocol is NDJSON, not Server-Sent Events (protocol constants). General HttpClient streaming concepts are useful background, but an SSE parser is not an interchangeable implementation here.

For validation, exercise the real endpoint with pump-demo-01 and inspect the four summary fields plus sessionId. Then use an unsupported identifier and verify an explicit client error. Finally, make the agent unavailable and verify that the application does not return a successful fabricated equipment answer.

Those are suggested checks, not reported test results. A successful HTTP connection alone would still leave structured output, model configuration, and application acceptance behavior unproven. Conversely, a source review can catch a wrong overload without demonstrating a running server.

Use a small verification matrix rather than one screenshot of a happy answer. Check an unknown identifier before the outbound call, absence of typed data after consumption, a malformed result, and a downstream failure. These cases distinguish the application's guardrails from the model's instruction-following. Record the exact client and server versions with any execution evidence; otherwise a later dependency upgrade can make an old result difficult to interpret.

The source review for this Vercel Eve tutorial checks the named overload, the JSON Schema property type, the outcome deserializer, and the single-consumer rule. It does not establish network reachability, successful model generation, or a passing deployment. Those require an actual running pair. Keeping that evidence boundary explicit is more useful than presenting a source-compatible snippet as a verified production integration.

Observe the two processes separately. The .NET endpoint owns the inbound request; the agent owns its execution. A disconnected browser, an ASP.NET timeout, and an agent failure are different events. This distinction prevents a useful tutorial from quietly turning into a misleading promise about recovery.

Before adding automatic retries, think about what another send means. HttpClient retry and resilience design supplies background for that decision. This example adds no custom retry or reconnect implementation and performs no equipment mutations.

Frequently Asked Questions

These questions clarify the boundaries of the read-only example. The answers refer to the pinned API rather than assuming newer preview versions behave identically.

Does this Vercel Eve tutorial run the agent inside ASP.NET Core?

No. The .NET library consumes Eve's HTTP surface rather than porting its Node runtime (client scope). The web application and TypeScript agent remain separately configured services.

Is NexusLabs.Eve an official Vercel SDK?

No. I maintain the independent client used here. Its public project describes a framework-neutral .NET HTTP client, not an official Vercel runtime distribution (project overview).

Does a C# record automatically create the output schema?

Not in this example. The schema is explicitly passed through EveTurnOptions.OutputSchema, and source-generated metadata is separately supplied for deserialization (structured-output documentation). Matching property names is part of the application contract.

Can I read live events and then call GetOutcomeAsync?

Not on the same response. The response enforces single-use consumption (enumerator source). Choose aggregation or direct enumeration and retain whatever application result that path produces.

Why check Failed before deserializing the result?

The failure can arrive as a protocol event, with GetOutcomeAsync returning a failed status and preserving events (error contract). Receiving data is not proof that the turn succeeded.

Is this endpoint ready for public deployment?

No. It is a synthetic instructional boundary, not a complete access-control design. Eve is beta and its APIs can change (pinned beta notice). Authentication, authorization, deployment configuration, and execution validation remain separate requirements.

Keep the First Integration Small

The useful result of this Vercel Eve tutorial is a clear division of responsibility. TypeScript configures a fictional agent. ASP.NET Core owns the application endpoint and transport scope. A per-turn schema requests structured output, while application checks decide whether that output is acceptable.

Do not mistake these boundaries for a production safety certification. The examples teach a complete request path without claiming they were executed or expanding into access policy, approval, or recovery. Keeping the first scenario read-only makes those remaining responsibilities easier to see.

How To Build A Personal Website in Blazor: An ASP.NET Core Tutorial

Learn how to build a personal website in Blazor and ASP.NET Core to show case your skills and experiences. Get started with our ASP.NET Core tutorial today!

How to Build An ASP.NET Core Web API: A Practical Beginner's Tutorial

Learn how to build an ASP.NET core web API! This tutorial for beginners will guide you through setting up the project to building the API endpoints.

Vercel Eve Explained: Durable AI Agents and C# Integration

Understand Vercel Eve's durable agent runtime, filesystem-first design, replay boundaries, and how C# applications connect through a version-pinned HTTP client.

An error has occurred. This application may no longer respond until reloaded. Reload