Cursor Skill

integrate-unprice-sdk

Integrate and troubleshoot the @unprice/api TypeScript SDK in server-side Node.js, Next.js, Hono, or edge applications. Use when onboarding Unprice customers, checking access, enforcing or recording usage, reserving budgets for multi-step workloads, handling Unprice API results,

LLM Mart · 0 points · 13 views 36 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download jhonsfran-unprice-skills_integrate-unprice-sdk-36a9d90.zip · 16 KB

Install

skills CLI npx skills add https://github.com/jhonsfran/unprice/tree/main/skills/integrate-unprice-sdk
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jhonsfran-unprice@llmmart
Git git clone https://github.com/jhonsfran/unprice.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jhonsfran/unprice collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Integrate the Unprice SDK

Build the smallest correct Unprice integration at the boundary that owns the paid action. Preserve the host project's architecture and use the installed SDK types as the exact contract.

Route the request first. Defining or changing the project's plans, prices, features, or limits is configuration: read configuration-workflow.md and stop at reviewUrl. Checking access, enforcing, or recording usage at runtime: read operation-selection.md and follow the workflow below.

Workflow

  1. Inspect the project before editing.

    • Identify the package manager, framework, server boundary, environment validation, customer identity model, logging style, and test conventions.
    • Search for existing @unprice/api, Unprice, UNPRICE_TOKEN, customer ID mappings, feature slugs, event slugs, and idempotency helpers.
    • Reuse an existing Unprice client or integration owner instead of creating a parallel path.
  2. Identify the commercial decision.

    • Read operation-selection.md.
    • Choose one primary runtime path before writing code.
    • Do not infer product configuration from UI labels or plan names.
    • Keep stable capability and event identifiers in the host. Unprice owns plan configuration: names, membership, prices, limits, the default signup plan, and feature-to-plan rules. Hosts that own plan selection may pass an explicit, published, policy-approved planSlug or planVersionId at signup without copying that configuration.
    • For included metered usage plus overage, or a customer-specific spend cap, read configuration-workflow.md.
  3. Confirm prerequisites.

    • Require a published plan version and the actual plan, feature, event, and meter property slugs.
    • Determine whether the application already stores an Unprice customerId.
    • Use customers.signUp when Unprice must provision or map the customer money path.
    • Ask for missing business identifiers instead of inventing them.
  4. Install or reuse the SDK.

    • Use the project's existing package manager.
    • Add @unprice/api only when it is absent.
    • Create one server-only Unprice client using the project's environment conventions.
  5. Implement the integration.

    • Read integration-patterns.md for the selected path.
    • Keep the API token out of browser bundles, client components, public environment variables, logs, fixtures, and committed files.
    • Handle SDK/API errors separately from valid commercial denials.
    • Preserve one stable idempotency key for the same logical mutation across retries.
    • Make fail-open versus fail-closed behavior an explicit host-application policy.
  6. Verify before finishing.

    • Read verification.md.
    • Add focused tests for success, API error, commercial denial, and retry behavior.
    • Run the smallest relevant formatter, typecheck, and test commands from the host project.
    • Do not make a live Unprice call unless the user requests it and provides an appropriate non-production environment.

Non-negotiable rules

  • Keep Unprice API keys server-side.
  • Check stable feature slugs; never branch product behavior on plan display names.
  • Use access.entitlements.current for a customer-wide capability or billing UI snapshot. Use access.check when one paid action needs one current decision.
  • Treat access.check as a read-only decision.
  • Never use usage.record as a spend gate.
  • Do not begin paid work after a denied usage.consume or rejected run reservation.
  • A run reservation authorizes a finite currency envelope; it does not constrain a downstream provider by itself. Before starting variable-cost work, derive a conservative worst-case cost for the request, reserve that amount, and pass the matching cap to the provider (for example, maxTokens). If the known input plus the minimum useful output cannot fit, deny before the provider call. Never use post-response runs.consume as the only spend gate.
  • A nonzero runs.start reserves wallet funds. An active subscription alone is insufficient: provision a wallet grant, credit line, or top-up large enough for the expected simultaneous reservations.
  • Close short-lived runs in finally. For a deliberately long-lived conversation, set expiresAt, reuse its stable idempotency key throughout that window, and explicitly close it on cancellation or failure.
  • End a run as failed after exceptions or rejected consumption; do not report it as completed.
  • Reuse the same idempotency key when retrying the same logical mutation.
  • Do not replace a repository's existing billing or authorization owner with route-level orchestration when a service or use-case layer owns that behavior.
  • Do not guess exact request or response fields. Inspect the installed @unprice/api types or the official OpenAPI contract.

Contract precedence

When sources disagree, use this order:

  1. Installed @unprice/api TypeScript types.
  2. Current official OpenAPI document at https://docs.unprice.dev/openapi.json.
  3. Bundled references in this skill for stable behavioral guidance.
  4. Individual documentation examples.

If a bundled example no longer typechecks, adapt it to the installed contract and report the drift instead of weakening type safety.

