Status: Accepted Date: 2026-08-22 Decider: Valentyn Yakovliev Supersedes: ROADMAP_V1 P10 source-attribution sketch (source-url.util.ts revived as fallback core) Related: ROADMAP_V2.md §Z4, §M0, §M2-M3
ROADMAP_V2 makes lead generation the primary product goal. Posts must carry measurable CTA links, but the social-poster-agent has no link infrastructure:
- Generation prompts forbid URLs outright (fallback-prompts.ts, generation.service.ts)
source-url.util.ts(P10) is dead code;CanonicalUrlServicecovers POSSE SEO only- No click tracking, no conversion attribution — leads are unmeasurable today
Building our own redirector/shortener/click-store was scoped out (ROADMAP_V2 §7
Non-goals): the sibling project my_zodiac_ai/back already ships a production
attribution stack (modules/business/attribution-links, Wave #213):
AttributionLink: slug →{platform(=utm_source), medium, campaign, content, refTag, customFields},destinationUrlallowlisted to*.my-zodiac-ai.comAttributionClick: per-click record with geo/device/ipHash and a stitch key (quizSessionId) that carries attribution through click → quiz session → snapshot → payment (conversion writeback incl. revenue)- Admin API
/api/v2/admin/attribution-links+ per-link funnel reports
The quiz funnel (quiz.my-zodiac-ai.com/r/<slug>) is the primary lead surface —
exactly where post CTAs should land.
Service-to-service access options were JWT service account vs direct MongoDB vs
internal token. Chosen: internal-token endpoint in zodiac-back, reusing its
existing MAIN_BACKEND_INTERNAL_TOKEN pattern — stable credentials, no password
rotation, no schema coupling.
-
No link infrastructure of our own. Social-poster-agent becomes a client of zodiac-back attribution links. Redirects, click records and conversions live entirely in zodiac-back.
-
Domain port first. Consumers depend on
ILinkPort(src/domain/ports/link.port.ts, Symbol DI token), never on an HTTP client:createTrackableLink({network, campaign, postId?, accountHandle?, destinationUrl?})getFunnelReport(linkId, {from?, to?})
Adapter:
ZodiacLinkClient(infrastructure/link/, M2.1) overPOST/GET {ZODIAC_API_URL}/internal/attribution-linkswithX-Internal-Token: ZODIAC_INTERNAL_TOKEN. The internal controller in zodiac-back is a thin pass-through to the existingAttributionLinksService. -
Posting is never blocked by the link service. Graceful degradation ladder:
- zodiac-back reachable → short link stored on
Post.ctaUrl,Post.attributionLinkId/attributionSlugset - unreachable / timeout / non-2xx →
LinkServiceUnavailableError→ caller falls back tobuildDirectUtmUrl()(modules/content-enhancements/source-url.util.ts, revived P10 core) — plain UTM-tagged destination URL;attributionLinkId = null - every degradation is logged as a structured event for the reliability dashboard
- zodiac-back reachable → short link stored on
-
Fallback UTM builder contract. Deterministic param order (utm_source → medium → campaign → content), preserves existing query params, rejects non-http(s) destinations at build time. Same inputs ⇒ byte-identical URLs (SimHash/dedup stability).
-
Schema.
Postgains three nullable columns (migration20260822000000_add_post_link_attribution):ctaUrl,attributionLinkId,attributionSlug(+ index). NoLeadEventmodel — clicks/conversions are read back from zodiac-back funnel reports (M2.4 dashboard). -
Env surface (validated in
env.validation.ts):ZODIAC_API_URL(empty = port disabled, always fallback),ZODIAC_INTERNAL_TOKEN,ZODIAC_DEFAULT_DESTINATION_URL(defaulthttps://quiz.my-zodiac-ai.com),ZODIAC_TIMEOUT_MS(default 5000).
Positive
- Leads measurable end-to-end down to paid conversions, not just clicks
- Zero new redirect/click infrastructure to operate or secure
- Posting pipeline decoupled from link-service availability
- Port-based design keeps M2.1 adapter swappable and trivially mockable
Negative / risks
- Cross-project deployment dependency (zodiac-back must expose the internal endpoint before M2.1 goes live) — mitigated by the fallback path
- Click records TTL is 30 days in zodiac-back; long-horizon analytics require periodic snapshotting into the conversion dashboard (noted in M2.4)
destinationUrlallowlist means posts can only drive traffic to*.my-zodiac-ai.comproperties — intentional: that is the lead goal
Neutral
- X policy stays "link in first reply" (M2.3
postWithReplyLink()); Threads/FB may inline the CTA — prompt-level concern, not transport