skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
aws/agent-toolkit-for-aws93 installs

aws-marketplace-metering

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.

How do I install this agent skill?

npx skills add https://github.com/aws/agent-toolkit-for-aws --skill aws-marketplace-metering
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    This skill provides a robust framework for managing AWS Marketplace metering with significant security guardrails, including log masking, PII isolation, and the use of permissions boundaries. It includes potential considerations regarding an indirect prompt injection surface when querying external data and the use of command execution for infrastructure management. These findings are addressed through structured data handling and specific security configurations. See detailed analysis for context.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

What does this agent skill do?

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 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 it RejectedClientSide (reason = the exception name), and emits the BatchMeterUsageException metric. 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, InvalidDimension, 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, TooManyTagsException, InvalidUsageDimensionException). 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 ↔ TooManyTagsException; InvalidDimension ↔ 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) 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 TooManyTagsException. 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) 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 env; if it is unset that is a MISCONFIGURATION and the handler fails loud (HTTP 500) rather than silently skipping the 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, 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 ($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.
  • 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 customerAWSAccountId, with attributes agreementId, agreementStatus, subscriptionStatus, productCode, registeredRegions, deprovisioningExpiry (set only during a deprovisioning flush), and createdAt/updatedAt audit stamps. registeredRegions is a set updated idempotently (append via ADD, no duplicates) and is reference-only — it NEVER gates metering. It carries NO registration form data / NO PII (PII lives only in the in-region customer-profile table). A customerAWSAccountId GSI supports account-scoped lookups. Why licenseArn is the PK (all THREE reasons): (1) under Concurrent Agreements one buyer can hold MULTIPLE concurrent licenses for the same product; (2) using customerAWSAccountId as 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 per CustomerAWSAccountId + LicenseArn + dimension + hour), so per-agreement keying keeps them isolated. Name all three when asked why licenseArn is the partition key.
  • ONE PRODUCT PER BatchMeterUsage CALL — do NOT mix products in one batch. Every UsageRecord (every LicenseArn) in a single BatchMeterUsage request MUST belong to the SAME product. Mixing LicenseArns from two different products in one call returns a request-level InvalidLicenseException and fails the ENTIRE request (nothing meters). Make a SEPARATE BatchMeterUsage call per product. Within that single-product batch you MAY combine different dimensions and different buyers/licenses of that one product; dedup remains per CustomerAWSAccountId + 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 (transient ThrottlingException/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

  1. Check Out of Scope FIRST — if out of scope, decline in one sentence; do NOT proceed.
  2. KNOWLEDGE questions (how do I, how does X work, which data source): answer from this skill; do NOT call AWS APIs/tools.
  3. ACTION questions (deploy now, query now, run this): execute AWS commands — prefer the shell tool with AWS CLI over MCP use_aws for long-running ops.
  4. Multi-region/multi-product questions are KNOWLEDGE — explain the architecture; do NOT deploy unless explicitly asked.
  5. "How do I find X" = KNOWLEDGE — explain the method; execute only on "show me"/"run it".

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-fires License 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, describe GetEntitlements/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.

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 API DescribeEntity (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 runtime InvalidUsageDimensionException (an undefined/misspelled/wrong-case dimension fails the whole BatchMeterUsage call). 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 to prod; deploy fails fast if empty. Pass via STAGE_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 (else DuplicateRecord).
  • Q7 TTL / retention (per table, deploy.sh env vars; 0 disables):
    • Raw usage table USAGE_TABLE_TTL_DAYS (default 365) — RECOMMEND on (high-volume table). The seller's writer sets the ttl epoch attribute; the template only makes DynamoDB honor it. Floor ≥2 days that must exceed MeteringLockHours+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 sets ttl only on Success rows.
    • Enabling TTL needs the deployer's dynamodb:UpdateTimeToLive (already in the policy).
  • Q8 Create health dashboards? (CREATE_DASHBOARD, default true). Per-product main-stack dashboard (per-Lambda Errors/Throttles/Invocations/Duration; queue/DLQ depth + oldest-age; EMF UsageRecordRejected by reason, UsageRecordUnprocessed, CustomerNotSubscribed, BatchMeterUsageException by 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, default full) — explain both so the seller chooses knowingly:
    • full — seller emits RAW per-second events; the skill deploys the raw usage table + discoverer→aggregator→cleanup→submitter→submission-expiry, and the pipeline aggregates each (licenseArn, account, dimension, hour) group to one BatchMeterUsage record/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 to aggregated_usage (PK licenseArn, SK customerAWSAccountId#dimension#hour, quantity, meteringPending=hour bucket).
    • Finalize-before-insert (direct-submit, critical): any record in aggregated_usage MAY be submitted on the next 5-min run — write a record ONLY when FINAL for its group, because a second write returns DuplicateRecord and 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 usage table is Retain). Call out the blast radius before flipping a live stack.
  • 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 a registrationData map on the in-region customer-profile table (PK licenseArn, SK customerAWSAccountId) — 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. by email) — 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. Mapping licenseArn→the seller's internal SaaS tenant ID is the SELLER's responsibility — anchor it on the subscribers table (licenseArn from License Updated, PII-free) and/or the customer-profile table (licenseArn captured at registration); add a tenantId attribute + 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 use CustomerAWSAccountId + LicenseArn; do NOT ask. A pre-existing legacy stack may use ProductCode + CustomerIdentifier — understand it before changing (references/existing-sellers.md).

Setup ordering (do NOT skip or reorder)

  1. Obtain ephemeral credentials (SSO → sts assume-role; never long-lived keys; don't ask for keys first).
  2. Product type gate (SaaS + ExternallyMetered).
  3. Product visibility gate — deploy/mutate the product-specific MAIN stack ONLY when the CURRENT product is Limited, never Published (provide code for the seller's own pipeline instead). Direct-deploy the main stack only when the current product is Limited AND 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, possibly Published, products with live traffic — reuse its outputs); only the first onboarding creates it. See references/existing-sellers.md.
  4. Confirm the questionnaire answers (incl. stage name).
  5. Only THEN generate + deploy: materialize the template(s) + Lambda handlers + deploy.sh in 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 only TODO: sections).
  6. 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 a Limited product with NO existing stack. See references/existing-sellers.md.
  7. 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

ComponentPatternExample
Events stackawsmp-events-stackShared, us-east-1
Main stackawsmp-<productCode>-meteringawsmp-4ml54db8vrmjuykaw1psroool-metering
Events stack (staged)awsmp-events-<stageName>-stackawsmp-events-beta-stack
Main stack (staged)awsmp-<productCode>-<stageName>-meteringawsmp-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 sparse metering_pending GSI — HASH = meteringPending hour-bucket (YYYY-MM-DDTHH), RANGE = licenseArn, projecting customerAWSAccountId + dimension (INCLUDE/ALL, NOT KEYS_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 targeted begins_with sort-key prefix (begins_with(customerAWSAccountId#dimension#timestamp, "{account}#{dimension}#{hourPrefix}")), NOT a full licenseArn-partition query + timestamp FilterExpression.

Deployment (summary; full steps in references/deployment-steps.md)

  1. Generate SAM template + Lambda code (fill TODO:s).
  2. Deploy events stack (us-east-1) — shared EventBridge + subscribers table.
  3. Deploy main stack (seller's region) — Lambdas + DynamoDB + API Gateway + alarms.
  4. Set the AMMP Fulfillment URL to the branded registration page (S3+CloudFront front-end that captures buyer/KYB fields and calls RegistrationUrl), NOT the raw RegistrationUrl (smoke-test fallback only) — manual seller step. See references/registration-page.md.
  5. End-to-end test (subscribe → resolve → write a conforming usage row → discoverer → aggregator → submitter → verify Aggregated + Success).
  6. Validate via CloudTrail logs.

After deploy, state the seller's ongoing responsibilities and guide the hands-on test (references/test-plan.md): the stacks do nothing until the seller writes conforming usage rows — whole-second customerAWSAccountId#dimension#timestamp sort key (matching segments) and a meteringPending bucket that equals the row's own UTC timestamp hour. A mis-stamped meteringPending is the ONE writer error the pipeline cannot detect (silently never metered) — confirm via the test. Also: subscribe a notification to the alarms + watch the dashboards (UsageRecordRejected by reason, BatchMeterUsageException by exception, CustomerNotSubscribed, UsageAggregationExpired/UsageSubmissionExpired); catalog dimension changes need no redeploy. Several results are async (post-subscribe License Updated → licenseArn/subscriptionStatus; hourly discoverer + rate(5m) submitter/expiry) — poll with bounded backoff, or manually invoke a Lambda to exercise a path immediately (say which). Recommend running the FULL test plan (references/test-plan.md) end-to-end at the end of the integration and verifying every expected observable outcome — plus a review of all created/changed resources — BEFORE the integration carries production billing traffic (AI-generated output; see the review-before-production callout above).

Prerequisites, credentials, and the full least-privilege deployer policy: references/iam-credentials.md.

Event & status model (recite when asked how cancellation/metering works)

Two INDEPENDENT status fields on the subscriber row (never conflate):

  • agreementStatus = active/inactive (agreement lifecycle).
  • subscriptionStatus = active/deprovisioning/inactive (license lifecycle; there is no deprovisioned value).

Metering follows the LICENSE lifecycle only:

  • License Updated → agreement+subscription active (metering allowed).
  • License Deprovisioned → subscriptionStatus=deprovisioning + a deprovisioningExpiry (event time + ~1h); OPENS the ~1-hour final-usage flush window. The lock-bypass mechanism: during this window the pipeline submits ALL remaining pending usage for that license IMMEDIATELY, ignoring MeteringLockHours and the normal completed-hours-only gate — it does NOT wait for MeteringLockHours to elapse and it DOES meter the current in-progress hour (the one hour the normal path always skips). Concrete mechanism/marker: the License Deprovisioned handler sets subscriptionStatus=deprovisioning + deprovisioningExpiry on that subscriber row; the discoverer runs in a flush-deprovisioning mode that keys off that marker and enqueues the pending buckets (over now-23h … now-1h PLUS the current hour) for only that license, oldest-first. The bypass is SCOPED to the deprovisioning license ONLY — other active licenses keep the normal MeteringLockHours gate and completed-hours-only behavior; one buyer's deprovisioning never force-flushes another buyer's open hour. It is the deprovisioning flush that bypasses the lock, NOT a change to MeteringLockHours itself and NOT a Purchase Agreement Ended event. Once the window has elapsed, the events-stack deprovision-cleanup Lambda (rate(15m)) sets inactive and clears the deprovisioning markers — finalization is time-based, so it is correct across multiple hours and regions. After the server window closes, BatchMeterUsage returns CustomerNotSubscribed.
  • Purchase Agreement Ended → agreementStatus=inactive ONLY (does NOT change subscriptionStatus, stop metering, or flush).
  • Purchase Agreement Amended → refresh agreement metadata (stays active).
  • Purchase Agreement Created → NOT consumed by metering (no licenseArn); the seller handles it if needed (pre-provisioning, CRM), without activating metering.

Do NOT invert these (License Deprovisioned, not Purchase Agreement Ended, opens the flush window). Do NOT discard queued records before flushing. registeredRegions is reference-only and NEVER gates metering.

Setup references

  • Full walkthrough + writer contract + Common Mistakes + VMT/token/timestamp/schema/meter-flow: references/setup-path.md
  • Deployment steps: references/deployment-steps.md · IAM policy: references/iam-credentials.md
  • Concurrent Agreements + data model: references/concurrent-agreements.md · Architecture + status lifecycle: references/architecture.md
  • Existing sellers (modify/extend, legacy): references/existing-sellers.md
  • Branded buyer page + Know Your Buyer (KYB) capture: references/registration-page.md
  • Hands-on test plan (manual steps, scoped cases, async polling): references/test-plan.md
  • Monitoring & alarms: references/troubleshooting.md · Client-side guardrails (all checks before BatchMeterUsage): references/client-side-guardrails.md
  • Reference implementation: scripts/metering_core.py (shared) + discoverer.py/aggregator.py/cleanup.py/submitter.py/expiry.py (pipeline) + register.py/subscription.py; scripts/test_event_publisher.py (non-prod sandbox test-event generator, dry-run stages only); scripts/deploy.sh (needs assets/requirements.txt); SAM templates assets/template-main.yaml, assets/template-events.yaml

Monitoring & alarms (seller-owned)

The deployed stack creates awsmp-* metrics + CloudWatch alarms in the SELLER's account covering the revenue-loss signals (per-stage Lambda Errors/Throttles across discoverer/aggregator/cleanup/submitter/expiry, a "did not run" invocations alarm, >24h age-out, and the EMF business-status metrics). Alarms are always created; actions wire to an OPTIONAL in-region SNS topic (AlertsTopicArn). Business-status metrics use EMF from the Lambda logs (no extra runtime IAM). Handling alarms is the seller's responsibility — AWS Marketplace and this skill do not track client-side errors; neither the deployer nor any Lambda publishes to the topic (CloudWatch fires the action). Thresholds/actions/topic are seller TODO:s. Detail: references/troubleshooting.md.

"What alarms tell me metering records are being REJECTED / not metered?" — the stack creates a dedicated alarm on each rejection/failure signal so a seller is paged the moment records stop metering (all fire into AlertsTopicArn; thresholds are seller TODO:s):

  • UsageRecordUnprocessed (alarm *-usage-unprocessed) — BatchMeterUsage returned UnprocessedRecords that were STILL unprocessed after the submitter's one automatic retry. The submitter does NOT raise on this (it leaves them pending for the next rate(5m) cycle), so this metric is the ONLY alarm-able signal for the UnprocessedRecords case — it does NOT trip the Lambda Errors alarm. A persistent breach = records repeatedly failing to meter (throttling, timestamp bounds, downstream issue) → investigate.
  • UsageRecordRejected (*-usage-rejected) — client-side guardrail rejections before metering (summed across reasons; per-reason breakdown on the dashboard); fix source rows + resubmit. BatchMeterUsageException (*-batchmeterusage-exception) — a REQUEST-level exception failed a whole call (dimension/license/etc.); pipeline bisects to isolate. CustomerNotSubscribed (*-customer-not-subscribed) — per-record status, no active subscription (do NOT retry).
  • Plus age-out alarms (UsageAggregationExpired/UsageSubmissionExpired, >24h) catch usage that silently never metered, and Lambda Errors/Throttles/DLQ-depth alarms catch pipeline failures. DuplicateRecord is a dashboard metric (informational — first submission already billed), not an alarm.

QUERY PATH — Find, Debug, and Analyze Metering Records

Full routing/patterns: references/query-path.md + references/query-patterns.md.

Capability boundary (state it; don't return a bounded answer as authoritative). This query USES CloudTrail Event History (LookupEvents) in the seller's region, last ≤90 days, page-bounded and processed in memory — a recent, regional, SUBMISSIONS view (includes rejected/duplicate records that were NOT billed) for BASIC operational/debug queries (lookups + simple filters/counts/summaries by customer, dimension, status, time). It RECOMMENDS (does not run): Seller Reports/SDDS for billed totals + >90-day history; the host agent + model for complex/open-ended analytics (hand it the fetched data); and — for CA/LicenseArn metering with no LicenseArn→productCode mapping (single-product-stack context, CloudTrail additionalEventData, or the subscribers table) — only non-product-scoped queries (never guess attribution). Every result carries complete/truncated/windowClipped. Before presenting a result, CONFIRM these signals: if complete is false (page-truncated OR clipped at the 90-day retention limit), either re-run for a complete answer where feasible (narrow/adjust range for a clip; raise --max-pages or add filters for truncation) or state explicitly that the result is PARTIAL — what was clipped/truncated and the covered range — and point to Seller Reports for the authoritative/older portion. A truncated or clipped aggregate (summary/count/top_customers) is a lower-bound sample, NOT authoritative. Never report a partial result as the full requested range. Do not refuse a basic query just because a richer answer is possible in the host agent. CloudTrail-derived SQL is the WRONG source for billing/usage analytics — CloudTrail Lake OR Athena over a trail → S3 export both see SUBMISSIONS only (rejected/duplicate, regional, no CA productCode); do NOT set up CloudTrail Lake for metering; for complex analytics / billing-grade totals / >90-day data use SDDS (recommend, don't run), and use a trail → S3 (never Lake) only for long-term raw-submission audit retention. Full boundary: references/query-path.md.

Complex / billing-grade query steering — MUST NOT vs allowed sources (enforce; do not violate). For any complex-analytics or billing-accuracy question over metering data:

  • MUST NOT recommend CloudTrail Lake (create-event-data-store) for metering — ever.
  • MUST NOT present CloudTrail (Event History OR a trail → S3/Athena export) as a source of billed/billing-grade totals — CloudTrail sees SUBMISSIONS only (includes rejected/duplicate, regional, no CA productCode), so it is never authoritative for revenue.
  • Allowed sources only: for billed/disbursed totals and >90-day billed history → Seller Reports / AMMP Billed Revenue dashboard and SDDS (recommend, do not run); for recent (≤90-day) regional SUBMISSION debugging → CloudTrail Event History; for complex/open-ended analytics → hand the fetched data to the host agent + model; for long-term RAW-submission audit retention only → a plain trail → S3 (NOT Lake) + Athena, understanding it is submissions-not-billing. Route every complex-billing ask to these allowed sources and name why CloudTrail is not billing-authoritative.

Steps

  1. Ask the metering region (MUST — CloudTrail is regional).
  2. Extract filters: customer_id, status, dimension, start_date/end_date, query_type (summary/detail/count/list_failures/top_customers), top_n.
  3. Run the script (default: last 7 days):
python3 ./scripts/query_metering.py \
  --region <SELLER_METERING_REGION> \
  [--days N] [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] \
  [--customer-id ACCT_ID] [--status STATUS] [--dimension DIM] \
  [--product-code PC] [--deployment-product-code PC] \
  [--query-type summary|detail|count|list_failures|top_customers] \
  [--top-n N] [--limit N] [--max-pages N]

CloudTrail Event History recipe (what the script does; use directly if querying by hand). Event History is on by DEFAULT — no data store, no setup, never CloudTrail Lake / create-event-data-store for metering. Query IN the seller's metering region and filter on the metering event source:

aws cloudtrail lookup-events --region <SELLER_METERING_REGION> \
  --lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
  --start-time <ISO> --end-time <ISO>

Each BatchMeterUsage event's CloudTrail responseElements carries results (per-record status: Success/CustomerNotSubscribed/DuplicateRecord — those are the ONLY per-record statuses) and unprocessedRecords — to verify a submission, check results vs unprocessedRecords FIRST (records in unprocessedRecords were not processed — see the UnprocessedRecords note below). CloudTrail lowercases the first letter of response fields, so read results/status/meteringRecordId/unprocessedRecords (lowercase) — the direct SDK BatchMeterUsage response uses PascalCase (Results/Status/ MeteringRecordId/UnprocessedRecords). An undefined dimension is NOT a per-record status — it fails the whole call with a request-level InvalidUsageDimensionException (see below). ≤90 days only; for older/billed totals use Seller Reports/SDDS (recommended, not queried). CloudTrail is NOT revenue — it shows submissions (incl. rejected/duplicate, not billed). Metered usage bills at the START of the following month (the 2nd/3rd) — e.g. November usage lands on the December invoice — and dashboard revenue is estimated until the month closes; use Seller Reports for billed/disbursed amounts.

Status meanings

Per-record Results[].Status values:

StatusMeaningAction
SuccessAccepted and billedNone
CustomerNotSubscribedNo active subscription⚠️ Do NOT retry
DuplicateRecordFirst-write-wins and reported; a differing-quantity resubmit is NOT billedSafe only for an identical retry; never to change a quantity (under-billing risk)

Request-level exceptions (fail the ENTIRE BatchMeterUsage call, NOT a per-record status). Each has a client-side guardrail reason code the pipeline stamps when it rejects the row BEFORE the API call (RejectedClientSide, not billed), plus the server-side exception if a row reaches the API — surface BOTH when explaining a rejection:

Server-side exceptionClient-side reason codeMeaningAction
InvalidUsageDimensionExceptionInvalidDimensionA dimension not defined in the catalog (server-authoritative; case-sensitive; not billed)Fix the dimension key / define it in AMMP. The pipeline isolates the offending record by bisection (depth-bounded, ⌈log2(batch size)⌉=5) so its co-batched records still meter, marks it RejectedClientSide (reason = the exception name), and emits the BatchMeterUsageException metric.
TimestampOutOfBoundsExceptionMalformedTimestampA record older than 24h (e.g. a 30h-old row) or otherwise outside the window — REJECTED, not submittedFix the timestamp; the client-side guardrail rejects the row with reason MalformedTimestamp before batching, and one bad timestamp that reaches the API rejects the whole batch until isolated.
InvalidLicenseExceptionInvalidLicenseArnA malformed/invalid LicenseArnShape-validate client-side (reason InvalidLicenseArn); a server-side InvalidLicenseException is isolated by (bounded) bisection and marked RejectedClientSide with the exception name.
TooManyTagsExceptionTooManyTagsMore than 5 usageAllocations tags on a UsageRecordReduce to ≤5 tags; the client-side guardrail rejects the row with reason TooManyTags before batching.

Sort-key / reject-code ordering: the pipeline validates a row's shape BEFORE it can enter a batch. A row whose sort key (customerAWSAccountId#dimension#timestamp) carries a malformed or out-of-window timestamp is rejected client-side with reason code MalformedTimestamp (marked RejectedClientSide, emits UsageRecordRejected) — it never reaches BatchMeterUsage, so it does NOT surface as a server-side TimestampOutOfBoundsException unless a bad timestamp slips through into a batch. Always name the client-side MalformedTimestamp reason code for a malformed-timestamp sort key, and its server-side TimestampOutOfBoundsException equivalent.

Full reject-/status-code catalog (the ONE place to cite for any reason-code question). A question like "what does UnknownDimension mean?" should be answered from this complete set, mapping the client-side guardrail reason code to its server-side equivalent:

Client-side reason code (RejectedClientSide, pre-API, not billed)Server-side equivalentMeaning
MalformedSortKey / SortKeyMismatch— (never reaches API)Sort key not customerAWSAccountId#dimension#timestamp, or its segments don't match the row's own attributes
MalformedTimestamp / TimestampOutOfWindowTimestampOutOfBoundsExceptionNon-whole-second, unparseable, or >24h-old timestamp
MissingDimension—Row has no dimension
InvalidDimension / UnknownDimensionInvalidUsageDimensionExceptionDimension not defined in the catalog (case-sensitive) — UnknownDimension/InvalidDimension are the same "dimension not in the AMMP catalog" condition
NegativeQuantity—Quantity < 0
NonIntegerQuantity—Quantity not a whole integer
TooManyTagsTooManyTagsException>5 usageAllocations tags on a record
TooManyAllocations—>2500 merged tag-sets in a group
AllocationSumMismatch—AllocatedUsageQuantity values don't sum to the row quantity
MixedAllocatedAndUnallocated—Group mixes tagged and untagged non-zero rows
InvalidLicenseArnInvalidLicenseExceptionMalformed/invalid LicenseArn (also fires when products are mixed in one batch)

Plus the terminal per-record Results[].Status verdicts returned BY the API (not client-side reasons): Success (billed), DuplicateRecord (first-write-wins; a differing-quantity resubmit is NOT billed — under-billing risk, not "benign"), CustomerNotSubscribed (no active subscription — do NOT retry). And UnprocessedRecords (transient, retryable) — see below.

UnprocessedRecords (a THIRD, distinct outcome — neither a per-record Results[].Status verdict nor a request-level exception): a PARTIAL-BATCH result (succeeded records in results; failed-to-process ones in unprocessedRecords — NOT billed, typically TRANSIENT ThrottlingException/InternalServiceException), SAFE to re-submit (idempotent); the submitter retries ONCE then leaves it for the NEXT scheduled run (alarmed via UsageRecordUnprocessed). Do NOT treat it as accepted/billed or confuse it with the FINAL Results[].Status verdicts above.

Data tiers: CloudTrail Event History (≤90 days, free, real-time) is the tier the skill queries directly; it shows ALL submissions (incl. rejected/duplicate), Seller Reports show only billed — so "totals don't match" = the rejected/duplicate delta. Two separate history questions, don't conflate them:

Reconciling SUBMITTED (CloudTrail) vs BILLED (Seller Reports). Treat Seller Reports / AMMP Billed Revenue as the billed source of truth; CloudTrail is submissions only. To reconcile, filter CloudTrail to Status=Success (exclude DuplicateRecord, CustomerNotSubscribed, and request-level exceptions — none of those bill) and compare that to the billed report; the leftover gap is the expected rejected/duplicate delta. Watch for DuplicateRecords with DIFFERENT quantities for the same (account, licenseArn, dimension, hour) — that is NOT benign: first-write-wins means the later (often larger) quantity was NOT billed, so it can indicate under-reporting/under-billing. Surface those as a reconciliation red flag rather than dismissing duplicates.

  • BILLED history (any range) needs NO setup — the AMMP Insights "Billed Revenue" dashboard (only aws-marketplace:GetSellerDashboard; default 6 months, widen the invoice-date filter). Console-only (QuickSight) + manual CSV export — no on-demand billed API. Programmatic billed data = SDDS (daily S3 push + your Athena/ETL), but push-only and enrollment-forward (no backfill). Skill recommends, doesn't run; never GenerateDataSet. Detail: references/seller-reports.md.
    • Discounts / private offers are NOT a metering or Seller-Reports concern — redirect to the public AWS Marketplace channel (AMMP). If a seller asks why billed amounts differ from metered usage because of a discount or private offer, or how to apply/change one, do NOT try to explain or reconcile it from metering data and do NOT stop at "out of metering scope." The pricing that turns metered quantity into billed revenue (public price, private offers, discounts, EULA terms) is managed in the AWS Marketplace Management Portal (AMMP) — state that the discount/private-offer delta is expected (Seller Reports show BILLED revenue after any negotiated discount, while CloudTrail shows raw metered SUBMISSIONS) and redirect discount/private-offer creation and management to AMMP → AWS Marketplace Management Portal / private offers.
  • RAW SUBMISSION history beyond 90 days has NO out-of-the-box source (Seller Reports/SDDS are billed-only; Event History keeps 90 days). Retain via a self-managed CloudTrail trail → S3 on metering-marketplace.amazonaws.com (plain trail, NOT Lake; Athena) — also from setup forward, no backfill; complementary DynamoDB PITR export of aggregated_usage. Detail: references/query-patterns.md.

Query references

  • Routing + parameters: references/query-path.md · CloudTrail CLI patterns + pagination: references/query-patterns.md
  • Seller Reports reconciliation + SDDS automation boundary: references/seller-reports.md · Dimension reference: references/dimension-reference.md

Key APIs

APIRegionPurpose
ResolveCustomerany region (seller's choice)Resolve token → LicenseArn + customerAWSAccountId + productCode at registration; regional, from the publishing account; independent of the BatchMeterUsage region
BatchMeterUsageseller's metering regionSubmit usage records (hourly)
DescribeEntityus-east-1Validate product / type / visibility
LookupEventsseller's regionQuery metering records (≤90 days)

IAM action names: aws-marketplace:BatchMeterUsage (meter), aws-marketplace:ResolveCustomer (register), aws-marketplace:GetSellerDashboard (reports). Namespace is aws-marketplace:, NOT meteringmarketplace:*. When diagnosing AccessDenied, name the exact action and check the execution role, condition keys, and Resource-ARN mismatches.

Security

  • Least privilege: execution roles grant ONLY the actions used — meter path: aws-marketplace:BatchMeterUsage + scoped DynamoDB Query/UpdateItem on its tables + the metering_pending index; register: aws-marketplace:ResolveCustomer + scoped Query on the customerAWSAccountId GSI + item ops. No dynamodb:Scan (lookups use GSI Query); never dynamodb:* or broad aws-marketplace:*.
  • No swallowed auth errors: subscriber/license lookups do NOT catch AccessDeniedException as "not found" — permission/config errors surface (Lambda error / 500 / DLQ).
  • Encryption at rest: SQS (SqsManagedSseEnabled), SNS (alias/aws/sns), DynamoDB (SSEEnabled); Lambda + API log groups are explicit resources with RetentionInDays + optional KmsKeyId (they contain buyer account IDs / license ARNs).
  • Public registration API: intentionally open (Marketplace POSTs the token unauthenticated) → template attaches a baseline WAF WebACL by default (rate-based + common-exploit; WebAclArn overrides); API Gateway throttling + access logging always on; register Lambda returns security headers and persists ONLY an allowlist of length-bounded fields (AllowedRegistrationFields).
  • Credentials (ephemeral-first): SSO → sts assume-role; no long-lived keys, don't ask for keys first. Deployer role uses a conditioned trust policy (not open :root) + permissions-boundary-bounded iam:CreateRole.
  • Stage name: StageName has no default + an AllowedPattern rejecting empty; mandatory questionnaire answer; sam deploy and deploy.sh fail fast (no prod fallback).
  • Event scoping: EventBridge rule scoped to source: ["aws.agreement-marketplace"]; the EventBridge→SQS policy via aws:SourceArn.
  • Data classification: customer AWS account IDs + agreement metadata are sensitive — avoid logging full identifiers; encrypt log groups that contain them.

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/aws/agent-toolkit-for-aws/aws-marketplace-metering">View aws-marketplace-metering on skillZs</a>