A full-stack NCAA March Madness bracket builder where you compete head-to-head against a Salesforce Agentforce AI agent that streams its predictions and reasoning in real time using live ESPN data.
Note: This project requires a Salesforce org with Agentforce enabled and a Heroku app with the Heroku AppLink add-on attached.
- March Madness with Agentforce, Data360, and Heroku
- Table of Contents
- Configuration
- License
- Disclaimer
This application lets users build their own March Madness bracket and then pits it against an AI agent powered by Salesforce Agentforce. The AI streams its picks and reasoning round by round — you can watch it think in real time. Once the tournament is underway, the app pulls live scores from ESPN so the AI can adapt its remaining predictions based on actual results.
The demo showcases two operational modes:
Human Bracket Builder — A manual bracket where you select winners through all 6 rounds:
- Full 64-team tournament bracket across 4 regions (East, West, South, Midwest)
- Pick winners for every matchup from the Round of 64 through the Championship
- Picks are auto-saved and persisted across page refreshes
- Live ESPN data feeds bracket seedings and scores in real time
AI Bracket Generation — Agentforce generates a complete bracket via streaming:
- 8 sequential prompts sent to the Agentforce agent (one per region for Round of 64, then Round of 32, Sweet 16, Elite 8, Final Four + Championship)
- Picks stream back in real time via Server-Sent Events (SSE)
- The AI's full reasoning is rendered as markdown in a side panel
- If the agent misses any matchups, a targeted retry regenerates only the missing picks
Key capabilities:
- Live ESPN Integration: Tournament data, seedings, and live scores pulled directly from the ESPN API with in-process TTL caching
- Streaming AI Predictions: Agentforce responses stream token-by-token via SSE — picks are extracted on-the-fly using regex pattern matching
- Adaptive AI: After real tournament rounds complete, the AI reviews the results and adjusts its remaining predictions accordingly via
POST /api/v1/af/bracket/adapt - Head-to-Head Scoring: Compare your bracket against the AI's bracket scored against actual ESPN results using exponential round points (1 → 2 → 4 → 8 → 16 → 32, max 192 points)
- Persistent State: User picks and AI reasoning survive page navigation and refreshes via localStorage — no backend persistence required
- Heroku AppLink Auth: Salesforce OAuth tokens are retrieved securely via the Heroku AppLink SDK — no credentials stored in environment variables
AI Bracket Generation Flow:
- Session Init: The frontend generates a UUID session ID and calls
POST /api/v1/af/sessions/:sessionIdto open an Agentforce session - Auth via AppLink: The backend uses the
@heroku/applinkSDK to retrieve a Salesforce OAuth access token and instance URL at request time - Round Prompt: The frontend calls
POST /api/v1/af/bracket/roundwith the current round and region context - Agent Call: The backend constructs a prompt with the full matchup list and forwards it to the Agentforce Sessions API (
/einstein/ai-agent/v1/sessions/{id}/messages/stream) - SSE Streaming: Agentforce streams the response chunk-by-chunk; the backend proxies this stream directly to the frontend via SSE
- Pick Extraction: The frontend's
useSSEhook accumulates text chunks and extracts picks in real time using the patternPICK: [matchupId] -> [winnerId] - Retry Logic: After each round, any matchups the agent missed are collected and a targeted
POST /api/v1/af/bracket/retryre-prompts the agent for only those picks - Repeat: Steps 3–7 repeat for all 8 prompts until the full bracket is complete
- Session Cleanup: On page unmount,
DELETE /api/v1/af/delete-sessioncloses the Agentforce session
Manual Bracket + Live Scoring Flow:
- Bracket Load: The frontend fetches the full bracket structure from
GET /api/v1/agentforce/results/bracket, which queries the ESPN Scoreboard API and maps games to the internal tournament model - ESPN Cache: The ESPN service caches bracket data for 5 minutes and live scores for 30 seconds to avoid hammering the API on low-RAM Heroku dynos
- User Picks: As users select winners, picks are saved locally to localStorage and persist across page refreshes
- Live Polling: The
useLivePollinghook pollsGET /api/v1/agentforce/results/liveevery N seconds to refresh scores for in-progress games - Adaptive AI: On the Live page, the AI can review completed round results and call
POST /api/v1/af/bracket/adaptwith actual outcomes to adjust remaining picks - Scoring: The Compare page scores both brackets locally against real ESPN results — no additional server call needed
All Agentforce-facing routes authenticate via Heroku AppLink. The backend SDK (@heroku/applink) retrieves a short-lived Salesforce OAuth access token at request time using the APP_LINK_CONNECTION_NAME attachment. No token is stored in environment variables.
Inbound requests from Salesforce (tool invocations on agentforceTools routes) are validated using HMAC signature verification against the API_SECRET shared secret. Invalid signatures return 401 Unauthorized.
| Middleware | Applies To | Purpose |
|---|---|---|
cors |
All routes | Restricts browser requests to the origin set in CLIENT_ORIGIN |
express.json |
All routes | Parses JSON request bodies |
validateSignature |
agentforceApiRoutes + agentforceTools routes |
HMAC-validates inbound requests from the client and Salesforce agent tool calls |
herokuServiceMesh |
Selected routes | Heroku service mesh integration |
errorHandler |
All routes (last) | Centralized JSON error responses |
POST /api/v1/af/sessions/:sessionId
- Auth required: Yes (AppLink)
- Description: Opens a new Agentforce agent session with the provided external session key
- URL params:
sessionId— UUID identifying the client session - Response:
200 { sessionId, messages } - Error responses:
500if AppLink auth or Agentforce API fails
DELETE /api/v1/af/delete-session
- Auth required: Yes (AppLink)
- Description: Closes an active Agentforce session
- Request body:
{ sessionId: string } - Response:
200 { message } - Error responses:
500if session deletion fails
POST /api/v1/af/bracket/round
- Auth required: Yes (HMAC signature)
- Description: Generates AI bracket picks for a specific round by prompting the Agentforce agent; streams picks back via SSE
- Request body:
{ sessionId: string, sequenceId: number, roundIndex: number, priorPicks: Record<string, string> } - Response:
text/event-stream - Error responses:
400if required fields are missing,500on agent error
POST /api/v1/af/bracket/retry
- Auth required: Yes (HMAC signature)
- Description: Retries AI pick generation for specific matchups the agent previously missed
- Request body:
{ sessionId: string, sequenceId: number, missingMatchupIds: string[], priorPicks: Record<string, string> } - Response:
text/event-stream - Error responses:
400,500
POST /api/v1/af/bracket/adapt
- Auth required: Yes (HMAC signature)
- Description: Prompts the Agentforce agent to review actual completed-round results and adapt its remaining bracket predictions; the full prompt is built server-side to prevent prompt injection
- Request body:
{ sessionId: string, sequenceId: number, round: string, completedMatchups: Matchup[], aiBracket: Bracket | null } - Response:
text/event-stream - Error responses:
400,500
GET /api/v1/agentforce/results/teams
- Auth required: No
- Description: Returns all 64 tournament teams with seedings and regions
- Response:
200 { teams: Team[] }
GET /api/v1/agentforce/results/bracket
- Auth required: No
- Description: Returns the full bracket structure from ESPN (cached 5 min) or falls back to static 2025 data
- Response:
200 { bracket: Bracket, isLive: boolean }
GET /api/v1/agentforce/results/live
- Auth required: No
- Description: Returns live game scores and statuses from ESPN (cached 30 sec)
- Response:
200 { games: LiveGame[] }
| Layer | Technology | Description |
|---|---|---|
| Frontend | React 19, Vite, Tailwind CSS 4, React Router 7 | UI library, build tool, utility-first CSS, client-side routing |
| Frontend | React Markdown, Lucide React, UUID | Markdown rendering for AI reasoning, icons, session ID generation |
| Backend | Node.js, Express 5, TypeScript 5 | JavaScript runtime, web framework, static typing |
| AI / LLM | Salesforce Agentforce, Salesforce Data360 | Autonomous AI agent platform, data cloud backend for agent knowledge |
| Auth | Heroku AppLink | Secure Salesforce OAuth token retrieval without storing credentials |
| Data | ESPN API | Real-time tournament bracket, scores, and game statuses |
| Hosting | Heroku | Cloud platform for both frontend static assets and backend API |
To run this application locally, you will need the following:
- Node.js v22 or later installed (run
node -vto check). Follow the Node.js install instructions if needed - npm v10 or later installed (run
npm -vto check). Node.js includesnpm - git installed. Follow the instructions to install git
- A Salesforce org with Agentforce enabled and an agent deployed
- A Heroku account with an app that has the Heroku AppLink add-on attached and connected to your Salesforce org
- (Optional) A Salesforce Data360 account if your agent uses Data Cloud as a knowledge source
-
Clone the repository
git clone https://github.com/your-org/salesforce-agentforce-march-madness.git cd salesforce-agentforce-march-madness -
Configure Salesforce Agentforce
Important: You must have a Salesforce org with Agentforce enabled before proceeding.
Follow the Agentforce documentation to:
- Create and deploy an Agentforce agent in your Salesforce org
- Note the Agent ID from the agent's detail page — you will need it for
AGENTFORCE_AGENT_ID
-
Configure Heroku AppLink
Important: Heroku AppLink is required for the backend to authenticate with Salesforce. It does not work with a plain
.envfile in production.Follow the Heroku AppLink setup guide to:
- Attach the AppLink add-on to your Heroku app
- Connect the add-on to your Salesforce org
- Note the connection name — this is your
APP_LINK_CONNECTION_NAMEvalue
-
Configure environment variables
Server:
cp server/src/.env.example server/src/.env
Open
server/src/.envand fill in all required values (see Environment Variables).Client:
cp client/.env.example client/.env
Open
client/.envand fill in all required values.Generate a shared secret for signature validation if needed:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"⚠️ Note:API_SECRETinserver/src/.envandVITE_API_SECRETinclient/.envmust match exactly. -
Install dependencies
Install all workspace dependencies from the root:
# Server cd server && npm install && cd .. # Client cd client && npm install && cd ..
-
Start the application
Open two terminals:
# Terminal 1 — backend cd server npm run dev
# Terminal 2 — frontend cd client npm run dev
-
Access the application
Once both servers are running, open your browser and navigate to
http://localhost:5173.
| Variable | Required | Description |
|---|---|---|
APP_PORT |
No | Port the Express server listens on locally. Defaults to 3000 |
CLIENT_ORIGIN |
No | CORS-allowed origin for the frontend. Defaults to http://localhost:5173 |
APP_LINK_CONNECTION_NAME |
Yes | Heroku AppLink connection name used to retrieve Salesforce OAuth tokens |
AGENTFORCE_AGENT_ID |
Yes | Salesforce Agentforce Agent ID from your org's agent configuration page |
API_SECRET |
Yes | Shared HMAC secret used to validate inbound Salesforce agent tool requests |
| Variable | Required | Description |
|---|---|---|
VITE_API_BASE |
No | Base URL for the backend API. Defaults to http://localhost:3000 |
VITE_API_SECRET |
Yes | Must match API_SECRET on the server for signature validation |
Your Agentforce agent must be:
- Published and active in your Salesforce org
- Accessible by the AppLink-connected user profile
- Configured with appropriate knowledge sources (Data360 recommended for team statistics and historical NCAA data)
- Ensure your Agentforce Actions are using the Heroku AppLink as described here.
The server exposes tool endpoints under agentforceTools routes that Salesforce agents can call back into the application. These endpoints are protected by HMAC signature validation using the shared API_SECRET.
| Field | Value |
|---|---|
| Name | March Madness Bracket Analyst |
| Description | This is the March Madness Bracket Analyst agent that analyzes the scores and provides suggestions accordingly. |
| Role | You are a March Madness Bracket Analyst powered by Salesforce Agentforce. Your job is to analyze the NCAA Men's Basketball Tournament field, generate data-driven bracket predictions, and adapt your picks in real time as the tournament unfolds. |
| Company | This is just a demo Agent that analyzes the scores of march madness and provides suggestions accordingly. |
| Field | Value |
|---|---|
| Name | Bracket Generation |
| Classification Description | Analyzing the full 64-team field and predicting winners through all 6 rounds of the tournament. |
| Scope | My job is to analyze the 64-team field, evaluate matchups, and predict winners for each of the 6 rounds in the tournament. I will not handle tasks outside of bracket analysis or predictions. |
-
Always explain your reasoning for each pick citing seed, record, region strength, and historical tournament performance.
-
When making bracket picks, output each pick on its own line in this exact format:
PICK: MATCHUP_ID -> TEAM_ABBREVIATIONfollowed byREASON: One sentence. Use the exactMATCHUP_IDandTEAM_ABBREVIATIONvalues from the bracket data. Do not use markdown bold, asterisks, backticks, or brackets around pick values. -
Work through regions in order: East → West → South → Midwest → Final Four → Championship.
-
Flag upset picks explicitly with
UPSET ALERT:when picking a seed 5 or lower over a seed 4 or higher. -
When providing bracket picks, you MUST follow this exact format for every single pick — no exceptions:
PICK: [MATCHUP_ID] -> [TEAM_ABBREVIATION] REASON: [One sentence explanation]Rules:
- Use ONLY the keyword
PICK:— neverUPSET ALERT:,PREDICTION:, or any other prefix MATCHUP_IDmust be exactly as returned by the Get Bracket Structure action (e.g.East-R64-1v16,Midwest-Roundof32-1,FF-1,CHAMP-1)TEAM_ABBREVIATIONmust be exactly as returned by the Get Bracket Structure action (e.g.DUKE,HOU,GONZ)- One
PICKline per matchup, on its own line - Do not use markdown bold (
**), bullet points, or any other formatting on thePICKlines themselves - You may use headers and prose between PICK blocks, but each PICK line must be clean and unformatted
Example:
PICK: East-R64-1v16 -> DUKE REASON: Duke dominates as a #1 seed against Mount St. Mary's. PICK: Midwest-Roundof32-1 -> HOU REASON: Houston advances on balanced scoring and elite defense. - Use ONLY the keyword
-
You are making PREDICTIONS for an entire bracket before any games are played. You MUST pick every single matchup in every round — Round of 64 through Championship — regardless of whether results exist. For rounds beyond Round of 64, determine the expected participants by picking the winner of each prior matchup based on seeding, historical performance, and your own analysis. Then make a pick for that matchup. Never refuse to pick a matchup because results are unknown. That is the entire point — you are predicting, not reporting. If no winner has been recorded, project one yourself and continue.
This project is deployed on Heroku. The root package.json start script handles the full build and launch sequence — Heroku runs it automatically on each deploy.
Set each server environment variable as a Heroku config var:
heroku config:set APP_LINK_CONNECTION_NAME=your_connection_name
heroku config:set AGENTFORCE_AGENT_ID=your_agent_id
heroku config:set API_SECRET=your_api_secret
heroku config:set CLIENT_ORIGIN=https://your-app-name.herokuapp.comPORT is set automatically by Heroku — do not override it.
git push heroku mainHeroku runs npm install and then npm start from the root automatically. The start script builds the React client, moves the compiled assets into server/public, and starts the Express server. The Express server then serves both the API and the React SPA.
This software is to be considered "sample code", a Type B Deliverable, and is delivered "as-is" to the user. Salesforce bears no responsibility to support the use or implementation of this software.








