---
title: "Opportunities"
description: "Dual-nature offer/request marketplace with LLM Matcher scoring, auto-fill, role-gated types, and verified DatabaseService APIs"
locale: "en"
---
# Opportunities

Ring Opportunities is the **supply ↔ demand loop** for each clone: members post **offers** and **requests** (plus specialized types), Matcher scores interested users, and notifications invite them to connect — without waiting for someone to search the feed.

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. `audience` frontmatter controls in-page blocks; sidebar visibility is controlled by `lib/docs/audience-curated-docs.ts`.

  Older copy claimed fixed accuracy %, hire-cost savings, and “15k+ users.” Those figures are **deprecated**. Live numbers come from **Admin → Matcher** analytics and clone traffic. Matcher thresholds are **config** (`ring-config.json` → `matcher`, overridable in platform settings) — not kingdom-wide SLAs.

## Traditional boards vs Ring

| Previous | Ring equivalent |
|----------|-----------------|
| Post job → hope someone searches | Post opportunity → Matcher scores candidates → notify with short explanations |
| Offers only (employer → talent) | **Dual-nature**: `offer` and `request` plus specialized types |
| One listing taxonomy | White-label `opportunities.enabledTypes` gate (16 defaults in Layer1 / org config) |
| Manual enrichment | Optional LLM **auto-fill** on create (`OpportunityAutoFillService`) |
| Opaque ranking | Eight scored factors (`MatchFactors` in `lib/ai/types.ts`) + configurable `scoreThreshold` |
| Separate funding jars | Collective / builder jars via [Public Pools](/docs/features/public-pools.md) (donation path live; on-chain escrow gated) |

## Matching loop (shared)

```mermaid
flowchart TD
  A["Human creates opportunity\\nUI Server Action or POST /api/opportunities"] --> B["createOpportunity\\nDatabaseService → SerializedOpportunity"]
  B --> C["syncOpportunityDiscovery\\ncache + revalidatePath + Tunnel"]
  C --> D["OpportunityAutoFillService.enrichOpportunity"]
  D --> E["OpportunityMatchingService.findMatches\\nMatcher + optional LLM explanations"]
  E --> F{"matcher.autoApprove\\nand score/confidence gates?"}
  F -->|yes| G["status → active"]
  F -->|no| H["remain pending / admin review"]
  E --> I["notifyMatchedUsers\\nnotification-service + entity block filter"]
  I --> J["Matched users open opportunity"]
```

Default Matcher install values (`ring-config.json` → `matcher`): `scoreThreshold` **0.7**, `maxMatches` **10**, `autoApprove` **false**, `autoApproveMinScore` **0.7**, `llmConfidenceGate` **0.8**. Env can override auto-approve via `MATCHER_AUTO_APPROVE` / `MATCHER_AUTO_APPROVE_MIN_SCORE` (see `features/admin/platform-settings/matcher-config.ts`). Cap matches with `MAX_MATCHES_PER_OPPORTUNITY`.

### Eight scoring factors

`MatchFactors` in `lib/ai/types.ts` (0–100 each): `skillMatch`, `experienceMatch`, `industryMatch`, `locationMatch`, `budgetMatch`, `availabilityMatch`, `careerMatch`, `cultureMatch`. LLM match runs populate this bag; the non-LLM heuristic path in `lib/ai/matcher.ts` blends skill / tag / location / experience with its own internal weights — **do not treat AI-CONTEXT percentage tables as runtime SSOT**.

Explanations aim for **≤160 characters** (`MatchingService` truncates longer LLM text). Quality bands in matching results: high ≥80, medium 60–79, low &lt;60 — for analytics, not a published accuracy KPI.

### For founders

## Why this matters for your clone

Opportunities turn your Ring into a **local marketplace of intent**: people publish what they can provide and what they need. Matcher pushes relevant invitations instead of relying on feed scroll. Specialized types (jobs, collective orders, scheduled services, program/investment → CRM, Ring customization quests) are **gated per clone** so you ship only the loops your vertical needs.

  
- **[Entities](/docs/features/entities.md)** — Link opportunities to verified organizations via `contactInfo.linkedEntity` / `organizationId`.

  
- **[Notifications](/docs/features/notifications.md)** — Match invites are real notifications — not marketing email blasts.

  
- **[Public Pools](/docs/features/public-pools.md)** — When a related jar fills, builders receive **net** native token (role fee from `publicPools.platformFeePercentByRole`).

  
- **[Email AI CRM](/docs/features/email-ai-crm.md)** — `program` opportunities can ingest a lead + review task for ops follow-up.

### Typical clone scenarios

1. **Talent / partnership ring** — enable `job`, `offer`, `request`, `partnership`; keep Matcher auto-approve **off** until you trust scoring.
2. **Commerce vertical** — enable `collective_order`, `scheduled_services`, `asset_rental`, `bounty`, `tender` with Wallet / PaymentConductor rails.
3. **Ringdom settler / builder ring** — enable `ring_customization` and `program` so customization quests and institution programs land in CRM.