Files (unprice)
  • agents
    • openai.yaml 227 B
      interface:
        display_name: "Integrate Unprice SDK"
        short_description: "Add safe Unprice SDK money-path integrations"
        default_prompt: "Use $integrate-unprice-sdk to integrate Unprice into this project's paid action safely."
      
  • references
    • configuration-workflow.md 9.1 KB
      # Configure a project's monetization
      
      Two operations own configuration: `monetization.get` reads how a project makes money, and
      `monetization.apply` writes it. Both speak the same document, so what `get` returns is what `apply`
      accepts.
      
      Configuration is not a runtime path. For `access.check`, `usage.consume`, `usage.record`, and
      budgeted runs, read [operation-selection.md](operation-selection.md) instead.
      
      ## The agent never publishes
      
      `monetization.apply` only ever creates or reuses **draft** plan versions. A plan version is either
      `draft` or `published`, and nothing in the API moves it between them. Publishing is a human action
      in the dashboard.
      
      `apply` returns `reviewUrl`, a dashboard link to the first draft it created. Handing that link to
      the user is where the agent's work ends. This is a hard stop, not a confirmation prompt to answer on
      the user's behalf: do not poll for publication, do not look for a publish method, do not proceed to
      integrate against a version that is still a draft.
      
      `reviewUrl` is `null` when every plan came back `unchanged` or `published`. Nothing needs review, so
      there is nothing to hand over.
      
      ## Workflow
      
      1. **Read the current configuration first.**
      
         ```ts
         const { result, error } = await unprice.monetization.get()
         ```
      
         Never write a document without reading one. `apply` takes the whole statement of how the project
         makes money — a plan left out of the document is a plan whose configuration the agent is no
         longer describing.
      
      2. **Handle `unrepresentablePlans` and `warnings` before composing anything.** See the two sections
         below. Both can stop the workflow.
      
      3. **Explain the proposed commercial model to the user, in prose, before writing anything.** Name
         each plan, its price and cadence, which features it includes, and every allowance. Say which plan
         is the default. Say what changes against what `get` returned. Get agreement on the money before
         composing the call, not after.
      
      4. **Compose the document and apply it.**
      
         ```ts
         const { result, error } = await unprice.monetization.apply({ config })
         ```
      
      5. **Stop, and hand the user `result.reviewUrl`.** Report the per-plan outcomes and any
         `staleDrafts`. Do not continue into integration code in the same breath — the runtime work needs
         a published version, and there isn't one yet.
      
      ## Read `unrepresentablePlans` as a stop sign
      
      `unrepresentablePlans` lists plans the project has that the document would **misstate** if it
      emitted them — for example a version anchored to a specific day of the month, which the document's
      billing cadence cannot express. They are excluded from `config`, never hidden.
      
      When this array is non-empty:
      
      - tell the user, by slug and reason;
      - **do not re-add the plan to the document.** Re-adding it from defaults looks like restoring a
        missing plan and is actually a silent rewrite of a live billing setting;
      - if the user wants that plan changed, that change belongs in the dashboard.
      
      Applying a document that omits an unrepresentable plan is safe. `apply` touches only the plans the
      document names.
      
      ## Key warning severity on `code`, never on the message
      
      `warnings` lists stored settings the emitted document is merely **silent** about. Those settings
      revert to server defaults if the document is applied back. The plan is still emitted, because hiding
      it would be worse.
      
      Severity is a property of the field, carried by `code`. Branch on the code. Do not string-match the
      message; message text is not a contract.
      
      | Code | Meaning | Action |
      | --- | --- | --- |
      | `enforcement_settings_dropped` | Commercial change | Stop. Get explicit human approval |
      | `version_settings_dropped` | Commercial change | Stop. Get explicit human approval |
      | `feature_settings_dropped` | Cosmetic or inert today | Report and continue |
      | `meter_fields_dropped` | Cosmetic or inert today | Report and continue |
      
      **This table is the contract for which codes block.** The blocking subset has no SDK representation
      — it is not a named schema in the OpenAPI document, and the server-side constant
      `MONETIZATION_BLOCKING_WARNING_CODES` ships from a package that is never published. Do not go
      looking for an import; take the two blocking values from the table above.
      
      The four code *values* do reach the installed types, so a typo fails to compile instead of silently
      missing a warning. The union is inline, so the reference is the whole path rather than a named
      component:
      
      ```ts
      type WarningCode =
        operations["monetization.get"]["responses"][200]["content"]["application/json"]["warnings"][number]["code"]
      // "enforcement_settings_dropped" | "version_settings_dropped"
      //   | "feature_settings_dropped" | "meter_fields_dropped"
      ```
      
      Warnings fire almost exclusively for versions authored in the dashboard. A version `apply` created
      never warns.
      
      ## There is no configuration file
      
      The agent composes a typed object and passes it to `monetization.apply`. There is no YAML, no JSON
      config, no schema file to keep in sync, and no drift check to add to CI. The installed `@unprice/api`
      types are the contract — the same precedence rules as the rest of this skill.
      
      Keeping the call as a script in the host project is fine and often useful as a record of what was
      sent. Nothing reads it back. Re-running it is safe, and deleting it changes nothing.
      
      Do not copy this commercial document into runtime application constants. The host keeps stable
      feature and event identifiers. Unprice remains the source of plan names, membership, prices,
      limits, default signup, and feature-to-plan rules so pricing operators can iterate without an
      application release.
      
      ## Idempotency and drafts
      
      `apply` is content-addressed. Each plan's desired version is hashed, and the same document sent
      twice reuses the version it made the first time.
      
      Per-plan outcomes:
      
      | `status` | Meaning |
      | --- | --- |
      | `created` | This apply wrote a new draft, or finished one an interrupted apply left half-written |
      | `unchanged` | A complete draft already matched |
      | `published` | A live version already matches. Nothing was written |
      
      Retrying after a timeout, a crash, or a partial failure means sending the identical request body.
      There is no state for the agent to carry and no cleanup to perform.
      
      `staleDrafts` lists drafts made by an earlier, now-superseded document. They are reported, never
      deleted. Surface them to the user; removing them is a dashboard action.
      
      A version authored in the dashboard carries no content address, so a document read with `get` and
      applied back will mint a draft for those plans. That is expected, not a bug — the read never writes.
      
      ## Composing the document
      
      Do not guess field shapes. Inspect the installed `@unprice/api` types.
      
      Rules that are easy to get wrong:
      
      - **Unlimited has exactly one spelling: omit `limit`.** `limit: 0` and `limit: null` are rejected at
        the boundary. A zero allowance is unstorable, and a second spelling of unlimited would hash
        differently and break idempotency.
      - **To make a feature unavailable on a plan, leave it out of that plan** — not `limit: 0`.
      - **Exactly one plan sets `defaultPlan: true`.** `customers.signUp` without a `planSlug` selects
        that plan, so a project needs one.
      - **Every feature a plan prices must be declared in the document's `features`**, and every event a
        meter references must be declared in `events`.
      - Prices cross the boundary as decimal strings, for example `"0.000002"`.
      
      ## Model included usage and an overage cap separately
      
      Use a `usageMode: "tier"`, `tierMode: "graduated"` feature when a plan includes
      some metered units and charges beyond them. For example, 1,000,000 included
      tokens followed by `$0.00001` per token has a zero-priced first tier and a
      second tier beginning at unit 1,000,001. Omit `limit` when usage should continue
      into the paid tier.
      
      This prices the included allowance; it is not a customer spend cap. To cap a
      customer's monthly exposure, pass `creditLinePolicy: "capped"` and
      `creditLineAmountMinor` to `customers.signUp`. The amount is in currency minor
      units (`1000` is `$10.00` USD) and applies to that customer's subscription
      phase. Keep the tiered plan price and the signup cap aligned deliberately.
      
      A nonzero budgeted run is a wallet reservation, not a unit allowance. A customer
      with an active subscription but no spendable wallet credit cannot start one. Set
      the signup credit line high enough for both the maximum period spend and the
      largest set of simultaneous holds. For example, three `$0.10` chat runs can hold
      `$0.30` at once even if a daily token limit keeps their combined eventual spend
      to `$0.10`.
      
      ## After a human publishes
      
      `apply` returns `integrationContract`: what the application actually has to call at runtime for the
      configuration to work — the required event properties per event, and how each feature is integrated
      (`flat-access`, `usage-gate`, `usage-evidence`, or `run-budget`).
      
      Use it to pick the runtime path, then follow
      [operation-selection.md](operation-selection.md) and
      [integration-patterns.md](integration-patterns.md). Its `warnings` flag features whose usage is not
      knowable before the work runs, which rules out a guessed `usage.consume` quantity.
      
      Do not write runtime integration code against a draft version. Wait for the user to confirm they
      published.
      
    • integration-patterns.md 11.4 KB
      # Unprice integration patterns
      
      Adapt these patterns to the host project. Match its environment validation, dependency injection,
      service boundaries, logging, errors, response conventions, and tests.
      
      Inspect the installed `@unprice/api` types before copying a request shape.
      
      ## Create one server-only client
      
      Keep client construction in a server-only module or dependency composition root.
      
      ```ts
      import { Unprice } from "@unprice/api"
      
      const token = process.env.UNPRICE_TOKEN
      
      if (!token) {
        throw new Error("UNPRICE_TOKEN is required")
      }
      
      export const unprice = new Unprice({ token })
      ```
      
      Prefer the project's validated environment object when one exists. In Next.js, preserve existing
      `server-only` boundaries. Never export this client from a browser-safe package barrel.
      
      Set `baseUrl` only for an intentional self-hosted or test environment. Do not silently redirect
      production traffic.
      
      ## Handle errors and denials separately
      
      Every SDK call returns an explicit result or error. Check `error` before reading `result`.
      
      ```ts
      const { result, error } = await unprice.access.check({
        customerId,
        featureSlug,
      })
      
      if (error) {
        logger.error(new Error(error.message), {
          operation: "unprice.access.check",
          requestId: error.requestId,
          code: error.code,
        })
      
        return applyConfiguredOutagePolicy()
      }
      
      if (!result.allowed) {
        return denyPaidAction(result)
      }
      
      return performPaidAction()
      ```
      
      An error means Unprice could not provide a valid commercial decision. A result with
      `allowed: false` is a valid denial. Do not collapse both into the same generic catch or silently
      allow work.
      
      Use the host project's logger and error primitives. Do not introduce `console.log`.
      
      ## Read the current capability snapshot
      
      Use one current-entitlements call when account UI or application state needs several feature
      decisions. Keep stable feature slugs in code and let Unprice decide which plans grant them.
      
      ```ts
      const { result, error } = await unprice.access.entitlements.current({ customerId })
      
      if (error) {
        logger.error(new Error(error.message, { cause: error }), {
          operation: "unprice.access.entitlements.current",
          requestId: error.requestId,
          code: error.code,
        })
      
        return applyConfiguredOutagePolicy()
      }
      
      const sharing = result.entitlements.find(
        (entitlement) => entitlement.featureSlug === "public-sharing"
      )
      
      const canSharePublicly =
        sharing?.status === "available" && sharing.allowed
      ```
      
      Treat unavailable or missing rows as unavailable according to the host's explicit outage and UI
      policy. Do not infer access from `planSlug`, a plan title, or price metadata.
      
      ## Provision and map a customer
      
      Call `customers.signUp` at the application workflow that owns customer or subscription onboarding.
      Persist the returned Unprice ID.
      
      ```ts
      const { result, error } = await unprice.customers.signUp({
        name: account.name,
        email: account.billingEmail,
        externalId: account.id,
        successUrl,
        cancelUrl,
      })
      
      if (error) {
        throw new Error(error.message)
      }
      
      await accounts.saveUnpriceCustomerId({
        accountId: account.id,
        unpriceCustomerId: result.customerId,
      })
      ```
      
      Make the surrounding onboarding workflow idempotent using the host application's established
      pattern. Do not call signup from every request path.
      
      Omit plan selectors when Unprice owns the default signup plan. Add `planSlug` or `planVersionId`
      only for an explicit host-owned selection. Do not copy the default plan into application code.
      
      Use a capped credit line only when the product needs a hard customer-specific
      spend ceiling. It is independent of a plan's usage tiers: model included units
      and overage with graduated tiers, then pass the cap in currency minor units at
      signup.
      
      ## Change to a catalog-selected plan
      
      Load the current plan catalog from Unprice for display. Submit the selected `planVersionId`, then
      reload and validate that exact version on the server before changing the subscription. Reject
      managed or unavailable versions by their API metadata, not by plan name.
      
      ```ts
      const target = await loadCurrentPlanVersion(planVersionId)
      
      if (!target || target.enterprise) {
        return rejectSelfServiceChange()
      }
      
      const { result, error } = await unprice.customers.changePlan({
        customerId,
        planVersionId: target.planVersionId,
      })
      
      if (error) {
        throw new Error(error.message)
      }
      
      return result
      ```
      
      Do not encode a `free -> pro -> enterprise` ladder, plan-specific credit lines, or display-name
      checks in the host. Pricing operators must be able to change packaging without an application
      release.
      
      ## Add a read-only shadow check
      
      Call `access.check` beside the current decision without changing production behavior. Record the
      comparison with the existing observability system.
      
      ```ts
      const currentDecision = await currentAuthorization.canRun(input)
      const { result, error } = await unprice.access.check({
        customerId: input.unpriceCustomerId,
        featureSlug: input.featureSlug,
      })
      
      if (error) {
        logger.error(new Error(error.message), {
          operation: "unprice.access.check.shadow",
          requestId: error.requestId,
          code: error.code,
        })
      } else {
        logger.info("unprice shadow decision", {
          currentAllowed: currentDecision.allowed,
          unpriceAllowed: result.allowed,
          featureSlug: input.featureSlug,
        })
      }
      
      return currentDecision
      ```
      
      Do not send secrets or sensitive customer data into logs. Use the repository's structured logging
      shape rather than copying these field names blindly.
      
      ## Enforce known usage synchronously
      
      Derive the idempotency key from a stable logical request or command ID.
      
      ```ts
      const { result, error } = await unprice.usage.consume({
        customerId,
        featureSlug: "ai-messages",
        eventSlug: "completions",
        idempotencyKey: requestId,
        properties: {
          aiMessages: 1,
          inputTokens,
          outputTokens,
        },
      })
      
      if (error) {
        throw new Error(error.message)
      }
      
      if (!result.allowed) {
        return denyPaidAction(result)
      }
      
      return performPaidAction()
      ```
      
      Place this call before the cost is created. Reuse `requestId` when retrying the same logical paid
      action. Do not use `crypto.randomUUID()` inside the retry loop.
      
      If the actual charge is unknown until after work runs, decide whether the action needs a budgeted
      run rather than pretending a guessed consume amount is exact.
      
      ## Record usage asynchronously
      
      Use the host's durable background or after-response mechanism when available.
      
      ```ts
      const { error } = await unprice.usage.record({
        customerId,
        eventSlug: "completions",
        idempotencyKey: requestId,
        properties: {
          aiMessages: 1,
          inputTokens,
          outputTokens,
        },
      })
      
      if (error) {
        logger.error(new Error(error.message), {
          operation: "unprice.usage.record",
          requestId: error.requestId,
          code: error.code,
        })
      }
      ```
      
      Adapt the call to `waitUntil`, a job queue, or another established mechanism only when the host
      already guarantees its lifecycle. Never drop the promise without error handling.
      
      Do not branch paid-work authorization on the outcome of `usage.record`.
      
      ## Hard-cap a variable-cost provider call
      
      Use this pattern when a provider call must not start unless its maximum possible cost fits the
      customer and workload budgets. A run is the financial authorization; the provider cap is what
      keeps the external call inside that authorization.
      
      The host must derive `worstCaseAmountMinor` from a model/provider pricing table it owns. Include
      known input, request overhead, and the configured output maximum. Do not substitute an estimate
      when the product promises a hard cap.
      
      ```ts
      const proposed = priceModelRequest({
        model,
        inputTokens: exactInputTokens,
        // Determine this from the remaining customer and conversation allowance.
        maxOutputTokens: affordableOutputTokens,
      })
      
      if (proposed.maxOutputTokens < minimumUsefulOutputTokens) {
        return denyPaidAction({ reason: "INSUFFICIENT_BUDGET" })
      }
      
      const { result: run, error: startError } = await unprice.runs.start({
        customerId,
        budgetAmountMinor: proposed.worstCaseAmountMinor,
        idempotencyKey: `chat:${chatId}:message:${messageId}`,
        workloadType: "custom",
        workloadId: messageId,
        metadata: { workload_kind: "chat_message" },
      })
      
      if (startError) throw new Error(startError.message)
      if (run.status !== "running" || run.remainingAmountMinor < proposed.worstCaseAmountMinor) {
        return denyPaidAction(run)
      }
      
      let finalStatus: "completed" | "failed" = "completed"
      
      try {
        const response = await modelProvider.stream({
          messages,
          maxTokens: proposed.maxOutputTokens,
        })
        const actual = await response.usage
      
        const { result: consumption, error: consumeError } = await unprice.runs.consume({
          runId: run.runId,
          featureSlug,
          eventSlug,
          idempotencyKey: `chat:${chatId}:message:${messageId}:usage`,
          properties: actualMeterProperties(actual),
        })
      
        if (consumeError) throw new Error(consumeError.message)
        if (!consumption.accepted) {
          finalStatus = "failed"
          throw new Error("Unprice rejected a usage amount that the reservation should have covered")
        }
      
        return response
      } catch (error) {
        finalStatus = "failed"
        throw error
      } finally {
        const { error: endError } = await unprice.runs.end({
          runId: run.runId,
          status: finalStatus,
        })
      
        if (endError) {
          logger.error(new Error(endError.message), {
            operation: "unprice.runs.end",
            requestId: endError.requestId,
            code: endError.code,
            runId: run.runId,
          })
        }
      }
      ```
      
      If a provider exposes no enforceable cap or the host cannot price the request's maximum possible
      cost, this is not a hard-cap integration. Reject the request before the provider call or use
      asynchronous metering with an explicit product decision to accept the risk.
      
      ## Budget a multi-step workload
      
      Track final status explicitly so failures are not reported as completed.
      
      ```ts
      const { result: run, error: startError } = await unprice.runs.start({
        customerId,
        budgetAmountMinor,
        idempotencyKey: `run:${workloadId}`,
        workloadType: "workflow",
        workloadId,
      })
      
      if (startError) {
        throw new Error(startError.message)
      }
      
      if (run.status !== "running") {
        return rejectWorkload(run)
      }
      
      let finalStatus: "completed" | "failed" = "completed"
      
      try {
        const { result, error } = await unprice.runs.consume({
          runId: run.runId,
          featureSlug,
          eventSlug,
          idempotencyKey: `run:${workloadId}:step:${stepId}`,
          properties,
        })
      
        if (error) {
          throw new Error(error.message)
        }
      
        if (!result.accepted) {
          finalStatus = "failed"
          return rejectWorkload(result)
        }
      
        return await performStep()
      } catch (error) {
        finalStatus = "failed"
        throw error
      } finally {
        const { error: endError } = await unprice.runs.end({
          runId: run.runId,
          status: finalStatus,
        })
      
        if (endError) {
          logger.error(new Error(endError.message), {
            operation: "unprice.runs.end",
            requestId: endError.requestId,
            code: endError.code,
            runId: run.runId,
          })
        }
      }
      ```
      
      Use a unique, stable idempotency key for each billable step. Keep the key stable when retrying that
      step.
      
      Do not let a `runs.end` error mask the original workload exception. Report it separately through
      the host's logger or error aggregation.
      
      ## Preserve architectural ownership
      
      Put Unprice orchestration at the layer that already owns the paid action:
      
      - Use a service or use case when several routes, jobs, or procedures share the workflow.
      - Keep HTTP and RPC adapters responsible for validation, authentication, and error mapping.
      - Inject the Unprice client when the project uses dependency composition.
      - Keep provider calls and paid work after the Unprice allow/reservation boundary.
      
      Avoid creating a second billing abstraction unless the host project already requires one.
      
    • operation-selection.md 7.6 KB
      # Choose the Unprice operation
      
      Select the operation from the commercial behavior the application needs, not from method-name
      similarity.
      
      ## Decision table
      
      | Application need | Operation | Blocks over-budget work? | Mutates state? |
      | --- | --- | --- | --- |
      | Ask whether a customer may use a feature | `access.check` | No | No |
      | Read all current capabilities for account UI or application state | `access.entitlements.current` | No | No |
      | Compare Unprice with existing logic in shadow | `access.check` | No | No |
      | Enforce a known usage amount before work runs | `usage.consume` | Yes | Yes |
      | Report usage for metering and invoice evidence | `usage.record` | No | Yes, asynchronously |
      | Reserve a budget for multi-step work | `runs.start` / `runs.consume` / `runs.end` | Yes | Yes |
      | Provision a customer from a published plan | `customers.signUp` | Not a runtime gate | Yes |
      
      Use this shorthand:
      
      > `check` asks. `consume` decides and applies known usage now. `record` reports what happened.
      > `runs` reserve before a variable-cost workload starts.
      
      ## `access.entitlements.current`
      
      Use `access.entitlements.current` when the application needs one customer-wide snapshot for account
      UI, capability discovery, or several independent feature flags. Match rows by stable `featureSlug`.
      Use `status` and `allowed` as returned, including unavailable rows. Do not reconstruct capability
      state from the subscription plan name, plan slug, price, or a host-owned plan table.
      
      Use `access.check` instead when one action needs one authoritative decision immediately before the
      action. A snapshot is useful application state; it is not a replacement for an action-specific
      gate when freshness or enforcement matters.
      
      ## `access.check`
      
      Use `access.check` for a read-only preflight. It resolves current entitlement, limit, budget, or
      credit context without consuming usage or reserving funds.
      
      Choose it for:
      
      - shadow adoption beside existing authorization logic;
      - preflight UI or API decisions;
      - feature access where no usage amount must be applied;
      - comparing Unprice decisions before enabling enforcement.
      
      Do not treat a successful HTTP response as an allow. Branch on `result.allowed`.
      
      Do not treat an API error as a commercial denial. Apply the host application's explicit outage
      policy and preserve the error's request ID in server logs.
      
      ## `usage.consume`
      
      Use `usage.consume` when the usage amount is known before the paid action and the request path must
      deny work once the customer reaches a limit, budget, or credit boundary.
      
      Choose it for:
      
      - one API call with known metered properties;
      - one export, generation, or job with a bounded known charge;
      - synchronous enforcement before provider or infrastructure cost is created.
      
      Send a stable `idempotencyKey` for the logical action. Reuse it after timeouts and retries. Never
      generate a replacement key merely because the transport attempt changed.
      
      Only perform the paid action after `result.allowed` is true.
      
      ## `usage.record`
      
      Use `usage.record` to enqueue usage for metering, analytics, and invoice evidence without making it
      a synchronous commercial gate.
      
      Choose it for:
      
      - background or eventual metering;
      - usage that may exceed funds by product policy;
      - evidence collection after work already happened;
      - one event that feeds multiple meters through its properties.
      
      `usage.record` never denies over-budget work. Do not call it before an expensive action and assume
      that awaiting the SDK response authorizes the work.
      
      The SDK invocation still performs network I/O and can return an error. Use the host application's
      existing background-task or after-response mechanism when the request must not wait; do not invent
      an unreliable fire-and-forget promise.
      
      ## Budgeted runs
      
      Use `runs.start`, `runs.consume`, and `runs.end` when a workload has multiple billable steps or an
      uncertain final cost.
      
      Choose runs for:
      
      - agents and tool chains;
      - workflows and multi-step jobs;
      - long-running tasks;
      - nested or iterative work that needs an up-front budget envelope.
      
      Follow the lifecycle:
      
      1. Call `runs.start` before paid work.
      2. Continue only when the returned run is `running`.
      3. Call `runs.consume` as billable steps spend the reservation.
      4. Stop the workload when consumption is rejected.
      5. Call `runs.end` in `finally` to release unused funds.
      6. End as `failed` after exceptions or rejected consumption.
      
      ### Hard caps for AI and other variable-cost provider calls
      
      `runs.start` reserves the amount the application supplies. It cannot discover or limit the cost
      of a provider request that the application has not bounded. For a hard "do not create more cost"
      guarantee, make each provider call a finite proposed operation before it starts:
      
      1. Price the known input with the provider/model's actual pricing contract, including any fixed
         request overhead.
      2. Choose a maximum output that fits the remaining approved envelope and configure the provider
         with that exact limit (for example, `maxTokens`).
      3. Start a run for that worst-case amount. Continue only when it is `running` with sufficient
         remaining budget.
      4. Call the provider only with the matching cap, then consume the actual usage and end the run.
      5. Treat an unexpected rejected consumption as a fail-closed mismatch: end the run as `failed`,
         log it, and do not start another paid step.
      
      For chat, a per-message run is usually the clearest authorization boundary. Its budget must be
      the lesser of the customer's remaining allowance and the conversation's remaining allowance.
      The application must atomically reserve that conversation allowance before calling the provider;
      separate message runs otherwise protect only the customer's total wallet, not a per-conversation
      cap. If the host cannot calculate a safe upper bound for the model, tool, or provider request,
      deny it before execution or accept that the action is metering-only rather than hard-capped.
      
      Do not use a run as a customer identity. The customer remains the economic actor; the run labels
      and bounds the workload.
      
      ## Customer provisioning
      
      Use `customers.signUp` when Unprice should create or map the customer and provision the money path
      from a published plan version. Persist the returned Unprice `customerId` against the application's
      stable account or tenant ID.
      
      Omit `planSlug` and `planVersionId` when Unprice owns the project's default signup plan. Use
      `planSlug` for the latest published version selected by an explicit host policy, or `planVersionId`
      when the integration must pin an exact published version.
      
      For self-service plan changes, send the selected `planVersionId` from the current Unprice plan
      catalog and validate it again on the server. Never search for a plan called `pro`, encode a plan
      order, or keep an application-owned list of upgrade targets.
      
      Do not recreate the customer on each request. Reuse the stored mapping.
      
      ## Names and identifiers
      
      - Use `customerId` for the Unprice customer, not an arbitrary application user ID unless the
        installed contract explicitly accepts the external ID.
      - Use `featureSlug` for the sellable capability being checked or consumed.
      - Use `eventSlug` for the broad activity the application emits.
      - Use `properties` for meter inputs such as token counts, requests, seats, or bytes.
      - Use a stable idempotency key for each logical mutation.
      
      Never invent slugs. Obtain them from the application's configuration, Unprice dashboard output,
      tests, fixtures, or the user.
      
      ## Deeper documentation
      
      - Runtime choice: `https://docs.unprice.dev/quickstart/choose-operation`
      - First customer: `https://docs.unprice.dev/quickstart/onboarding-customer`
      - SDK overview: `https://docs.unprice.dev/libraries/ts/sdk/overview`
      - Documentation index: `https://docs.unprice.dev/llms.txt`
      
    • verification.md 4.5 KB
      # Verify an Unprice integration
      
      Review behavior at the owning service or use-case layer. Keep adapter tests focused on validation,
      authentication, and error mapping.
      
      ## Static checks
      
      - Confirm `@unprice/api` is installed with the host package manager.
      - Confirm one server-only client owns the token.
      - Search browser and client bundles for `UNPRICE_TOKEN`, `Unprice`, or public token exposure.
      - Confirm environment values are validated using the project's existing mechanism.
      - Confirm feature, event, and plan slugs come from real configuration.
      - Confirm the host does not copy plan names, plan order, membership, prices, limits, the default
        signup plan, or feature-to-plan rules into runtime code.
        Permit an explicit, published, policy-approved signup plan identifier when the host owns
        selection; reject duplicated plan configuration.
      - Confirm exact request shapes typecheck against the installed SDK.
      - Confirm no new `any`, unhandled promise, or `console.log` was introduced.
      - Confirm errors log `error.requestId` without logging the token or sensitive payloads.
      
      ## Behavioral checks
      
      For `access.check`:
      
      - Test allowed, denied, and API-error outcomes.
      - Confirm the call does not mutate usage.
      - In shadow mode, confirm existing authorization still controls behavior.
      
      For `access.entitlements.current`:
      
      - Test available, denied, unavailable, missing, and API-error outcomes.
      - Confirm feature behavior matches stable feature slugs, not plan names.
      - Confirm account UI uses one customer-wide snapshot instead of one request per displayed feature.
      
      For `usage.consume`:
      
      - Test allowed, denied, and API-error outcomes.
      - Confirm paid work runs only after an allow.
      - Retry the logical request and assert that the same idempotency key is reused.
      - Confirm a denial is not treated as an exception or transport failure.
      
      For `usage.record`:
      
      - Confirm the integration never uses it to authorize paid work.
      - Confirm the returned promise is awaited or attached to an established durable lifecycle.
      - Test and observe reporting errors without changing the paid-action decision.
      - Confirm event properties match the configured meter inputs.
      
      For budgeted runs:
      
      - Test start error and non-running start results.
      - Test accepted and rejected consumption.
      - Test workload exceptions.
      - Assert `runs.end` executes for every started run.
      - Assert rejected consumption and exceptions end with `status: "failed"`.
      - Assert an end error does not hide the original workload error.
      - Assert every step has its own stable idempotency key.
      
      For a hard-capped variable-cost provider call:
      
      - Test that an insufficient known-input or output envelope rejects before the provider is called.
      - Test that the provider receives the same output cap used to calculate the run reservation.
      - Test that one conversation cannot reserve more than its remaining conversation allowance.
      - Test an unexpected rejected settlement as a failure that prevents the next paid call.
      
      For customer signup:
      
      - Confirm signup occurs in the onboarding/subscription owner, not every request.
      - Confirm the returned Unprice customer ID is persisted against the stable account or tenant.
      - Confirm a retry cannot create an accidental parallel mapping.
      - When Unprice owns the default, confirm signup omits `planSlug` and `planVersionId`.
      - When the host selects a plan, confirm the version is published and the choice matches explicit
        product policy.
      
      ## Failure-policy checks
      
      Require an explicit policy for Unprice API errors:
      
      - Fail closed when creating unbounded customer cost would be unsafe.
      - Fail open only when the product owner intentionally accepts the margin and abuse risk.
      - Preserve the request ID and operation in server-side observability.
      - Do not turn an outage into a fake commercial denial unless the product explicitly wants that UX.
      
      ## Commands
      
      Use the host project's documented commands. Run the smallest relevant checks first:
      
      1. Format or lint only changed files.
      2. Typecheck the changed package.
      3. Run focused unit tests.
      4. Broaden validation when the integration owns money-path or request-path behavior.
      
      Do not start development servers, create live customers, consume production usage, or reserve real
      funds merely to validate generated code.
      
      ## Completion report
      
      State:
      
      - which Unprice operation was selected and why;
      - where the server-only client and customer mapping live;
      - how errors, denials, and outages behave;
      - how idempotency keys are derived and reused;
      - which tests and validation commands passed;
      - which dashboard configuration or live verification remains for the user.
      
  • SKILL.md 5.9 KB
    ---
    name: integrate-unprice-sdk
    description: Integrate and troubleshoot the @unprice/api TypeScript SDK in server-side Node.js, Next.js, Hono, or edge applications. Use when onboarding Unprice customers, checking access, enforcing or recording usage, reserving budgets for multi-step workloads, handling Unprice API results, reviewing an existing Unprice integration, adding customer-spend authorization before paid work runs, or writing a project's plan, feature, price, and limit configuration into Unprice.
    license: MIT
    ---
    
    # Integrate the Unprice SDK
    
    Build the smallest correct Unprice integration at the boundary that owns the paid action. Preserve
    the host project's architecture and use the installed SDK types as the exact contract.
    
    Route the request first. Defining or changing the project's plans, prices, features, or limits is
    configuration: read [configuration-workflow.md](references/configuration-workflow.md) and stop at
    `reviewUrl`. Checking access, enforcing, or recording usage at runtime: read
    [operation-selection.md](references/operation-selection.md) and follow the workflow below.
    
    ## Workflow
    
    1. Inspect the project before editing.
       - Identify the package manager, framework, server boundary, environment validation, customer
         identity model, logging style, and test conventions.
       - Search for existing `@unprice/api`, `Unprice`, `UNPRICE_TOKEN`, customer ID mappings, feature
         slugs, event slugs, and idempotency helpers.
       - Reuse an existing Unprice client or integration owner instead of creating a parallel path.
    
    2. Identify the commercial decision.
       - Read [operation-selection.md](references/operation-selection.md).
       - Choose one primary runtime path before writing code.
       - Do not infer product configuration from UI labels or plan names.
       - Keep stable capability and event identifiers in the host. Unprice owns plan configuration:
         names, membership, prices, limits, the default signup plan, and feature-to-plan rules.
         Hosts that own plan selection may pass an explicit, published, policy-approved `planSlug`
         or `planVersionId` at signup without copying that configuration.
       - For included metered usage plus overage, or a customer-specific spend cap,
         read [configuration-workflow.md](references/configuration-workflow.md).
    
    3. Confirm prerequisites.
       - Require a published plan version and the actual plan, feature, event, and meter property slugs.
       - Determine whether the application already stores an Unprice `customerId`.
       - Use `customers.signUp` when Unprice must provision or map the customer money path.
       - Ask for missing business identifiers instead of inventing them.
    
    4. Install or reuse the SDK.
       - Use the project's existing package manager.
       - Add `@unprice/api` only when it is absent.
       - Create one server-only `Unprice` client using the project's environment conventions.
    
    5. Implement the integration.
       - Read [integration-patterns.md](references/integration-patterns.md) for the selected path.
       - Keep the API token out of browser bundles, client components, public environment variables,
         logs, fixtures, and committed files.
       - Handle SDK/API errors separately from valid commercial denials.
       - Preserve one stable idempotency key for the same logical mutation across retries.
       - Make fail-open versus fail-closed behavior an explicit host-application policy.
    
    6. Verify before finishing.
       - Read [verification.md](references/verification.md).
       - Add focused tests for success, API error, commercial denial, and retry behavior.
       - Run the smallest relevant formatter, typecheck, and test commands from the host project.
       - Do not make a live Unprice call unless the user requests it and provides an appropriate
         non-production environment.
    
    ## Non-negotiable rules
    
    - Keep Unprice API keys server-side.
    - Check stable feature slugs; never branch product behavior on plan display names.
    - Use `access.entitlements.current` for a customer-wide capability or billing UI snapshot. Use
      `access.check` when one paid action needs one current decision.
    - Treat `access.check` as a read-only decision.
    - Never use `usage.record` as a spend gate.
    - Do not begin paid work after a denied `usage.consume` or rejected run reservation.
    - A run reservation authorizes a finite currency envelope; it does not constrain a downstream
      provider by itself. Before starting variable-cost work, derive a conservative worst-case cost
      for the request, reserve that amount, and pass the matching cap to the provider (for example,
      `maxTokens`). If the known input plus the minimum useful output cannot fit, deny before the
      provider call. Never use post-response `runs.consume` as the only spend gate.
    - A nonzero `runs.start` reserves wallet funds. An active subscription alone is
      insufficient: provision a wallet grant, credit line, or top-up large enough
      for the expected simultaneous reservations.
    - Close short-lived runs in `finally`. For a deliberately long-lived conversation,
      set `expiresAt`, reuse its stable idempotency key throughout that window, and
      explicitly close it on cancellation or failure.
    - End a run as `failed` after exceptions or rejected consumption; do not report it as `completed`.
    - Reuse the same idempotency key when retrying the same logical mutation.
    - Do not replace a repository's existing billing or authorization owner with route-level
      orchestration when a service or use-case layer owns that behavior.
    - Do not guess exact request or response fields. Inspect the installed `@unprice/api` types or the
      official OpenAPI contract.
    
    ## Contract precedence
    
    When sources disagree, use this order:
    
    1. Installed `@unprice/api` TypeScript types.
    2. Current official OpenAPI document at `https://docs.unprice.dev/openapi.json`.
    3. Bundled references in this skill for stable behavioral guidance.
    4. Individual documentation examples.
    
    If a bundled example no longer typechecks, adapt it to the installed contract and report the drift
    instead of weakening type safety.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related