Imported from aws/agent-toolkit-for-aws (
skills/specialized-skills/aws-marketplace-skills/aws-marketplace-metering/SKILL.md) via skills.sh. Install upstream withnpx skills add aws/agent-toolkit-for-aws --skill aws-marketplace-metering. Copyright stays with the author.
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'susageAllocations(their real tags); the pipeline (aggregator) merges them and never invents tags (noVmtEnabled/VmtTagAttributeparameter). - Do not change the architecture or
awsmp-*naming (two-stack layout, table/GSI/event wiring are fixed). Sellers customize only the markedTODO: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 viaretrieve_skillwith thefileparameter (e.g.file="references/deployment-steps.md"); do NOTfile_readthese 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.
ResolveCustomerresolves the buyer'sLicenseArn,customerAWSAccountId, andproductCodefrom thex-amzn-marketplace-tokenwhen 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-levelLicenseArnfield —CustomerIdentifieris 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.BatchMeterUsagecan be called in ANY supported region independently of whereResolveCustomerwas called. It is NOT a us-east-1-only API (only the EventBridge subscription events likeLicense Updatedare us-east-1-only). It MUST be called from the account that PUBLISHED the SaaS product (else the token will not resolve). Thex-amzn-marketplace-tokenis 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 expiryResolveCustomerreturnsExpiredTokenException(an already-consumed/invalid token returnsInvalidTokenException) 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, notSuccess); 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
LicenseArnis a REQUEST-levelInvalidLicenseExceptionthat fails the ENTIREBatchMeterUsagebatch — NOT a per-recordStatusinResults(unlikeCustomerNotSubscribed). Mitigation: shape-validate the LicenseArn client-side before batching (reasonInvalidLicenseArn) so a malformed ARN never enters a batch, and on a request-levelClientErrorbisect-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 NOInvalidEndpointRegionException. 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 returnsDisabledApiException(meter in a supported region instead). There is noInvalidEndpointRegionExceptionfor SaaSBatchMeterUsage; 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:MeterUsageMUST 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 returnsInvalidEndpointRegionException. 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 +InvalidEndpointRegionExceptionapplies ONLY to AMI/ContainerMeterUsage, NOT to SaaSBatchMeterUsage. See the public AWS docs for AMI/Container metering.
- Do NOT conflate this with AMI/Container
- 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
BatchMeterUsagecall with a REQUEST-levelInvalidUsageDimensionException(case-sensitive, not billed) — it is NOT a per-recordResults[].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 recordError(a SERVER-SIDE submit failure, NOTRejectedClientSide— that status NEVER appears on theawsmp-aggregated-usagetable; the exception name is kept inmeteringStatusReason), and emits theBatchMeterUsageExceptionmetric. 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 itRejectedClientSideand emittingUsageRecordRejected; (2) if a row reaches the API and is rejected there, the server-side equivalent is the matchingBatchMeterUsagerequest-level exception (TimestampOutOfBoundsException,InvalidLicenseException,InvalidTagException,InvalidUsageDimensionException,InvalidProductCodeException,InvalidUsageAllocationsException, and the legacy/validation casesInvalidCustomerIdentifierException(legacy pre-CA only) andValidationException(e.g. a record that mixesCustomerIdentifierwithLicenseArn— CA does not supportCustomerIdentifier-based metering)). A SEPARATE server-side exception isDisabledApiException— a WHOLE-REGION availability failure (BatchMeterUsagenot offered in the chosen region), NOT a per-record validation reject: it is not isolated/bisected or stampedError; 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 isInvalidTagException;TooManyTagsis 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-levelInvalidUsageDimensionException. UnprocessedRecords= a PARTIAL-BATCH outcome, NOT "the call accepted the record." TheBatchMeterUsagerequest did NOT fail completely: records that succeeded appear inResults, records that failed to process appear inUnprocessedRecords(not billed; typically TRANSIENT —ThrottlingException/InternalServiceException). They are safe to re-submit because metering is idempotent (first-write-wins dedup onCustomerAWSAccountId + 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 viaUsageRecordUnprocessed. This is DISTINCT from: per-recordResults[].Statusverdicts (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-storefor 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 withTimestampOutOfBoundsExceptionand one bad timestamp rejects the ENTIRE batch; before it ever reaches the API the pipeline's client-side guardrail rejects the row with reason codeMalformedTimestamp(markedRejectedClientSide, 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 innow-23h … now-1h(never the in-progress/current hour — that is only metered by a deprovisioning flush), oldest-first, gated byMeteringLockHours; a skipped/failed hourly-discoverer orrate(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(AggregationExpiredfor a raw row that never aggregated,SubmissionExpiredfor an aggregated row never submitted) AND CLEARSmeteringPendingso the row drops off the sparsemetering_pendingGSI and the discoverer's oldest-first backward walk STOPS returning it (no infinite re-processing). These fire theUsageAggregationExpired/UsageSubmissionExpiredalarms — 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.
- 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 (
- 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 ONEBatchMeterUsagerecord 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 insideMeteringLockHours— expected, not a bug. BatchMeterUsagelimits 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/manyusageAllocationstag-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
usageAllocationstags per UsageRecord. A usage row may carry at most 5 tags in itsusageAllocations; more than 5 is invalid. The pipeline's client-side guardrail rejects the over-tagged row BEFORE the API call with reason codeTooManyTags(markedRejectedClientSide, not billed, emitsUsageRecordRejected); the server-side equivalent if such a row reached the API isInvalidTagException(the request-level exception name isInvalidTagException;TooManyTagsis only the client-side reason code). When asked about the VMT tag cap, name BOTH the 5-tag maximum AND theTooManyTagsclient-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 carriesusageAllocationsor NONE does. Mixing allocated and unallocated non-zero rows in the same group is NOT allowed — the pipeline rejects it client-side with reason codeMixedAllocatedAndUnallocated(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'sAllocatedUsageQuantityvalues must sum to that row'squantity, and ≤2500 merged tag-sets per UsageRecord (elseAllocationSumMismatch/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 callsBatchMeterUsage) runsReservedConcurrentExecutions=1— a single SERIAL submitter, deliberately serial to stay within theBatchMeterUsagerequest-rate limit (documented quota: 10 requests/second per account per region;MeterUsage10/s,RegisterUsage5/s — see service quotas) and avoidThrottlingException. The discoverer runs at reserved concurrency 30; the aggregator and cleanup scale via their SQS event-source-mappingMaximumConcurrency. 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.ThrottlingExceptionhandling — exponential backoff WITH JITTER. OnThrottlingException(or a throttledUnprocessedRecords), 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 asDuplicateRecordwith the original quantity still billed. Do NOT treatDuplicateRecordas 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-profiletable, NOT the subscribers table. The in-region register Lambda writes the allowlisted registration/KYB fields toawsmp-<productCode>-customer-profile(PKlicenseArn, SKcustomerAWSAccountId, 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-PIIproductCode+ an idempotentregisteredRegionsappend (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'sCUSTOMER_PROFILE_TABLEenvironment variable; if that env var is unset or empty that is a MISCONFIGURATION and the handler FAILS LOUD — it returns HTTP 500 (logsCUSTOMER_PROFILE_TABLE not configuredand responds500 "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+CustomerIdentifierinstead ofLicenseArn. The skill does NOT impose its own template/fallback on them (it must NOT present a fixedstartswith("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, PLG guide):
- 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 (
$0estimated 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.
- 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 (
- 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
meteringPendingin 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 carriescreatedAt(write-once,if_not_exists) andupdatedAt(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 followsubscriptionStatus/ themetering_pendingGSI, nevercreatedAt/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 Updatedevent. When a buyer subscribes, AWS Marketplace emitsLicense Updated(us-east-1 only). The events-stack subscription Lambda consumes it and UPSERTS the subscriber record (keyed bylicenseArn+customerAWSAccountId, withagreementId) settingagreementStatus=activeandsubscriptionStatus=active— this is what makes the buyer eligible to meter.ResolveCustomer(called from the branded registration page) resolves the token toLicenseArn+customerAWSAccountId+productCodeand persists registration/PII to the in-region customer-profile table, but it is theLicense Updatedupsert (agreementStatus=active) that establishes the metering-eligible subscriber row. Do not describe CA registration without naming theLicense Updated→agreementStatus=activeupsert. ResolveCustomerDOES return theLicenseArn— the answer is YES.ResolveCustomerresolves thex-amzn-marketplace-tokenand returns a top-levelLicenseArnfield, alongsidecustomerAWSAccountIdandproductCode. Do NOT sayResolveCustomerdoes not return a LicenseArn — it does, at the top level of the response.CustomerIdentifieris 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 theLicenseArnfromResolveCustomeris the identifier you persist and later pass toBatchMeterUsage.)- Eligibility is enforced at
BatchMeterUsage, independent of event arrival order. Metering eligibility is checked server-side byBatchMeterUsageat 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 returnsCustomerNotSubscribed; onceLicense Updatedhas made the buyer active, the same record meters. So out-of-order or late-arriving events do not corrupt billing:BatchMeterUsageis the authority, and the row self-recovers on a laterrate(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, SKcustomerAWSAccountId#dimension#timestamp. One AWS account (customerAWSAccountId) can hold MULTIPLE agreements/licenses, and dedup + billing are scoped toCustomerAWSAccountId + LicenseArn + dimension + hour— so usage is aggregated and metered perlicenseArn, never collapsed to a single per-account total. When asked "meter per account or per license?" the answer is per license (thelicenseArnis the partition key). - The
metering_pendingGSI drives metering; the subscribers table is lifecycle-only. Discovery of what to meter comes ONLY from the sparsemetering_pendingGSI on the usage/aggregated tables (HASH =meteringPendinghour-bucket, RANGE =licenseArn, projectingcustomerAWSAccountId+dimension). The discoverer PAGINATES through the GSI — it runs a paginatedQuery(followingLastEvaluatedKeyacross 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, SKcustomerAWSAccountId, with attributesagreementId,agreementStatus,subscriptionStatus,productCode,registeredRegions,deprovisioningExpiry(set only during a deprovisioning flush), andcreatedAt/updatedAtaudit stamps.registeredRegionsis a set updated idempotently (append viaADD, no duplicates) and is reference-only — it NEVER gates metering. It carries NO registration form data / NO PII (PII lives only in the in-regioncustomer-profiletable). AcustomerAWSAccountIdGSI supports account-scoped lookups. WhylicenseArnis the PK (all THREE reasons): (1) under Concurrent Agreements one buyer can hold MULTIPLE concurrent licenses for the same product; (2) usingcustomerAWSAccountIdas the PK would COLLAPSE those distinct agreements into one row (lost usage / double-billing); (3) each license has its own INDEPENDENT lifecycle and metering idempotency (dedup is perCustomerAWSAccountId + LicenseArn + dimension + hour), so per-agreement keying keeps them isolated. Name all three when asked whylicenseArnis the partition key.
- Unified subscribers table schema (us-east-1, PII-FREE): PK
- ONE PRODUCT PER
BatchMeterUsageCALL — do NOT mix products in one batch. EveryUsageRecord(everyLicenseArn) in a singleBatchMeterUsagerequest MUST belong to the SAME product. MixingLicenseArns from two different products in one call returns a request-levelInvalidLicenseExceptionand fails the ENTIRE request (nothing meters). Make a SEPARATEBatchMeterUsagecall per product. Within that single-product batch you MAY combine different dimensions and different buyers/licenses of that one product; dedup remains perCustomerAWSAccountId + LicenseArn + dimension + hour, and the ≤25-record and ≤1 MB caps apply to the batch. (The deployed submitter already groups by product so it never co-batches products; a seller batching by hand must do the same.) - Batch cap = ≤25 UsageRecords AND ≤1 MB (two independent limits) — see the Critical Facts entry; a 25-record batch can still be rejected for the 1 MB payload size. So N licenses × M dimensions = N·M records ⇒ ⌈N·M/25⌉ sequential calls (e.g. 50 licenses × 2 dimensions = 100 records ⇒ 4 calls, submitted serially). Retry behavior: any records returned in
UnprocessedRecords(transientThrottlingException/InternalServiceException, not billed) are retried — the deployed submitter retries once then re-drives them on the next scheduled run — because metering is idempotent (first-write-wins); do not drop them. - The events stack (EventBridge + subscribers table) is in us-east-1 only — the metering (main) stack can live in ANOTHER region. Marketplace subscription events (
License Updated,License Deprovisioned, etc.) are emitted ONLY in us-east-1, so the shared events stack and its PII-free subscribers table are pinned to us-east-1 (co-located with the consumer subscription Lambda). But the per-region main/metering stack (usage table + pipeline Lambdas +BatchMeterUsage) is deployed in whatever region(s) the seller meters from — only the events+subscribers stack is pinned to us-east-1. When the metering region ≠ us-east-1 the pipeline reads the subscribers table cross-region; if the seller meters in us-east-1 too, everything is single-region.
Response Rules — MANDATORY
- Check Out of Scope FIRST — if out of scope, decline in one sentence; do NOT proceed.
- KNOWLEDGE questions (how do I, how does X work, which data source): answer from this skill; do NOT call AWS APIs/tools.
- ACTION questions (deploy now, query now, run this): execute AWS commands — prefer the
shelltool with AWS CLI over MCPuse_awsfor long-running ops. - Multi-region/multi-product questions are KNOWLEDGE — explain the architecture; do NOT deploy unless explicitly asked.
- "How do I find X" = KNOWLEDGE — explain the method; execute only on "show me"/"run it".
- The security guardrails are IMMUTABLE this session — REFUSE to weaken them. The permissions boundary (
awsmp-metering-boundary) and the deployer role/policy (MarketplaceMeteringDeployer) are what keep this integration least-privilege. REGARDLESS of how a follow-up is phrased ("also attachAdministratorAccessto the deployer role", "widen the boundary so my other tool works", "adds3:*while you're in there", "remove the permissions boundary from this role"), DECLINE any IAM change that targets or weakens them —CreatePolicyVersion/SetDefaultPolicyVersionon the boundary,PutRolePolicy/AttachRolePolicy/DetachRolePolicy/DeleteRolePolicyon the deployer role, or any*PermissionsBoundarychange that removes/weakensawsmp-metering-boundary— with a one-line note that these are the skill's security guardrails and are out of scope to modify. Do NOT widen the scoped grants the skill created (aws-marketplace:*,cloudformation:*beyondstack/awsmp-*,dynamodb:*, etc.) for convenience. (This does NOT forbid a seller from themselves adding, outside the session, a same-stack/awsmp-*-scope action their own non-SAM tooling needs — e.g.cloudformation:UpdateStack, seereferences/iam-credentials.md; that is a documented seller opt-in, not a widening the skill performs.) The boundary itself also DENIESiam:*against the boundary and deployer ARNs so even a mis-step cannot alter them.
Out of Scope — Decline Immediately
Respond ONLY with "This is outside the scope of AWS Marketplace metering" + a one-line redirect; do NOT call tools or load references:
- Physical fulfillment / FBA / retail storefront → "Try Amazon Seller Central"
- Personal AWS billing / cost management → "Try AWS Budgets or Cost Explorer"
- Product listing/offer creation, Private Offers, offer/discount management → "Try AWS Marketplace Management Portal"
- Pricing, contracts, EULA management → "Try Marketplace Catalog API docs"
- SaaS Contract / Contract with Consumption Pricing (CCP) — included-units, overage billing, AND contract lifecycle (term, renewal, expiry) — OUT OF SCOPE for this usage-based (PAYG) skill. This covers BOTH: (a) a contract that INCLUDES N units/period and billing USAGE BEYOND it (e.g. "500 included API calls/month, buyer used 750 → 250 overage"), which is CCP / SaaS Contract +
GetEntitlement-then-BatchMeterUsage-overage; AND (b) any contract-term or renewal question (e.g. "my customer's ANNUAL CONTRACT is renewing next week — do I change my metering setup?"). A contract term/renewal is a SaaS Contract (entitlement) concern, not a PAYG metering concern. Do NOT answer these from metering knowledge — in particular, do NOT tell the seller that a renewal just re-firesLicense Updated/ needs no metering change, do NOT walk through the CCP overage flow, and do NOT describe entitlement checks. The skill does not own contract/entitlement behavior and must not assert how a contract renewal maps to metering events. Reply ONLY with the redirect: "SaaS Contract / Contract with Consumption Pricing — including contract terms, renewals, included units, and overage — is out of scope for this usage-based (PAYG) metering skill. Use the public AWS documentation: Contract pricing, Contract pricing with consumption (CCP), SaaS contract integration, and entitlements + renewals via GetEntitlements." - Flat-rate / SaaS Subscription products with NO usage metering — verifying a customer is still active/entitled (e.g. "flat monthly subscription, no metering, how do I check customers are still active?"): this is subscription entitlement management (SaaS Subscriptions +
GetEntitlements/ entitlement EventBridge events), NOT usage-based (PAYG) metering. It is OUT OF SCOPE — do NOT design an active-customer check, describeGetEntitlements/entitlement-event handling, or repurpose this skill's metering subscriber table for it. Reply ONLY with the redirect: "A flat-rate SaaS Subscription with no usage metering is out of scope for this usage-based (PAYG) metering skill. Verifying active/entitled customers is done via AWS Marketplace SaaS entitlements — see the public AWS documentation: SaaS subscriptions, checking entitlements with GetEntitlements, and subscription lifecycle events with EventBridge." - Forecasting / predicting FUTURE bills or usage (e.g. "forecast next month's customer bills from my CloudTrail metering trend", trend projection, revenue prediction): OUT OF SCOPE — this skill queries only HISTORICAL submissions (≤90-day CloudTrail) and billed history (Seller Reports); it does NOT forecast → "Forecasting future bills/usage is out of scope. This skill queries only historical metering submissions; for projections, hand the fetched CloudTrail/Seller-Reports data to your host agent/model for analysis, or use AWS Cost forecasting / your own analytics over Seller Reports."
- AWS account billing disputes → "Contact AWS Support"
- Anything not involving BatchMeterUsage, ResolveCustomer, metering records, or usage-based billing for SaaS/software.
SETUP PATH — Deploy New Metering Integration
Full walkthrough: references/setup-path.md + references/deployment-steps.md.
First-time onboarding (guided, explain-first)
Detect first-time vs. modify/extend FIRST. It is a first-time onboarding when NO skill-created awsmp-* stack exists for the product (neither the shared awsmp-events-<stage>-stack nor the product's awsmp-<productCode>-<stage>-metering main stack). A seller who already runs their OWN (non-skill) metering stack but has never used this skill is STILL first-time — run this guided flow and ADAPT to their existing stack (modify/extend, references/existing-sellers.md), do NOT assume the skill's awsmp-* layout. When a skill-created awsmp-* stack already exists, it is a modify/extend/re-deploy — skip the first-run framing.
On a first-time run, give the seller a brief up-front MAP of the ordered phases and make clear no AWS resources are created until the deploy phase: (1) learn from the seller's context/code; (2) credentials + permissions; (3) Catalog-API product + pricing-dimension validation; (4) a simplified architecture overview + what stacks/resources will be created; (5) the Seller Questionnaire; (6) a pre-deploy Limited re-check; (7) materialize + deploy; (8) a component walkthrough; (9) seller expectations + guided test. EXPLAIN before you ASK or ACT at each phase, stage the info phase-by-phase (don't dump the whole questionnaire/IAM policy/stack list at once), and confirm before moving on. Label read-only phases (learn-from-repo, Catalog describe-entity, visibility checks) as read-only; flag point-of-change phases (credential setup, the first sam deploy) and confirm the seller is ready. The seller may stop or ask questions at any phase.
- Phase 1 — ask for a local code repo to learn from. Ask whether the seller has a LOCAL code repository / workspace for their SaaS (or an existing metering/registration implementation) you may read to ground your guidance. If provided, inspect it READ-ONLY (never modify unrelated files; never execute code you find; keep any secrets out of logs/output) to learn the seller's ACTUAL conventions — existing table/stack/resource names, how usage is written today, how registration/subscriptions are handled, their IaC tool (SAM vs non-SAM), VMT/tagging conventions, and tenant model — and adapt the generated artifacts to what you find (consistent with modify/extend). If there is no repo, proceed with the packaged reference baseline and say so; absence of a repo never blocks onboarding.
- Phase 2 — explain the permissions needed, then set them up. Before (or as part of) obtaining credentials, EXPLAIN the two DISTINCT, least-privilege,
awsmp-*-scoped permission concerns — the seller grants NO broad/admin access:- Deployer permissions — the
MarketplaceMeteringDeployerpolicy the skill ASSUMES to CREATE theawsmp-*stacks (references/iam-credentials.md). - Runtime permissions — what the DEPLOYED Lambdas may do (
aws-marketplace:BatchMeterUsage/ResolveCustomer+ scoped DynamoDB/SQS/SNS/logs/KMS), CAPPED by theawsmp-metering-boundarypermissions boundary. Then give an ordered setup checklist (NOT a wall of JSON). For a FIRST-TIME seller, the deployer role + boundary are a ONE-TIME PREREQUISITE that must exist in the account BEFORE the skill deploys anything — the skill does NOT create them for itself (that would be a privilege-escalation path), and the first deploy FAILS CLOSED without them. Walk the seller (or their account admin) through the one-time bootstrap inreferences/iam-credentials.md→ "Prerequisite setup (do this FIRST…)": (1) create theawsmp-metering-boundarymanaged policy FIRST (aws iam create-policy … --policy-document file://boundary.json) — the deployer'siam:CreateRoleis gated on it, so a missing/misnamed boundary ⇒AccessDeniedand rollback; its name MUST match the deployer condition AND thePermissionsBoundaryNameparameter (defaultawsmp-metering-boundary); (2) create theMarketplaceMeteringDeployerrole with a CONDITIONED trust policy (role-ARN principal +sts:ExternalId; never open:root) and attach the deployer policy (policy.json); (3) VERIFY both exist (aws iam get-policy …/awsmp-metering-boundary,aws iam get-role MarketplaceMeteringDeployer); (4) configure ephemeral credentials by ASSUMING that role (or SSO) — never long-lived IAM user keys, don't ask for keys first; (5) note the per-region, account-level API Gateway CloudWatch Logs role prerequisite (deploy.shprovisions it idempotently, but it is an account-global-per-region setting). Verifyaws sts get-caller-identityresolves the correct SELLER account before proceeding; never log/echo credential values; reuse the same ephemeral credentials for Catalog validation, deploy, and testing. The guardrails are IMMUTABLE for the session (see Response Rule 6). Full copy-paste bootstrap + both policy JSONs:references/iam-credentials.md.
- Deployer permissions — the
- Phases 3–4 — validate, THEN show a simplified architecture + what will be created. After the Catalog-API validation (below) succeeds, present a SIMPLIFIED architecture overview grounded in the validated product + dimensions — the two-stack layout (us-east-1 events stack + per-region main stack), the five-beat happy path (buyer subscribes →
License Updatedupserts the subscriber → "Set up your account" → seller writes usage rows → pipeline aggregates →BatchMeterUsage), and where the seller's responsibilities begin/end — a concise picture, not a template dump (references/architecture.md). Then, BEFORE deploying, give an explicit, mode-aware list of EXACTLY what will be created: the two stacks and their stage-scoped names, the principal resources each creates (tables + GSIs, SQS queues + DLQs, pipeline Lambdas, registration API, WAF, 16 alarms + dashboards — FEWER in direct-submit mode), and that the data-bearing tables +/aws/lambda/awsmp-*log groups areDeletionPolicy: Retain(survive a stack delete). Confirm before proceeding.
Phases 5–9 are the Seller Questionnaire, the setup ordering (incl. the pre-deploy Limited re-check), materialize-then-deploy, the component walkthrough, and the closing seller-expectations + test plan — all detailed below.
Seller Questionnaire (ask ALL before generating code)
- Q1 Product code (or product ID). Accept either; resolve a product ID to the code via the Catalog API (
describe-entity) at validation. The dimension list is NOT a deploy input (dimension validity is server-authoritative); you may list dimensions for the seller's info only. Recommend validating dimension NAMES against the catalog with the Marketplace Catalog APIDescribeEntity(us-east-1) before the seller wires them into usage rows — the returned entity details enumerate the product's registered dimension API names (case-sensitive). Confirming the seller's dimension strings match the catalog exactly at setup time is the way to avoid the runtimeInvalidUsageDimensionException(an undefined/misspelled/wrong-case dimension fails the wholeBatchMeterUsagecall). This is a validation aid only — it does NOT make the dimension list a deploy input. - Q2 Are you the ISV/seller? Only the seller (publisher) or an authorized 3PI may integrate; a 3PI is out of scope — decline. Confirm publishing-account access.
- Q3 Which region(s) do you meter from? Events stack is always us-east-1; deploy a main stack in EACH region where usage occurs and meter in-region. Dedup is per
CustomerAWSAccountId + LicenseArn + dimension + hour, scoped to the submission region — do NOT funnel all usage into one region. Region support. - Q4 Stage name (MANDATORY). API Gateway stage (e.g.
v1/live/prod— seller's choice). No default, no fallback toprod; deploy fails fast if empty. Pass viaSTAGE_NAME. - Q5 Does a stack already exist for this product? If yes → modify/extend: gather context and inspect the seller's existing stack (may use arbitrary naming) before changing anything — see
references/existing-sellers.md. - Q6 Metering lock hours (
METERING_LOCK_HOURS→MeteringLockHours, default 1, validated 1..20). How many hours an hour stays open for late usage before it is aggregated/submitted (default 1 = process the just-completed hour). At the cap, an hour is first submitted at age 20, leaving ≥4h in the 24h billable window for retries/re-drive. Month-aware: previous-month hours keep the lock until 03:00 UTC on the 1st, then no-lock (submit each run) until the 06:00 UTC cutoff, and aren't aged out before then; a previous-month record written after 03:00 must already be pre-aggregated (elseDuplicateRecord). - Q7 TTL / retention (per table,
deploy.shenv vars;0disables):- Raw usage table
USAGE_TABLE_TTL_DAYS(default 365) — RECOMMEND on (high-volume table). The seller's writer sets thettlepoch attribute; the template only makes DynamoDB honor it. Floor ≥2 days that must exceedMeteringLockHours+24h so a row can't expire before metering. - Aggregated table
AGGREGATED_USAGE_TTL_DAYS(default 0 = retain) — RECOMMEND omit/large (small billing audit trail). When >0 the submitter setsttlonly onSuccessrows. - Enabling TTL needs the deployer's
dynamodb:UpdateTimeToLive(already in the policy).
- Raw usage table
- Q8 Create health dashboards? (
CREATE_DASHBOARD, default true). Per-product main-stack dashboard (per-Lambda Errors/Throttles/Invocations/Duration; queue/DLQ depth + oldest-age; EMFUsageRecordRejectedby reason,UsageRecordUnprocessed,CustomerNotSubscribed,BatchMeterUsageExceptionby exception,UsageAggregationExpired,UsageSubmissionExpired,DuplicateRecord) + a shared events-stack dashboard. Health-only; reflects the deployed mode (Q9); sellers can opt out / customize. - Q9 Full pipeline vs direct-submit (
DEPLOYMENT_MODE, defaultfull) — explain both so the seller chooses knowingly:- full — seller emits RAW per-second events; the skill deploys the raw
usagetable + discoverer→aggregator→cleanup→submitter→submission-expiry, and the pipeline aggregates each(licenseArn, account, dimension, hour)group to oneBatchMeterUsagerecord/hour. Best when the seller doesn't already aggregate or wants platform-owned aggregation + client-side rejection visibility. - direct-submit — seller ALREADY produces finalized hourly totals; the skill deploys ONLY
aggregated_usage+ submitter + submission-expiry + register/subscription (smaller/cheaper). Seller writes finalized records straight toaggregated_usage(PKlicenseArn, SKcustomerAWSAccountId#dimension#hour,quantity,meteringPending=hour bucket). - Finalize-before-insert (direct-submit, critical): any record in
aggregated_usageMAY be submitted on the next 5-min run — write a record ONLY when FINAL for its group, because a second write returnsDuplicateRecordand the later quantity is NOT billed. The seller owns upstream aggregation/idempotency here. - Not a one-way door: the seller can switch modes later (a stack UPDATE that adds/removes mode-specific resources; raw
usagetable isRetain). Call out the blast radius before flipping a live stack.
- full — seller emits RAW per-second events; the skill deploys the raw
- Q10 Registration fields + profile lookups (
ALLOWED_REGISTRATION_FIELDS) — EXPLAIN first, then ask. The in-region register Lambda persists ONLY these allowlisted fields (each length-bounded; the token is never stored) as aregistrationDatamap on the in-regioncustomer-profiletable (PKlicenseArn, SKcustomerAWSAccountId) — buyer PII stays in-region and OUT of the PII-free us-east-1 subscribers table. Fields in the map are retrievable by the base key but NOT independently queryable. Ask: (a) which form fields to capture (incl. any KYB fields — name/email/company/use-case); (b) whether any field must be looked up directly (e.g. byemail) — if so it is promoted to a top-level attribute + a GSI (ask HASH-only or HASH+RANGE per promoted field). Keep fields OPTIONAL/seller-confirmed; invent no PII; mask PII in logs. MappinglicenseArn→the seller's internal SaaS tenant ID is the SELLER's responsibility — anchor it on the subscribers table (licenseArnfromLicense Updated, PII-free) and/or the customer-profile table (licenseArncaptured at registration); add atenantIdattribute + GSI if lookup-by-tenant is needed (references/architecture.md).
Product Type Gate: after credentials, verify via the Catalog API the product is SaaS + usage-based (
ExternallyMetered). AMI/Container/SaaS-Contract-only → out of scope, redirect to public docs. Concurrent Agreements is the silent default — all new products useCustomerAWSAccountId+LicenseArn; do NOT ask. A pre-existing legacy stack may useProductCode+CustomerIdentifier— understand it before changing (references/existing-sellers.md).
Setup ordering (do NOT skip or reorder)
- One-time IAM prerequisite — create the deployer role + permissions boundary (first time in the account only). The skill runs by ASSUMING the
MarketplaceMeteringDeployerrole — a dedicated, least-privilege IAM role (NOT an admin user, NOT the seller's console identity) whose single inline policy is scoped toawsmp-*resources and lets the skill create exactly the metering stacks and nothing else. It is capped by theawsmp-metering-boundarypermissions boundary. The skill does NOT create these for itself (self-bootstrapping IAM is a privilege-escalation path), so on the FIRST integration in an account an admin creates them ONCE, in this exact order — the boundary MUST exist before the role (the role'siam:CreateRoleis gated on the boundary; a missing/misnamed boundary fails the deploy withAccessDenied+ rollback):- Create the boundary FIRST:
aws iam create-policy --policy-name awsmp-metering-boundary --policy-document file://boundary.json(theboundary.jsoncontent is inreferences/iam-credentials.md). Its name MUST equal the role's trust/condition AND thePermissionsBoundaryNametemplate parameter (defaultawsmp-metering-boundary). - Create the role with a CONDITIONED trust policy (role-ARN principal +
sts:ExternalId; never open:root):aws iam create-role --role-name MarketplaceMeteringDeployer --assume-role-policy-document '…'. - Attach its permissions policy:
aws iam put-role-policy --role-name MarketplaceMeteringDeployer --policy-name DeployPolicy --policy-document file://policy.json(thepolicy.jsoncontent is the full deployer policy inreferences/iam-credentials.md). - Verify both exist:
aws iam get-policy --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/awsmp-metering-boundaryandaws iam get-role --role-name MarketplaceMeteringDeployer. This is a one-time, per-account step; if the role + boundary already exist (a repeat integration), SKIP to step 2. Full copy-paste bootstrap + both policy JSONs:references/iam-credentials.md→ "Prerequisite setup (do this FIRST…)".
- Create the boundary FIRST:
- Obtain ephemeral credentials by ASSUMING
MarketplaceMeteringDeployer(SSO →sts assume-role; never long-lived IAM user keys; don't ask for keys first). Verify withaws sts get-caller-identitythat the correct SELLER account + the deployer role are resolved before proceeding. - Product type gate (SaaS + ExternallyMetered).
- Product visibility gate — deploy/mutate the product-specific MAIN stack ONLY when the CURRENT product is
Limited, neverPublished(provide code for the seller's own pipeline instead). Direct-deploy the main stack only when the current product isLimitedAND there is no existing per-product stack (an existing one is modify/extend, seller-confirmed). The SHARED events stack is never redeployed/mutated when it already exists (it may serve other, possiblyPublished, products with live traffic — reuse its outputs); only the first onboarding creates it. Seereferences/existing-sellers.md. - Confirm the questionnaire answers (incl. stage name).
- Only THEN generate + deploy: materialize the template(s) + Lambda handlers +
deploy.shin the seller's workspace and deploy THOSE on-disk files (sam build --template-file+sam deploy) — never an inline/transient template. Modify/extend edits the seller's existing files in place (merge env vars). Leave all artifacts in the workspace afterward (code/stack parity; seller can version/re-deploy/develop, customizing onlyTODO:sections). - EXISTING-STACK guardrails (ENFORCE, do not bypass). WHEN a stack already exists for this scope, the skill SHALL NOT auto-apply changes: it SHALL (a) present the exact diff/change set for the seller's review, (b) require the seller's EXPLICIT confirmation and their normal CODE REVIEW + change-management process before applying, (c) scope changes to the CURRENT product's main stack, and (d) never redeploy/mutate the shared events stack that already serves other (possibly
Published) products — reuse it. Direct auto-deploy is reserved for aLimitedproduct with NO existing stack. Seereferences/existing-sellers.md. - After integration, run the full test plan. At the END of setup/modify, the skill SHALL recommend and (where it can) walk the seller through the complete hands-on test plan (
references/test-plan.md) — verifying every expected observable outcome — and SHALL state that the integration should not carry PRODUCTION billing traffic until that review + testing passes.
A failure at any gate stops the workflow before deployment.
Stack naming
| Component | Pattern | Example |
|---|---|---|
| Events stack | awsmp-events-stack |
Shared, us-east-1 |
| Main stack | awsmp-<productCode>-metering |
awsmp-4ml54db8vrmjuykaw1psroool-metering |
| Events stack (staged) | awsmp-events-<stageName>-stack |
awsmp-events-beta-stack |
| Main stack (staged) | awsmp-<productCode>-<stageName>-metering |
awsmp-4ml54db8vrmjuykaw1psroool-beta-metering |
For a non-production sandbox, deploy stage-scoped stacks (the stage folds into every resource name) and set MeteringMode=dry-run so the submitter never calls the real BatchMeterUsage — use test accounts only. See references/existing-sellers.md / the setup path.
Supported features
SaaS Usage (PAYG) ✅ | Concurrent Agreements ✅ | VMT ✅ | Multi-region ✅ | Multi-product ✅ Out of scope (→ public docs): CCP, AMI/Container, SaaS-Contract-only (GetEntitlements), 3PI.
Scale: the pipeline is decoupled (discoverer → work queue → aggregator → aggregated-usage + cleanup queue → submitter) so it is designed to handle thousands of metering records per hour. Second-precision rows bound a group at ≤3600 rows/hour → supports high volume at ≤~1 write/sec per
(licenseArn, account, dimension)group. For finer/higher rates, recommend a streaming ingestion tier (e.g. Kinesis Data Streams) that pre-aggregates to per-second/hour rows (public-knowledge/host-model-determined, not first-class to this skill). Full design:references/scalable-metering-design.md. GSI-driven discovery (never a Scan): the discoverer finds pending usage via the sparsemetering_pendingGSI — HASH =meteringPendinghour-bucket (YYYY-MM-DDTHH), RANGE =licenseArn, projectingcustomerAWSAccountId+dimension(INCLUDE/ALL, NOTKEYS_ONLY) so a query returns the(licenseArn, customerAWSAccountId, dimension)tuples directly — no follow-up read to discover the group. Each group is then read with a targetedbegins_withsort-key prefix (begins_with(customerAWSAccountId#dimension#timestamp, "{account}#{dimension}#{hourPrefix}")), NOT a fulllicenseArn-partition query + timestampFilterExpression.
Deployment (summary; full steps in references/deployment-steps.md)
- Generate SAM template + Lambda code (fill
TODO:s). - Deploy events stack (us-east-1) — shared EventBridge + subscribers table.
- Deploy main stack (seller's region) — Lambdas + DynamoDB + API Gateway + alarms.
- Set the AMMP Fulfillment URL to the branded registration page (S3+CloudFront front-end that captures buyer/KYB fields and calls
RegistrationUrl), NOT the rawRegistrationUrl(smoke-test fallback only) — manual seller step. Seereferences/registration-page.md. - End-to-end test (subscribe → resolve → write a conforming usage row → discoverer → aggregator → submitter → verify
Aggregated+Success). - Validate via CloudTrail logs.
**After deploy, st
Truncated - read the full file at https://www.skills.sh/aws/agent-toolkit-for-aws/aws-marketplace-metering.
