Skip to main content
skill CX Codex

aws-marketplace-metering — Claude Code skill for AWS Marketplace

A skill that deploys and debugs AWS Marketplace usage-based metering: the SAM pipeline, ResolveCustomer, BatchMeterUsage, plus record and failure analysis.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a claude-code
Open on GitHub

What aws-marketplace-metering does

The skill covers the whole SaaS PAYG metering lifecycle. For setup it builds a serverless integration with SAM: ResolveCustomer, BatchMeterUsage and EventBridge. For existing traffic it helps find and inspect submitted records through CloudTrail Event History and Seller Reports.

Packaged scripts/, assets/ and references/ show how to structure a metering stack. They are a reference, not code to copy. Every seller-specific value — product code, regions, stage name, VMT tags, alarm thresholds — goes into the marked TODO: spots. The two-stack layout and the awsmp-* naming stay as they are.

One thing to weigh before you run it: the skill generates CloudFormation/SAM templates, Lambda handlers, IAM policies and deploy.sh, and those land in your AWS account. Metering mistakes hit revenue directly, so review every created resource and run the full test plan in references/test-plan.md before real billing traffic. Also note CustomerIdentifier is a deprecated legacy field, null for new SaaS integrations — new products use CustomerAWSAccountId and LicenseArn per usage record.

What it helps with

  • deploy a SAM metering pipeline for a SaaS PAYG product
  • inspect BatchMeterUsage records and their statuses
  • trace why a record came back DuplicateRecord or CustomerNotSubscribed
  • pull billed history from Seller Reports
  • check which region to call BatchMeterUsage in

Example prompts for the agent

  • Deploy AWS Marketplace metering for my SaaS product with SAM
  • Why are my BatchMeterUsage records returning DuplicateRecord?
  • Show billed usage from Seller Reports for last month

How to install aws-marketplace-metering

1Skills CLI (Vercel)

npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a claude-code

2Manually: put the files in place

~/.claude/skills/aws-marketplace-metering/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/aws-marketplace-metering/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about aws-marketplace-metering

How do I install aws-marketplace-metering in Claude?

Run npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a claude-code. The skill file then sits at ~/.claude/skills/aws-marketplace-metering/SKILL.md.

What does aws-marketplace-metering do?

It deploys a serverless metering integration for SaaS PAYG, then helps query and debug submitted records: statuses, failures, discrepancies.

What should I check before running it in production?

The skill generates templates, Lambda code and IAM policies that go into your account. Put them through code review and run the full test plan in references/test-plan.md before real billing traffic.

Source text

SKILL.md · Apache-2.0
---
name: aws-marketplace-metering
description: Deploys, queries, and debugs AWS Marketplace usage-based (PAYG) metering — the pipeline (ResolveCustomer, BatchMeterUsage, EventBridge via SAM) and querying/debugging metering records, statuses, failures, discrepancies. Covers Concurrent Agreements (CustomerAWSAccountId + LicenseArn), multi-product, CloudTrail Event History (90 days; never CloudTrail Lake), Seller Reports (Billed Revenue/Usage columns + CSV/SDDS parsing), existing-stack detection, metering region for BatchMeterUsage/MeterUsage (DisabledApiException), metered AMI/Container + IAM roles, where registration data lives (customer-profile table), legacy ProductCode + CustomerIdentifier, DuplicateRecord/CustomerNotSubscribed, bad LicenseArn. Also SCOPES adjacent asks — flat-rate/SaaS-Subscription "do I still meter", CCP included-units + overage, contract term/annual renewal, forecasting future bills/usage — with the right answer or a one-line redirect. Use for any BatchMeterUsage/ResolveCustomer or SaaS usage-billing question.
version: 2
---

# AWS Marketplace Metering

## Overview

Complete lifecycle management for AWS Marketplace usage-based metering:

- **Setup (SaaS PAYG)** — deploy a new serverless metering integration (SAM, ResolveCustomer, BatchMeterUsage, EventBridge).
- **Query** — find, inspect, debug, analyze existing metering submissions (CloudTrail Event History ≤90 days; Seller Reports for billed history).