### Operator checklist

1. Confirm feature flag / whitelabel includes `opportunities`.
2. Set `opportunities.enabledTypes` in `ring-config.json` (do not assume every `OpportunityType` union member is UI-visible).
3. Configure LLM (`LLM_PROVIDER`, `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, `LLM_MODEL`) before expecting auto-fill / LLM explanations.
4. Tune Matcher in **Admin → Matcher / Platform Settings → AI → Matcher** (threshold, max matches, auto-approve).
5. Teach members **My Opportunities** at `/opportunities/my` (`ROUTES.MY_OPPORTUNITIES`) — legacy `/opportunities/my-opportunities` is removed.

### For developers

## Implementation map

| Concern | Verified path |
|---------|----------------|
| Types / `SerializedOpportunity` | `features/opportunities/types/index.ts` |
| Enabled-type gate | `features/opportunities/lib/opportunity-enabled-types.ts` ← `ring-config.json` `opportunities.enabledTypes` |
| Role create matrix | `features/opportunities/lib/opportunity-permissions.ts` (`MEMBER_OFFER_TYPES`, `REQUEST_TYPES`, `SUBSCRIBER_TYPES`, confidential) |
| Create + AI pipeline | `features/opportunities/services/create-opportunity.ts` |
| List / search | `get-opportunities.ts`, `search-opportunities.ts` |
| Matcher core | `lib/ai/matcher.ts` + `OpportunityMatchingService` |
| Auto-fill | `features/opportunities/services/auto-fill-service.ts` |
| Auto-approve | `features/opportunities/services/auto-approval-service.ts` |
| PG JSONB ↔ client | `features/opportunities/lib/opportunity-db-mapper.ts` |
| Post-mutation sync | `features/opportunities/lib/opportunity-mutation-sync.ts` → `syncOpportunityDiscovery` |
| Program → CRM | `features/opportunities/lib/program-crm-ingest.ts` |
| UI Server Actions | `app/_actions/opportunities.ts` |
| Firestore legacy | `lib/converters/opportunity-converter.ts` when `DB_BACKEND_MODE=firebase-full` only |

**Database:** default production clones use `DB_BACKEND_MODE=k8s-postgres-fcm` (also `supabase-fcm`). Services return **`SerializedOpportunity`** (ISO date strings). Do not invent `DATABASE_MODE=firebase_only`.

### Visibility & status

- **Visibility:** `public` \| `subscriber` \| `member` \| `confidential`
- **Status:** `draft` \| `pending` \| `active` \| `closed` \| `expired` \| `archived`
- Confidential create/edit requires confidential-capable roles (`canCreateOpportunityConfidential` / `canEditOpportunity`)

### Role gates (create)

| Bucket | Types (examples) | Minimum role |
|--------|------------------|--------------|
| Member offers | `offer`, `job`, `partnership`, `volunteer`, `mentorship`, `resource`, `event`, `ring_customization`, `program`, `collective_order`, `tender`, `asset_rental` | member privileges |
| Requests | `request` | subscriber+ |
| Subscriber specials | `cv`, `scheduled_services`, `bounty` | subscriber+ |
| Other union members | e.g. future-facing types not in enabled/permission sets | admin+ (and must be enabled) |

### REST surfaces (alpha)

| Method | Route | Service |
|--------|-------|---------|
| `GET` / `POST` | `/api/opportunities` | `getOpportunitiesForRole` / `createOpportunity` |
| `GET` / `PUT` / `PATCH` / `DELETE` | `/api/opportunities/[id]` | get / update / delete |
| `GET` / `POST` | `/api/opportunities/search` | `searchOpportunities` |
| `GET` | `/api/opportunities/my` | user-owned list |
| `POST` | `/api/opportunities/upload` | attachments |

UI forms may still prefer Server Actions in `app/_actions/opportunities.ts`.

### Create (server service)

Authenticate and call `createOpportunity` with a permitted type. Default type when omitted is `offer`.

{`import { createOpportunity } from '@/features/opportunities/services/create-opportunity'

const opportunity = await createOpportunity({
  type: 'offer',
  title: 'Senior Full-Stack Developer',
  briefDescription: 'Build Ring-powered products',
  fullDescription: 'React 19, Next.js 16, PostgreSQL, Auth.js v5',
  createdBy: userId,
  organizationId: entityId,
  status: 'pending',
  category: 'engineering',
  tags: ['react', 'typescript', 'nextjs'],
  location: 'Remote',
  requiredSkills: ['React', 'TypeScript', 'PostgreSQL'],
  requiredDocuments: [],
  attachments: [],
  visibility: 'public',
  contactInfo: { linkedEntity: entityId, contactAccount: userId },
  applicantCount: 0,
  isConfidential: false,
  expirationDate: new Date(Date.now() + 90 * 864e5),
})
// Side effects: syncOpportunityDiscovery → auto-fill → findMatches →
// maybeAutoApproveOpportunity → notifyMatchedUsers (async)`}

Search with role-aware filters (`query`, `types`, budget band, `sortBy`).

{`import { getOpportunitiesForRole } from '@/features/opportunities/services/get-opportunities'
import { searchOpportunities } from '@/features/opportunities/services/search-opportunities'

const { opportunities } = await getOpportunitiesForRole({
  userRole: UserRole.member,
  limit: 20,
})

const searchResults = await searchOpportunities({
  query: 'React TypeScript',
  types: ['offer'],
  location: 'Remote',
  budgetMin: 100000,
  sortBy: 'relevance',
  limit: 20,
})`}

After mutations, rely on `syncOpportunityDiscovery` (already called from create/update/delete paths): invalidates opportunity cache tags, `revalidatePath` for `/[locale]/opportunities`, detail, and `/[locale]/opportunities/my`, and publishes Tunnel discovery for channel `opportunities`. This is **not** a separate full-text index reindex CLI.

### LLM / Matcher env (template)

Keys documented in `env.local.template`: `LLM_PROVIDER`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `LLM_MODEL`, `MAX_MATCHES_PER_OPPORTUNITY`, `MATCHING_MAX_TOKENS`. Without a working provider, Matcher falls back to non-LLM / tag-style explanations with lower confidence.

## Recommended path

Use the **ring-ai-matcher-specialist** (or Cursor skill of the same family) with a pause-for-approval prompt:

> “Audit this clone’s Opportunities loop: list `opportunities.enabledTypes` and `matcher` from `ring-config.json`, confirm LLM env is set, dry-run create of one `offer` and one `request`, report MatchFactors and whether `autoApprove` would promote pending→active. Do not change platform_settings until I approve.”

Inputs checklist: clone key, admin access, LLM credentials present, sample entity id for `organizationId`.

## Manual path

Set `opportunities.enabledTypes` and `matcher.*` in `ring-config.json`; redeploy or reload config snapshot as your clone does for ring-config.

In Admin → Matcher / AI settings, leave **auto-approve** off; set `scoreThreshold` / `maxMatches` to match your vertical tolerance.

Create a test opportunity from the type selector UI (or `POST /api/opportunities`); open **My Opportunities** at `/opportunities/my`.

Confirm a matched user received a notification and that discovery lists refresh (Tunnel / soft refresh) without inventing a search-index reindex step.

## Frequently asked questions

### Impact

#### Will Matcher auto-publish every new listing?
No. Default `matcher.autoApprove` is **false**. When enabled, `maybeAutoApproveOpportunity` still requires score and LLM confidence gates (`autoApproveMinScore`, `llmConfidenceGate`).

#### Do I get a kingdom-wide “95% match accuracy”?
No. Treat historical marketing percentages as deprecated. Use Admin Matcher analytics for your tenant.

### Migration

#### We still have Firestore converters — are they required?
Only when `DB_BACKEND_MODE=firebase-full`. PostgreSQL clones use `opportunity-db-mapper.ts` and `SerializedOpportunity`.

#### Where did `/opportunities/my-opportunities` go?
Removed. Canonical dashboard is `/opportunities/my` via `ROUTES.MY_OPPORTUNITIES`.

### Ops

#### Why did nobody get notified?
Check LLM availability, Matcher score threshold, `maxMatches`, and entity block filters in `matcher-notification-filter`. Notifications go through `features/notifications/services/notification-service`.

#### How do collective-order money and jars relate?
Opportunity specializations can carry JSONB `metadata` (e.g. collective order slots). Community funding jars and builder payout SSOT are documented under [Public Pools](/docs/features/public-pools.md) — do not invent PSP “donation product” APIs as the jar path.

## Related documentation

  
- [api/opportunities](/docs/api/opportunities.md) — Next-step: REST route contract and payloads after you understand the create → match pipeline.

  
- [architecture/discovery-mutation-sync](/docs/architecture/discovery-mutation-sync.md) — Depends-on: how syncOpportunityDiscovery invalidates cache and publishes Tunnel events.

  
- [features/entities](/docs/features/entities.md) — Same-workflow: organization linkage and verified posters for opportunities.

  
- [features/notifications](/docs/features/notifications.md) — Same-workflow: match invitations are delivered as platform notifications.

  
- [features/public-pools](/docs/features/public-pools.md) — See-also: collective / builder jar payouts tied to opportunity owners.

  
- [features/email-ai-crm](/docs/features/email-ai-crm.md) — See-also: program opportunity ingest into CRM contacts and tasks.

  
- [features/payments](/docs/features/payments.md) — See-also: PaymentConductor rails when opportunity types collect fiat.

  
- [features/admin](/docs/features/admin.md) — Deep-dive: Admin Matcher tab and platform AI settings for thresholds.
