aws-marketplace-metering — скилл для Claude Code: метринг AWS
Скилл разворачивает и отлаживает usage-based метринг AWS Marketplace: SAM-пайплайн, ResolveCustomer, BatchMeterUsage, разбор записей и ошибок.
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a claude-codeЧто делает aws-marketplace-metering
Скилл ведёт весь цикл работ с SaaS PAYG-метрингом. На старте он собирает serverless-интеграцию через SAM: ResolveCustomer, BatchMeterUsage и EventBridge. Дальше помогает искать и разбирать уже отправленные записи — по CloudTrail Event History и Seller Reports.
Внутри лежат готовые scripts/, assets/ и references/, но это образец структуры, а не код для копирования. Все значения продавца — product code, регионы, имя стейджа, теги VMT, пороги алармов — нужно подставить самому в места с TODO:. Архитектуру и имена awsmp-* менять нельзя.
Важное ограничение: скилл генерирует CloudFormation/SAM-шаблоны, Lambda-обработчики, IAM-политики и deploy.sh, и всё это идёт в ваш аккаунт. Ошибка в метринге бьёт по выручке — недосчитаете или пересчитаете. Поэтому перед боевым трафиком прогоните сгенерированное через обычное ревью и полный тест-план из references/test-plan.md. Ещё: CustomerIdentifier — устаревшее поле, для новых интеграций оно null, вместо него нужны CustomerAWSAccountId и LicenseArn.
Какие задачи решает
- развернуть SAM-пайплайн метринга для SaaS PAYG
- разобрать записи BatchMeterUsage и их статусы
- найти причину DuplicateRecord или CustomerNotSubscribed
- посчитать billed-историю по Seller Reports
- проверить, в каком регионе вызывать BatchMeterUsage
Примеры запросов агенту
- Разверни метринг AWS Marketplace для моего SaaS-продукта через SAM
- Почему мои записи BatchMeterUsage возвращают DuplicateRecord?
- Покажи billed usage из Seller Reports за прошлый месяц
Как установить aws-marketplace-metering
1Через Skills CLI (Vercel)
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a claude-code2Вручную: положить файлы
~/.claude/skills/aws-marketplace-metering/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/aws-marketplace-metering/
1Через Skills CLI (Vercel)
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a codex2Вручную: положить файлы
.agents/skills/aws-marketplace-metering/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a cursor2Вручную: положить файлы
.cursor/skills/aws-marketplace-metering/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install aws/agent-toolkit-for-aws aws-marketplace-metering2Вручную: положить файлы
.github/skills/aws-marketplace-metering/SKILL.md1Через Skills CLI (Vercel)
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a gemini-cli2Командой платформы
gemini skills install https://github.com/aws/agent-toolkit-for-aws --path skills/specialized-skills/aws-marketplace-skills/aws-marketplace-meteringВручную: положить файлы
.devin/skills/aws-marketplace-metering/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о aws-marketplace-metering
Как установить aws-marketplace-metering в Claude?
Выполните npx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering -a claude-code. После установки файл скилла лежит в ~/.claude/skills/aws-marketplace-metering/SKILL.md.
Что делает aws-marketplace-metering?
Разворачивает serverless-интеграцию метринга для SaaS PAYG, а потом помогает искать и отлаживать отправленные записи: статусы, ошибки, расхождения.
Что нужно проверить перед боевым метрингом?
Скилл генерирует шаблоны, Lambda-код и IAM-политики, которые попадают в ваш аккаунт. Прогоните их через ревью и полный тест-план из references/test-plan.md, прежде чем пускать реальный трафик.
Исходный текст
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