Scope is SaaS Usage-based (PAYG). SaaS Contract, Contract with Consumption Pricing (CCP), and AMI/Container are out of scope — redirect to public AWS docs.

> **Recommended:** the AWS MCP server provides sandboxed execution and audit logging.
>
> ⚠️ **AI-generated output — review before production. This skill is executed by an AI agent and
> can make mistakes.** Everything it generates (CloudFormation/SAM templates, Lambda handlers,
> IAM policies, `deploy.sh`) and everything it deploys to a seller's AWS account MUST be treated
> as a proposal to review, not a trusted artifact. Before any deployed stack carries PRODUCTION
> metering traffic (real billing), the seller MUST:
>
> - **Thoroughly review every resource the skill created or changed** — templates, Lambda code,
>   IAM roles/policies (least-privilege, no unintended grants), tables/indexes, queues, alarms —
>   in their own account, and confirm it matches their intent and their org's standards. Metering
>   errors directly affect revenue (under- or over-billing), so this review is not optional.
> - **Put the generated code through the seller's normal code review + change-management process**
>   (do not ship AI output straight to production), and
> - **Run the full hands-on test plan** (`references/test-plan.md`) end-to-end and verify each
>   expected observable outcome BEFORE relying on the integration for real billing.
> The skill SHALL surface this review-before-production expectation to the seller, and SHALL NOT
> imply that a successful deploy means the integration is production-ready without this review + test.

## Using this skill's packaged resources

The packaged `scripts/`, `assets/`, `references/` show how to **structure** a metering stack — a reference, NOT content to copy verbatim.

- **Get every seller-specific value from the seller** (product code, region(s), stage name, VMT tags, alarm actions/thresholds, retention/KMS) and fill the `TODO:` markers. If a required value is unknown, **ask** — do not assume. VMT is presence-based: the seller writes each usage row's `usageAllocations` (their real tags); the pipeline (aggregator) merges them and never invents tags (no `VmtEnabled`/`VmtTagAttribute` parameter).
- **Do not change the architecture or `awsmp-*` naming** (two-stack layout, table/GSI/event wiring are fixed). Sellers customize only the marked `TODO:` sections. A request that would break the architecture/naming is explained as a constraint, not silently accommodated.

## Guardrail — where this skill's own files live (MCP vs local)

Before reading a reference or running a script, determine how the skill was loaded:

- **AWS MCP `retrieve_skill`:** the skill is NOT on the local filesystem — fetch each reference/script via `retrieve_skill` with the `file` parameter (e.g. `file="references/deployment-steps.md"`); do NOT `file_read` these paths.
- **Installed locally:** read/run files from the local skill dir via relative paths.

This applies only to the skill's own files; user data/session artifacts are always in the user's working directory.

## Critical Facts (apply to ALL metering operations)

- **CustomerAWSAccountId** (not CustomerIdentifier) + **LicenseArn** per UsageRecord for new (Concurrent Agreements) products.
- **EventBridge** (not SNS) for subscription events — emitted only in us-east-1.
- **`ResolveCustomer`** resolves the buyer's `LicenseArn`, `customerAWSAccountId`, and `productCode` from the `x-amzn-marketplace-token` when the buyer is redirected from the Marketplace console to the SaaS registration site; the SaaS then collects additional registration data and persists it (the PII fields) in the **in-region customer-profile table** (the PII-free subscribers table gets only the CA identity + statuses + `registeredRegions`). It returns a **top-level `LicenseArn`** field — `CustomerIdentifier` is a **deprecated legacy field, null for new SaaS integrations** (do NOT use it and do NOT treat its value as the LicenseArn). It is a **regional** API, but the region is the **seller's free choice** — it does NOT have to match where the buyer uses the software, and it does NOT gate metering. `BatchMeterUsage` can be called in ANY supported region independently of where `ResolveCustomer` was called. It is NOT a us-east-1-only API (only the EventBridge subscription events like `License Updated` are us-east-1-only). It MUST be called from the account that PUBLISHED the SaaS product (else the token will not resolve). **The `x-amzn-marketplace-token` is NOT one-time-use — it is REUSABLE until it expires (~4 hours)**: re-resolving the same token before expiry returns the same result, so a buyer who errored on the registration page can retry with their original link while it is still within the ~4h window. After expiry `ResolveCustomer` returns `ExpiredTokenException` (an already-consumed/invalid token returns `InvalidTokenException`) and the buyer re-clicks "Set up your account" for a FRESH token (no resubscribe). A retry days later = expired token → mint a new one. (`references/troubleshooting.md`.)
- **DuplicateRecord** = first-write-wins and REPORTED (`Status = DuplicateRecord`, not `Success`); the original quantity stays billed. A resubmit with a *different* quantity is NOT billed (under-billing risk). Safe only for an *identical* retry — never a way to correct a quantity; never call it "benign".
- **CustomerNotSubscribed** = real failure, do NOT retry. Causes: (1) invalid identifier, (2) agreement ended/expired, (3) buyer account suspended.
- **A bad `LicenseArn` is a REQUEST-level `InvalidLicenseException` that fails the ENTIRE `BatchMeterUsage` batch** — NOT a per-record `Status` in `Results` (unlike `CustomerNotSubscribed`). Mitigation: shape-validate the LicenseArn client-side before batching (reason `InvalidLicenseArn`) so a malformed ARN never enters a batch, and on a request-level `ClientError` bisect-and-retry to isolate the poison record so its co-batched records still meter.
- **`BatchMeterUsage` (SaaS) is REGIONAL, but the SELLER CHOOSES the region — there is NO `InvalidEndpointRegionException`.** Call it in the region where usage occurs (the seller's choice; nearest supported region for on-prem/other-cloud); deploy a stack per metering region; dedup is per region. It is supported in MOST commercial regions but not all — where the API is unavailable in a chosen region the call returns **`DisabledApiException`** (meter in a supported region instead). There is **no** `InvalidEndpointRegionException` for SaaS `BatchMeterUsage`; the seller picks the region and per-region dedup makes multi-region metering correct, not an error.
  - **Do NOT conflate this with AMI/Container `MeterUsage`.** Metered **AMI/Container** products are OUT OF SCOPE for this skill (redirect to public docs), and their region rule is the OPPOSITE: **`MeterUsage` MUST be called in the SAME region as the running EC2 instance / ECS task — you CANNOT call it from us-east-1 (or any other region) regardless of where the instance runs; a mismatched region returns `InvalidEndpointRegionException`.** Such products authenticate with an **IAM instance/task role** (EC2 instance profile, ECS task role, or EKS IRSA), never embedded/long-lived access keys. This same-region enforcement + `InvalidEndpointRegionException` applies ONLY to AMI/Container `MeterUsage`, NOT to SaaS `BatchMeterUsage`. See the [public AWS docs](https://docs.aws.amazon.com/marketplace/latest/userguide/) for AMI/Container metering.
- **Dimension validity is server-authoritative.** The pipeline is NOT configured with a dimension list and does NOT reject client-side; an undefined dimension fails the `BatchMeterUsage` call with a REQUEST-level `InvalidUsageDimensionException` (case-sensitive, not billed) — it is NOT a per-record `Results[].Status`. The pipeline isolates the offending record by bisection (depth-bounded to ⌈log2(batch size)⌉ = 5 so a many-failure batch can't overload the submitter), marks the aggregated record `Error` (a SERVER-SIDE submit failure, NOT `RejectedClientSide` — that status NEVER appears on the `awsmp-aggregated-usage` table; the exception name is kept in `meteringStatusReason`), and emits the `BatchMeterUsageException` metric. **Remediation: define/fix the dimension in the AMMP catalog (case-sensitive) — do NOT change the usage-row writer; the row was well-formed, AWS Marketplace rejected it.** Adding/renaming a catalog dimension needs no redeploy.
- **Rejection reason codes come in a client-side ↔ server-side PAIR — always surface BOTH.** A row can be rejected two ways and the rubric expects the named code for each: (1) the pipeline's **client-side guardrail** rejects the row BEFORE the API call and stamps a client-side reason code (`MalformedTimestamp`, `InvalidLicenseArn`, `TooManyTags`, etc.), marking it `RejectedClientSide` and emitting `UsageRecordRejected`; (2) if a row reaches the API and is rejected there, the **server-side** equivalent is the matching `BatchMeterUsage` request-level exception (`TimestampOutOfBoundsException`, `InvalidLicenseException`, `InvalidTagException`, `InvalidUsageDimensionException`, `InvalidProductCodeException`, `InvalidUsageAllocationsException`, and the legacy/validation cases `InvalidCustomerIdentifierException` (legacy pre-CA only) and `ValidationException` (e.g. a record that mixes `CustomerIdentifier` with `LicenseArn` — CA does not support `CustomerIdentifier`-based metering)). A SEPARATE server-side exception is **`DisabledApiException`** — a WHOLE-REGION availability failure (`BatchMeterUsage` not offered in the chosen region), NOT a per-record validation reject: it is not isolated/bisected or stamped `Error`; the fix is to meter in a supported region (see the regional-availability fact above). When explaining any rejection, name BOTH the client-side reason code AND its server-side exception equivalent, not just one. Client-side reasons ↔ server-side exceptions: `MalformedTimestamp` ↔ `TimestampOutOfBoundsException`; `InvalidLicenseArn` ↔ `InvalidLicenseException`; `TooManyTags` / `TooManyAllocations` / `AllocationSumMismatch` / `MixedAllocatedAndUnallocated` ↔ `InvalidTagException` / `InvalidUsageAllocationsException` (the allocation/tag server-side exceptions). (Note: the server-side exception for a too-many-tags row is `InvalidTagException`; `TooManyTags` is only the client-side reason code.) **Dimension validity has NO client-side reason code** — the pipeline does NOT validate dimension names client-side (it is not configured with the catalog's dimension list); an undefined dimension is caught ONLY server-side as the request-level `InvalidUsageDimensionException`.
- **`UnprocessedRecords` = a PARTIAL-BATCH outcome, NOT "the call accepted the record."** The `BatchMeterUsage` request did NOT fail completely: records that succeeded appear in `Results`, records that failed to process appear in `UnprocessedRecords` (**not billed; typically TRANSIENT** — `ThrottlingException` / `InternalServiceException`). They are **safe to re-submit** because metering is idempotent (first-write-wins dedup on `CustomerAWSAccountId + LicenseArn + dimension + hour`, per region). The deployed submitter **RETRIES the record ONCE**; if it still fails it is **left for reprocessing and picked up on the NEXT SCHEDULED run** (`rate(5m)`) — NOT a blind client-side retry loop; the persistent case is alarmed via `UsageRecordUnprocessed`. This is DISTINCT from: **per-record `Results[].Status` verdicts** (`Success` / `CustomerNotSubscribed` / `DuplicateRecord` — FINAL, do NOT blindly retry), and **request-level exceptions** (`InvalidUsageDimensionException` / `TimestampOutOfBoundsException` / `InvalidLicenseException` / `InvalidTagException` / `InvalidProductCodeException` / `InvalidUsageAllocationsException`) that fail the ENTIRE batch and require isolating the poison record by bisection.
- **CloudTrail is regional** — query the same region BatchMeterUsage is called in. **~15 min** delivery latency. **90-day** retention (recommend Seller Reports for older). Use CloudTrail **Event History** (default, no setup) filtered on `eventSource = metering-marketplace.amazonaws.com` — **never CloudTrail Lake / `create-event-data-store`** for metering lookups.
- **Timestamp window = 24 HOURS** (not 1h), measured from `now`; a record whose timestamp is older than 24h is **outside the window and REJECTED — it is NOT submitted.** Concretely, a **30-hour-old** record is past the 24h boundary and does **NOT** fall inside the window: server-side it fails the call with `TimestampOutOfBoundsException` and one bad timestamp rejects the ENTIRE batch; before it ever reaches the API the pipeline's client-side guardrail rejects the row with reason code **`MalformedTimestamp`** (marked `RejectedClientSide`, not billed) so a stale/out-of-window timestamp never enters a batch. Do NOT tell a seller a >24h (e.g. 30h) record is "within the window" or "should be submitted" — it is rejected; the only exception is the 6-hour month-boundary grace, which accepts *previous-month* records until 06:00 UTC on the 1st. The pipeline meters only COMPLETED hours in `now-23h … now-1h` (never the in-progress/current hour — that is only metered by a deprovisioning flush), **oldest-first**, gated by `MeteringLockHours`; a skipped/failed hourly-discoverer or `rate(5m)`-submitter run self-recovers on a later run while the hour is still in the 24h window. Detail: `references/scalable-metering-design.md`.
  - **Best practice — submit usage within ~1 hour of generation.** Late submission is a TOP real-world failure mode: the closer a row gets to the 24h boundary the less retry headroom it has, and past 24h it is permanently lost (`TimestampOutOfBounds`, not billed). Write/submit usage promptly (well inside the window) rather than batching a day's worth at the edge.
  - **Age-out is TERMINAL and clears the pending marker.** When a row/aggregation cannot be metered before it exceeds the 24h window, the pipeline ages it out: it sets a **terminal `meteringStatus` (`AggregationExpired` for a raw row that never aggregated, `SubmissionExpired` for an aggregated row never submitted)** AND **CLEARS `meteringPending`** so the row drops off the sparse `metering_pending` GSI and the discoverer's oldest-first backward walk STOPS returning it (no infinite re-processing). These fire the `UsageAggregationExpired` / `UsageSubmissionExpired` alarms — a paged signal that usage silently never metered; investigate the writer/pipeline delay.
  - **A ~1-hour-old timestamp should NOT normally get `TimestampOutOfBoundsException`** — 1h is well inside the 24h lookback, so this is UNUSUAL. Do NOT tell the seller to "just wait until HH:05". The real cause is almost always (a) **one bad record failing the WHOLE batch** (a co-batched malformed/out-of-window/undefined-dimension record makes the request-level exception reject every record in the call until it is isolated) or (b) **clock skew** (an unsynchronized host stamping timestamps in the future / far past, or local time instead of UTC). Fix: **inspect the batch for the malformed record** (bisect to isolate it) and verify the writer's clock is NTP-synced and stamping whole-second UTC.
- **Current (in-progress) hour is NEVER metered on the normal path.** The pipeline only submits COMPLETED hours (`now-23h … now-1h`); the hour currently in progress stays open and is skipped until it completes. `MeteringLockHours` (default **1**, validated 1..20) is how many hours an hour stays open for late usage before it is aggregated/submitted — with the default of 1, the pipeline processes the just-completed hour. Pending hour-buckets are drained **OLDEST-FIRST** so the oldest usage is submitted before it approaches the 24h boundary and ages out (`TimestampOutOfBounds`). The aggregator collapses each `(licenseArn, customerAWSAccountId, dimension, hour)` group to **exactly ONE `BatchMeterUsage` record per hour** (metering is per-group-per-hour, first-write-wins). The ONLY time the current in-progress hour is metered is the deprovisioning final-usage flush (lock-bypassed, see the event model). So "why isn't this hour showing usage yet?" = it is still the current hour and/or still inside `MeteringLockHours` — expected, not a bug.
- **`BatchMeterUsage` limits are TWO independent caps: ≤25 UsageRecords AND ≤1 MB request payload.** A batch with 25 VALID records can still be rejected for SIZE if the serialized request exceeds 1 MB (large/many `usageAllocations` tag-sets, long ARNs) — it is the byte size, not the record count. Fix: put fewer records per call so each request stays under 1 MB. (The submitter already coalesces ≤25 per call; a size rejection means those records are too large — split further.)
- **VMT (vendor metered tagging) tag limit = 5 `usageAllocations` tags per UsageRecord.** A usage row may carry at most **5 tags** in its `usageAllocations`; more than 5 is invalid. The pipeline's client-side guardrail rejects the over-tagged row BEFORE the API call with reason code **`TooManyTags`** (marked `RejectedClientSide`, not billed, emits `UsageRecordRejected`); the server-side equivalent if such a row reached the API is **`InvalidTagException`** (the request-level exception name is `InvalidTagException`; `TooManyTags` is only the client-side reason code). When asked about the VMT tag cap, name BOTH the 5-tag maximum AND the `TooManyTags` client-side rejection (not just the number). VMT is presence-based — the seller supplies the tags; the pipeline never invents them.
- **VMT is ALL-OR-NOTHING per group — do NOT mix tagged and untagged rows.** Within one `(licenseArn, customerAWSAccountId, dimension, hour)` group, either EVERY non-zero row carries `usageAllocations` or NONE does. Mixing allocated and unallocated non-zero rows in the same group is **NOT allowed** — the pipeline rejects it client-side with reason code **`MixedAllocatedAndUnallocated`** (`RejectedClientSide`, not billed). It is NOT "partial VMT attribution." Zero-quantity rows are ignored for this check, and DIFFERENT groups in the same hour/batch may independently have or omit allocations. Also per group: each row's `AllocatedUsageQuantity` values must sum to that row's `quantity`, and ≤2500 merged tag-sets per UsageRecord (else `AllocationSumMismatch` / `TooManyAllocations`). Detail: `references/setup-path.md`, `references/client-side-guardrails.md`.
- **Metering is idempotent (first-write-wins dedup per `CustomerAWSAccountId + LicenseArn + dimension + hour`, per region)**, and the pipeline is DECOUPLED — there is no single meter Lambda and **no "reserved concurrency = 2"** knob. The **submitter** (the ONLY stage that calls `BatchMeterUsage`) runs **`ReservedConcurrentExecutions=1` — a single SERIAL submitter**, deliberately serial to stay within the `BatchMeterUsage` request-rate limit (**documented quota: 10 requests/second per account per region**; `MeterUsage` 10/s, `RegisterUsage` 5/s — see [service quotas](https://docs.aws.amazon.com/marketplace/latest/developerguide/metering-service-quotas.html)) and avoid `ThrottlingException`. The **discoverer** runs at reserved concurrency **30**; the **aggregator** and **cleanup** scale via their **SQS event-source-mapping `MaximumConcurrency`**. **SQS absorbs bursts** — throughput comes from **draining the queues**, not from raising submitter concurrency. If a seller needs higher sustained metering TPS, **contact AWS Marketplace Seller Operations** rather than raising concurrency past a safe level.
  - **`ThrottlingException` handling — exponential backoff WITH JITTER.** On `ThrottlingException` (or a throttled `UnprocessedRecords`), retry with **exponential backoff plus JITTER** (randomized delay — name jitter explicitly; do NOT use a fixed/synchronized retry interval, which re-collides). Keep each call within the **≤25-record (and ≤1 MB)** limit — do not grow the batch to "catch up." Retries are SAFE because metering is **idempotent (first-write-wins)**: a re-sent already-billed record simply comes back as **`DuplicateRecord`** with the original quantity still billed. Do NOT treat `DuplicateRecord` as a reason to avoid retrying a throttled/unprocessed record — it is the idempotency guarantee that MAKES retry safe (the only caveat is never resubmit the same group with a DIFFERENT quantity, which would not be billed).
- **Registration form data (PII) lives in a per-Region `customer-profile` table, NOT the subscribers table.** The in-region register Lambda writes the allowlisted registration/KYB fields to `awsmp-<productCode>-customer-profile` (PK `licenseArn`, SK `customerAWSAccountId`, seller-requested GSIs) IN the Region it runs in, keeping buyer PII in-region. The profile row is **ALWAYS upserted for a registered buyer — even with ZERO extra form fields** (the row keyed by identity is always written). The unified subscribers table (us-east-1) is **PII-FREE** — the register Lambda writes only the non-PII `productCode` + an idempotent `registeredRegions` append (NO registration form data) — plus the CA identity (`licenseArn`, `customerAWSAccountId`, `agreementId`) and the two lifecycle statuses — so it is safe in us-east-1 even for opt-in-Region products. The template ALWAYS sets the register Lambda's **`CUSTOMER_PROFILE_TABLE`** environment variable; if that env var is **unset or empty** that is a MISCONFIGURATION and the handler **FAILS LOUD — it returns HTTP 500** (logs `CUSTOMER_PROFILE_TABLE not configured` and responds `500 "Registration store not configured"`) rather than silently skipping the buyer's in-region PII write. (An existing seller's own registration handler may store data differently — the skill adapts and does not force this reference Lambda verbatim.)
- **Legacy (pre-CA) integrations** may use `ProductCode` + `CustomerIdentifier` instead of `LicenseArn`. The skill does NOT impose its own template/fallback on them (it must NOT present a fixed `startswith("arn:")` fallback as its template) — it grounds legacy behavior in the seller's EXISTING integration + public AWS docs (`references/existing-sellers.md`).
- **Free trials — behavior depends on WHICH model (do not give a blanket "always meter" answer); confirm the model first and ground specifics in the public docs ([SaaS free trials](https://docs.aws.amazon.com/marketplace/latest/userguide/saas-free-trials.html), [PLG guide](https://aws.amazon.com/blogs/awsmarketplace/a-guide-to-executing-a-saas-product-led-growth-strategy-in-aws-marketplace/)):**
  - **AWS Marketplace SaaS free trial feature** (native, time-bound: the buyer subscribes to a separate trial offer and must upgrade before it ends): **meter normally throughout** — do NOT withhold. AWS applies the trial pricing so trial usage invoices at $0 (`$0` estimated revenue), and after conversion the same dimensions bill with **no integration change**.
  - **Embedded free tier / usage-based trial** (buyer on the STANDARD paid offer, seller decides when charges begin): WHEN/WHAT to meter is the seller's own pricing decision — the skill does NOT prescribe a metering strategy. For plain usage-based (PAYG) the default is **meter all billable usage** and let pricing/offers handle any free allowance. Billing only BEYOND a contract-included quantity is **Contract with Consumption (CCP)** — OUT OF SCOPE (see below) → [public docs](https://docs.aws.amazon.com/marketplace/latest/userguide/saas-integrate-contract.html).
- **Usage ingestion is the seller's responsibility.** The seller writes rows into the usage table (incl. cross-account collection if multi-account). In the default **full** mode the PIPELINE aggregates hourly; in **direct-submit** mode the seller supplies finalized hourly totals (see Q9).
- **All timestamps are UTC** — the seller stamps the sort-key timestamp AND `meteringPending` in UTC; the GSI is queried by UTC hour bucket, so a local-time write mis-files/mis-meters the row silently.
- **Audit timestamps (`createdAt`/`updatedAt`)** — every row the pipeline writes carries `createdAt` (write-once, `if_not_exists`) and `updatedAt` (refreshed each write), ISO-8601 whole-second UTC, applied uniformly via a shared stamp helper. These are **audit-only metadata and do NOT gate metering** — metering decisions follow **`subscriptionStatus` / the `metering_pending` GSI**, never `createdAt`/`updatedAt`.

## Concurrent Agreements (CA) — registration & metering model (recite when asked how CA works)

All new SaaS products use Concurrent Agreements: identity is **`CustomerAWSAccountId` + `LicenseArn`** (never the legacy `CustomerIdentifier`). Explain these facts explicitly whenever a CA registration, event-ordering, attribution, or lookup question comes up:

- **Registration → subscriber upsert is driven by the EventBridge `License Updated` event.** When a buyer subscribes, AWS Marketplace emits **`License Updated`** (us-east-1 only). The events-stack subscription Lambda consumes it and **UPSERTS the subscriber record** (keyed by `licenseArn` + `customerAWSAccountId`, with `agreementId`) setting **`agreementStatus=active`** and `subscriptionStatus=active` — this is what makes the buyer eligible to meter. `ResolveCustomer` (called from the branded registration page) resolves the token to `LicenseArn` + `customerAWSAccountId` + `productCode` and persists registration/PII to the in-region customer-profile table, but it is the **`License Updated` upsert (agreementStatus=active)** that establishes the metering-eligible subscriber row. Do not describe CA registration without naming the `License Updated` → `agreementStatus=active` upsert.
- **`ResolveCustomer` DOES return the `LicenseArn` — the answer is YES.** `ResolveCustomer` resolves the `x-amzn-marketplace-token` and returns a **top-level `LicenseArn`** field, alongside `customerAWSAccountId` and `productCode`. Do NOT say `ResolveCustomer` does not return a LicenseArn — it does, at the top level of the response. `CustomerIdentifier` is a deprecated legacy field that is **null for new CA integrations** — do NOT use it or treat its value as the LicenseArn. (For a new SaaS product the `LicenseArn` from `ResolveCustomer` is the identifier you persist and later pass to `BatchMeterUsage`.)
- **Eligibility is enforced at `BatchMeterUsage`, independent of event arrival order.** Metering eligibility is checked server-side by `BatchMeterUsage` at submit time against the current agreement/subscription state — it does NOT depend on the order in which EventBridge events arrived. If usage is submitted before the subscriber is active (or after the agreement ended) the record returns `CustomerNotSubscribed`; once `License Updated` has made the buyer active, the same record meters. So out-of-order or late-arriving events do not corrupt billing: `BatchMeterUsage` is the authority, and the row self-recovers on a later `rate(5m)` run while still inside the 24h window. Do not claim the pipeline gates metering on event ordering — it gates on eligibility at submit time.
- **Metering is attributed PER LICENSE, not per account** — reinforce the usage-table key model. The raw usage table is **PK `licenseArn`, SK `customerAWSAccountId#dimension#timestamp`**. One AWS account (`customerAWSAccountId`) can hold MULTIPLE agreements/licenses, and dedup + billing are scoped to `CustomerAWSAccountId + LicenseArn + dimension + hour` — so usage is aggregated and metered **per `licenseArn`**, never collapsed to a single per-account total. When asked "meter per account or per license?" the answer is **per license** (the `licenseArn` is the partition key).
- **The `metering_pending` GSI drives metering; the subscribers table is lifecycle-only.** Discovery of what to meter comes ONLY from the sparse **`metering_pending` GSI** on the usage/aggregated tables (HASH = `meteringPending` hour-bucket, RANGE = `licenseArn`, projecting `customerAWSAccountId` + `dimension`). The discoverer **PAGINATES** through the GSI — it runs a paginated `Query` (following `LastEvaluatedKey` across pages), NOT a single read — so an hour with more pending groups than one page still fully drains. The **subscribers table is NOT consulted to find usage to meter** — it holds only CA identity + the two lifecycle statuses (`agreementStatus` / `subscriptionStatus`) and is used for **lifecycle/deprovisioning** (is this license active? has it entered the deprovisioning flush?). Do not say the pipeline scans or queries the subscribers table to locate pending usage — that is the GSI's job.
  - **Unified subscribers table schema (us-east-1, PII-FREE):** **PK `licenseArn`, SK `customerAWSAc...

© aws · Apache-2.0 · aws/agent-toolkit-for-aws

Similar skills

Everything for Cloud: AWS, Azure, GCP →

From the same set133

aws-well-architected-review Runs an AWS Well-Architected Framework review: reads your code and IaC, checks it against live AWS docs, and returns a prioritized report. CX Codex +1 1,400installs amazon-neptune Guides setup and operations for Amazon Neptune Database and Neptune Analytics across GraphRAG, fraud detection, and graph queries. Requires explicit confirmation before any resource changes. CX Codex +1 1,400installs aws-fault-injection-service Designs, runs, and analyzes chaos experiments in AWS Fault Injection Service. It helps test application resilience against AZ outages, network packet loss, and compute failures. CX Codex +1 1,000installs amazon-eventbridge-event-bus A skill for building and debugging apps on the new EventBridge custom event buses. Useful when events fan out to subscribers and some never arrive. CX Codex +1 1,000installs testing-mwaa-workflow Runs end-to-end execution tests for Amazon MWAA workflows and tracks them to completion. Useful for verifying Python DAGs and Serverless workflows safely. CX Codex +1 861installs agents-pay Sets up an agent to pay for x402-protected content during a task, with policy controls and session spending limits. CX Codex +1 809installs aws-lambda-web-functions Walks you through building and shipping a Node.js HTTP app on AWS Lambda Web Functions: Express, Fastify or Hono, local run, packaging, deploy via CLI. CX Codex +1 507installs tf-best-practices Sets the security posture for generated AWS Terraform, then checks the written terraform/ directory against policy. Read-only, no file edits. CX Codex +1 323installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor