Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 

Repository files navigation

Affinity EV Updater (Vercel webhook)

This project listens for Affinity webhook events and updates an "Expected Value (USD)" field using the Affinity v2 API.

  • Incoming events: Affinity Webhooks (API v1)
  • Compute: EV = midpoint(min, max) × likelihood%
  • Write: Affinity v2 per-entry field update
  • Hosting: Vercel Serverless Function

Why this design

  • V1 for webhooks, V2 for data: Affinity v2 doesn’t expose webhook management. Affinity v1 does. We subscribe with v1, then read/write data with v2.
  • Per-entry fetch avoids nulls: The bulk list-entries endpoint may return null for list-level fields that are actually attached to entities (e.g., organization). Fetching per-entry /v2/lists/{id}/list-entries/{entryId}/fields reliably returns values that match the UI.
  • Serverless on Vercel: Gives us an always-on HTTPS URL with minimal ops, easy env vars, and logs.

Repository layout

  • api/affinity-ev.js: Serverless handler (entry point)
  • package.json: ESM + axios dependency
  • .gitignore: Ignores .env and other local artifacts

Deploy is via GitHub (evandk/affinity-ev-updater). Vercel deploys on push.


Configuration

Environment variables (Vercel)

  • AFFINITY_V2_TOKEN: Affinity v2 Bearer token. Required.
    • Set in Vercel project → Settings → Environment Variables.
    • Make sure it’s set for the environment you’re hitting (Production/Preview).

Notes:

  • Do not store any secrets in repo. .env is ignored by .gitignore.
  • If a token was ever exposed, rotate it.

List and Field IDs

Set inside api/affinity-ev.js:

  • LIST_ID = 300305
  • FID_MIN = 'field-5140816' (Min Commitment USD)
  • FID_MAX = 'field-5140817' (Max Commitment USD)
  • FID_P = 'field-5150465' (Likelihood %)
  • FID_EV = 'field-5305096' (Expected Value USD)

Update these if you move to a different list or fields.


Webhook subscription (Affinity API v1)

Use an Affinity v1 API key (Basic Auth), not your v2 token.

  1. Verify your v1 key
curl "https://api.affinity.co/auth/whoami" -u :$AFFINITY_V1_API_KEY
  1. Create the subscription (quote subscriptions[] to avoid zsh globbing)
curl -X POST 'https://api.affinity.co/webhook/subscribe' \
  -u :$AFFINITY_V1_API_KEY \
  -d 'webhook_url=https://affinity-ev-updater.vercel.app/api/affinity-ev' \
  -d 'subscriptions[]=field_value.created' \
  -d 'subscriptions[]=field_value.updated'
  1. List / Delete
# List
curl 'https://api.affinity.co/webhook' -u :$AFFINITY_V1_API_KEY

# Delete one
curl -X DELETE 'https://api.affinity.co/webhook/<webhook_id>' -u :$AFFINITY_V1_API_KEY

Important:

  • v1 subscriptions are instance-wide. We filter inside the function to only act on list_id = 300305 and the relevant fields.

EV calculation

  • Formula: EV = midpoint(min, max) × (likelihood / 100)
    • midpoint(min, max) = (min + max) / 2; if only one bound is present, use that.
  • Units: Likelihood is a percentage (e.g., 60 = 60%).
  • Rounding: We round to nearest integer dollar.
  • Zero-as-missing rule: If exactly one bound is zero and the other is > 0, we treat the zero as missing and use the non-zero bound. This avoids averaging a real bound with zero when zero actually means “unset”. See ZERO_AS_MISSING in code.

How the function works

Path: api/affinity-ev.js

  1. Parse the webhook body robustly:
    • Supports JSON, Buffer body, and application/x-www-form-urlencoded.
    • Handles Affinity v1 envelope shape { type, body: {...} } and direct JSON.
  2. Filter to our list: list_id == 300305.
  3. Per-entry GET (v2): /v2/lists/{LIST_ID}/list-entries/{listEntryId}/fields
  4. Compute EV from Min/Max/Likelihood.
  5. POST EV (v2): /v2/lists/{LIST_ID}/list-entries/{listEntryId}/fields/{FID_EV} with { value: { type: 'number', data: ev } }.
  6. Verify by GET with a short retry (eventual consistency).

Why per-entry: bulk pages can return null for fields not set in the current list context, even if visible in the UI. Per-entry returns what the UI shows.


Testing

Quick ping (should skip):

curl -s -X POST https://affinity-ev-updater.vercel.app/api/affinity-ev \
  -H "Content-Type: application/json" \
  -d '{"ping":true}'

Real test for a known entry:

curl -s -X POST https://affinity-ev-updater.vercel.app/api/affinity-ev \
  -H "Content-Type: application/json" \
  -d '{"list_entry_id":215745884,"field":{"list_id":300305}}'

Read-back verification (v2):

curl -s -H "Authorization: Bearer $AFFINITY_V2_TOKEN" \
  "https://api.affinity.co/v2/lists/300305/list-entries/215745884/fields" \
| jq '.data[] | select(.id=="field-5305096")'

Vercel logs: Vercel → Project → Functions → Logs.


Common pitfalls and fixes

  • Using v2 token for v1 API: v1 webhooks use Basic Auth with a v1 API key. v2 uses Bearer tokens.
  • Zsh eats subscriptions[]: Quote or escape subscriptions[] in curl.
  • Event body shape: Affinity v1 sends { type, body: {...} }. We parse both that and direct JSON.
  • Nulls from bulk endpoints: Use per-entry field GET to match UI values.
  • Likelihood 0%: EV will be 0; that’s expected even on success.
  • Eventual consistency: The verify step retries briefly; UI may lag a few seconds.
  • Env var name is case-sensitive: AFFINITY_V2_TOKEN must match exactly in Vercel.
  • Different list: We ignore events where list_id !== 300305.
  • Permissions/403/401: Ensure the v2 token has rights to that list and fields; confirm tenant with auth/whoami.

Extending to other automations

  • Copy the pattern: per-entry fetch → compute → write.
  • Add new fields/logic inside api/affinity-ev.js or a new function file (e.g., api/another-automation.js).
  • For batch jobs (backfills), prefer a separate Node script that pages entries, per-entry fetches, computes, and writes with throttling.

Security

  • Do not commit secrets. Keep tokens in Vercel env vars. .env is ignored.
  • Rotate tokens if they were ever displayed publicly.

Support checklist

  • Vercel env AFFINITY_V2_TOKEN set (Production)
  • Webhook (v1) subscribed to field_value.created and field_value.updated
  • List and Field IDs updated if needed
  • Test curl returns { ok: true, ... }
  • Logs show no 4xx/5xx from Affinity v2

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages