aws-marketplace-metering
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAWS Marketplace Metering
AWS Marketplace 计量
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,) 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:deploy.sh
- 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 (
) 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.references/test-plan.md
完整的 AWS Marketplace 按用量计费生命周期管理:
- 设置(SaaS PAYG)——部署新的无服务器计量集成(SAM、ResolveCustomer、BatchMeterUsage、EventBridge)。
- 查询——查找、检查、调试、分析现有的计量提交(CloudTrail 事件历史 ≤90 天;卖家报告用于已计费历史)。
范围是 SaaS 按用量(PAYG)。SaaS Contract、含消费定价的合同(CCP)以及 AMI/Container 不在范围内——重定向到公开 AWS 文档。
建议: AWS MCP 服务器提供沙盒执行和审计日志。⚠️ AI 生成输出——生产前审查。此 skill 由 AI Agent 执行,可能会出错。 它生成的所有内容(CloudFormation/SAM 模板、Lambda 处理器、IAM 策略、)以及它部署到卖家 AWS 账户的所有内容都必须视为待审查的提案,而非可信工件。在任何已部署堆栈承载生产计量流量(真实计费)之前,卖家必须:deploy.sh
- 彻底审查 skill 创建或更改的每个资源——模板、Lambda 代码、IAM 角色/策略(最小权限、无意外授权)、表/索引、队列、告警——在其自己的账户中,并确认符合其意图和组织标准。计量错误直接影响收入(少计或多计),因此此审查不可省略。
- 将生成的代码经过卖家正常的代码审查 + 变更管理流程(不要将 AI 输出直接投入生产),并且
- 完整运行动手测试计划(
)并验证每个预期的可观察结果,然后才依赖该集成进行真实计费。 该 skill 应向卖家传达此"生产前审查"预期,且不得暗示成功部署即意味着无需审查和测试即可投入生产。references/test-plan.md
Using this skill's packaged resources
使用此 skill 打包的资源
The packaged , , show how to structure a metering stack — a reference, NOT content to copy verbatim.
scripts/assets/references/- Get every seller-specific value from the seller (product code, region(s), stage name, VMT tags, alarm actions/thresholds, retention/KMS) and fill the markers. If a required value is unknown, ask — do not assume. VMT is presence-based: the seller writes each usage row's
TODO:(their real tags); the pipeline (aggregator) merges them and never invents tags (nousageAllocations/VmtEnabledparameter).VmtTagAttribute - Do not change the architecture or naming (two-stack layout, table/GSI/event wiring are fixed). Sellers customize only the marked
awsmp-*sections. A request that would break the architecture/naming is explained as a constraint, not silently accommodated.TODO:
打包的 、、 展示了如何构建计量堆栈——作为参考,而非逐字复制的内容。
scripts/assets/references/- 从卖家获取每个卖家特定的值(产品代码、区域、阶段名称、VMT 标签、告警操作/阈值、保留/KMS),并填写 标记。如果必需值未知,请询问——不要假设。VMT 基于存在性:卖家写入每个用量行的
TODO:(其真实标签);管道(聚合器)合并它们,绝不发明标签(无usageAllocations/VmtEnabled参数)。VmtTagAttribute - 不要更改架构或 命名(双堆栈布局、表/GSI/事件接线是固定的)。卖家只自定义标记的
awsmp-*部分。会破坏架构/命名的请求被解释为约束,而非静默迁就。TODO:
Guardrail — where this skill's own files live (MCP vs local)
护栏——此 skill 自身文件的位置(MCP 与本地)
Before reading a reference or running a script, determine how the skill was loaded:
- AWS MCP : the skill is NOT on the local filesystem — fetch each reference/script via
retrieve_skillwith theretrieve_skillparameter (e.g.file); do NOTfile="references/deployment-steps.md"these paths.file_read - 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.
在读取参考或运行脚本之前,确定 skill 的加载方式:
- AWS MCP : skill 不在本地文件系统上——使用
retrieve_skill参数通过file获取每个参考/脚本(例如retrieve_skill);不要对这些路径执行file="references/deployment-steps.md"。file_read - 本地安装: 通过相对路径从本地 skill 目录读取/运行文件。
这仅适用于 skill 自身的文件;用户数据/会话工件始终位于用户的工作目录中。
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.
- resolves the buyer's
ResolveCustomer,LicenseArn, andcustomerAWSAccountIdfrom theproductCodewhen 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 +x-amzn-marketplace-token). It returns a top-levelregisteredRegionsfield —LicenseArnis 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.CustomerIdentifiercan be called in ANY supported region independently of whereBatchMeterUsagewas called. It is NOT a us-east-1-only API (only the EventBridge subscription events likeResolveCustomerare us-east-1-only). It MUST be called from the account that PUBLISHED the SaaS product (else the token will not resolve). TheLicense Updatedis 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 expiryx-amzn-marketplace-tokenreturnsResolveCustomer(an already-consumed/invalid token returnsExpiredTokenException) 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. (InvalidTokenException.)references/troubleshooting.md - DuplicateRecord = first-write-wins and REPORTED (, not
Status = DuplicateRecord); 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".Success - CustomerNotSubscribed = real failure, do NOT retry. Causes: (1) invalid identifier, (2) agreement ended/expired, (3) buyer account suspended.
- A bad is a REQUEST-level
LicenseArnthat fails the ENTIREInvalidLicenseExceptionbatch — NOT a per-recordBatchMeterUsageinStatus(unlikeResults). Mitigation: shape-validate the LicenseArn client-side before batching (reasonCustomerNotSubscribed) so a malformed ARN never enters a batch, and on a request-levelInvalidLicenseArnbisect-and-retry to isolate the poison record so its co-batched records still meter.ClientError - (SaaS) is REGIONAL, but the SELLER CHOOSES the region — there is NO
BatchMeterUsage. 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 returnsInvalidEndpointRegionException(meter in a supported region instead). There is noDisabledApiExceptionfor SaaSInvalidEndpointRegionException; the seller picks the region and per-region dedup makes multi-region metering correct, not an error.BatchMeterUsage- Do NOT conflate this with AMI/Container . 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 returnsMeterUsage. 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/ContainerInvalidEndpointRegionException, NOT to SaaSMeterUsage. See the public AWS docs for AMI/Container metering.BatchMeterUsage
- 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 call with a REQUEST-level
BatchMeterUsage(case-sensitive, not billed) — it is NOT a per-recordInvalidUsageDimensionException. 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 itResults[].Status(reason = the exception name), and emits theRejectedClientSidemetric. Adding/renaming a catalog dimension needs no redeploy.BatchMeterUsageException - 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 itInvalidDimensionand emittingRejectedClientSide; (2) if a row reaches the API and is rejected there, the server-side equivalent is the matchingUsageRecordRejectedrequest-level exception (BatchMeterUsage,TimestampOutOfBoundsException,InvalidLicenseException,TooManyTagsException). 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:InvalidUsageDimensionException↔MalformedTimestamp;TimestampOutOfBoundsException↔InvalidLicenseArn;InvalidLicenseException↔TooManyTags;TooManyTagsException↔InvalidDimension.InvalidUsageDimensionException - = a PARTIAL-BATCH outcome, NOT "the call accepted the record." The
UnprocessedRecordsrequest did NOT fail completely: records that succeeded appear inBatchMeterUsage, records that failed to process appear inResults(not billed; typically TRANSIENT —UnprocessedRecords/ThrottlingException). They are safe to re-submit because metering is idempotent (first-write-wins dedup onInternalServiceException, 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 (CustomerAWSAccountId + LicenseArn + dimension + hour) — NOT a blind client-side retry loop; the persistent case is alarmed viarate(5m). This is DISTINCT from: per-recordUsageRecordUnprocessedverdicts (Results[].Status/Success/CustomerNotSubscribed— FINAL, do NOT blindly retry), and request-level exceptions (DuplicateRecord/InvalidUsageDimensionException/TimestampOutOfBoundsException) that fail the ENTIRE batch and require isolating the poison record by bisection.InvalidLicenseException - 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 — never CloudTrail Lake /
eventSource = metering-marketplace.amazonaws.comfor metering lookups.create-event-data-store - Timestamp window = 24 HOURS (not 1h), measured from ; 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
nowand one bad timestamp rejects the ENTIRE batch; before it ever reaches the API the pipeline's client-side guardrail rejects the row with reason codeTimestampOutOfBoundsException(markedMalformedTimestamp, 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 inRejectedClientSide(never the in-progress/current hour — that is only metered by a deprovisioning flush), oldest-first, gated bynow-23h … now-1h; a skipped/failed hourly-discoverer orMeteringLockHours-submitter run self-recovers on a later run while the hour is still in the 24h window. Detail:rate(5m).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 (, not billed). Write/submit usage promptly (well inside the window) rather than batching a day's worth at the edge.
TimestampOutOfBounds - 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 (
meteringStatusfor a raw row that never aggregated,AggregationExpiredfor an aggregated row never submitted) AND CLEARSSubmissionExpiredso the row drops off the sparsemeteringPendingGSI and the discoverer's oldest-first backward walk STOPS returning it (no infinite re-processing). These fire themetering_pending/UsageAggregationExpiredalarms — a paged signal that usage silently never metered; investigate the writer/pipeline delay.UsageSubmissionExpired - A ~1-hour-old timestamp should NOT normally get — 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.
TimestampOutOfBoundsException
- 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 (); the hour currently in progress stays open and is skipped until it completes.
now-23h … now-1h(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 (MeteringLockHours). The aggregator collapses eachTimestampOutOfBoundsgroup to exactly ONE(licenseArn, customerAWSAccountId, dimension, hour)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 insideBatchMeterUsage— expected, not a bug.MeteringLockHours - 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
BatchMeterUsagetag-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.)usageAllocations - VMT (vendor metered tagging) tag limit = 5 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 codeusageAllocations(markedTooManyTags, not billed, emitsRejectedClientSide); the server-side equivalent if such a row reached the API isUsageRecordRejected. When asked about the VMT tag cap, name BOTH the 5-tag maximum AND theTooManyTagsExceptionclient-side rejection (not just the number). VMT is presence-based — the seller supplies the tags; the pipeline never invents them.TooManyTags - VMT is ALL-OR-NOTHING per group — do NOT mix tagged and untagged rows. Within one group, either EVERY non-zero row carries
(licenseArn, customerAWSAccountId, dimension, hour)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 codeusageAllocations(MixedAllocatedAndUnallocated, 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'sRejectedClientSidevalues must sum to that row'sAllocatedUsageQuantity, and ≤2500 merged tag-sets per UsageRecord (elsequantity/AllocationSumMismatch). Detail:TooManyAllocations,references/setup-path.md.references/client-side-guardrails.md - Metering is idempotent (first-write-wins dedup per , 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
CustomerAWSAccountId + LicenseArn + dimension + hour) runsBatchMeterUsage— a single SERIAL submitter, deliberately serial to stay within theReservedConcurrentExecutions=1request-rate limit (documented quota: 10 requests/second per account per region;BatchMeterUsage10/s,MeterUsage5/s — see service quotas) and avoidRegisterUsage. The discoverer runs at reserved concurrency 30; the aggregator and cleanup scale via their SQS event-source-mappingThrottlingException. 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.MaximumConcurrency- handling — exponential backoff WITH JITTER. On
ThrottlingException(or a throttledThrottlingException), 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 asUnprocessedRecordswith 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).DuplicateRecord
- Registration form data (PII) lives in a per-Region table, NOT the subscribers table. The in-region register Lambda writes the allowlisted registration/KYB fields to
customer-profile(PKawsmp-<productCode>-customer-profile, SKlicenseArn, 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-PIIcustomerAWSAccountId+ an idempotentproductCodeappend (NO registration form data) — plus the CA identity (registeredRegions,licenseArn,customerAWSAccountId) 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'sagreementIdenv; 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.)CUSTOMER_PROFILE_TABLE - Legacy (pre-CA) integrations may use +
ProductCodeinstead ofCustomerIdentifier. The skill does NOT impose its own template/fallback on them (it must NOT present a fixedLicenseArnfallback as its template) — it grounds legacy behavior in the seller's EXISTING integration + public AWS docs (startswith("arn:")).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 (estimated revenue), and after conversion the same dimensions bill with no integration change.
$0 - 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 in UTC; the GSI is queried by UTC hour bucket, so a local-time write mis-files/mis-meters the row silently.
meteringPending - Audit timestamps (/
createdAt) — every row the pipeline writes carriesupdatedAt(write-once,createdAt) andif_not_exists(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 followupdatedAt/ thesubscriptionStatusGSI, nevermetering_pending/createdAt.updatedAt
- CustomerAWSAccountId(而非 CustomerIdentifier)+ 每个 UsageRecord 的 LicenseArn,用于新的(并发协议)产品。
- 订阅事件使用 EventBridge(而非 SNS)——仅在 us-east-1 发出。
- 在买家从 Marketplace 控制台重定向到 SaaS 注册站点时,从
ResolveCustomer解析买家的x-amzn-marketplace-token、LicenseArn和customerAWSAccountId;然后 SaaS 收集额外的注册数据并将其持久化(PII 字段)到区域内 customer-profile 表(无 PII 的订阅者表仅获得 CA 身份 + 状态 +productCode)。它返回顶级registeredRegions字段——LicenseArn是已弃用的旧字段,对于新的 SaaS 集成为 null(不要使用它,也不要将其值视为 LicenseArn)。它是区域性 API,但该区域是卖家的自由选择——不必与买家使用软件的位置匹配,也不限制计量。CustomerIdentifier可以在任何受支持的区域调用,独立于调用BatchMeterUsage的区域。它不是仅限 us-east-1 的 API(只有ResolveCustomer等 EventBridge 订阅事件是仅限 us-east-1 的)。必须从发布 SaaS 产品的账户调用(否则 token 将无法解析)。License Updated不是一次性的——它可重复使用直到过期(约 4 小时):在过期前重新解析同一 token 会返回相同结果,因此注册页面出错的买家可以在约 4 小时窗口内使用原始链接重试。过期后x-amzn-marketplace-token返回ResolveCustomer(已消费/无效 token 返回ExpiredTokenException),买家重新点击"Set up your account"以获取新的 token(无需重新订阅)。几天后重试 = token 过期 → 生成新的。(InvalidTokenException。)references/troubleshooting.md - DuplicateRecord = 首次写入优先并已报告(,而非
Status = DuplicateRecord);原始数量保持已计费。以不同数量重新提交不会被计费(少计风险)。仅对相同重试安全——绝不是更正数量的方法;绝不可称之为"良性"。Success - CustomerNotSubscribed = 真实失败,不要重试。原因:(1)标识符无效,(2)协议已结束/过期,(3)买家账户被暂停。
- 错误的 是请求级的
LicenseArn,会使整个InvalidLicenseException批次失败——不是BatchMeterUsage中的逐记录Results(不同于Status)。缓解措施:在批处理前于客户端进行 LicenseArn 形状验证(原因CustomerNotSubscribed),使格式错误的 ARN 永远不会进入批次;在请求级InvalidLicenseArn时二分并重试以隔离问题记录,使其同批次的记录仍然计量。ClientError - (SaaS)是区域性的,但卖家选择区域——没有
BatchMeterUsage。 在用量的区域调用它(卖家选择;本地部署/其他云选择最近的支持区域);每个计量区域部署一个堆栈;去重按区域进行。它在大多数商业区域受支持,但并非全部——在所选区域 API 不可用时调用返回InvalidEndpointRegionException(改为在受支持的区域计量)。SaaSDisabledApiException没有BatchMeterUsage;卖家选择区域,按区域去重使多区域计量正确,而非错误。InvalidEndpointRegionException- 不要将其与 AMI/Container 混淆。 计量 AMI/Container 产品超出此 skill 的范围(重定向到公开文档),其区域规则相反:
MeterUsage必须在运行中的 EC2 实例 / ECS 任务所在的同一区域调用——无论实例运行在哪里,都不能从 us-east-1(或任何其他区域)调用;区域不匹配返回MeterUsage。 此类产品使用 IAM 实例/任务角色(EC2 实例配置文件、ECS 任务角色或 EKS IRSA)进行认证,绝不使用嵌入式/长期访问密钥。此同区域强制 +InvalidEndpointRegionException仅适用于 AMI/ContainerInvalidEndpointRegionException,不适用于 SaaSMeterUsage。参见公开 AWS 文档了解 AMI/Container 计量。BatchMeterUsage
- 不要将其与 AMI/Container
- 维度有效性由服务端权威决定。 管道未配置维度列表,也不在客户端拒绝;未定义的维度会使 调用以请求级
BatchMeterUsage失败(区分大小写,不计费)——它不是逐记录InvalidUsageDimensionException。管道通过二分(深度限制为 ⌈log2(批次大小)⌉ = 5,因此大量失败的批次不会使提交器过载)隔离违规记录,将其标记为Results[].Status(原因 = 异常名称),并发出RejectedClientSide指标。添加/重命名目录维度无需重新部署。BatchMeterUsageException - 拒绝原因代码以客户端 ↔ 服务端成对出现——始终同时呈现两者。 一行可能以两种方式被拒绝,规则期望每个都有命名的代码:(1)管道的客户端护栏在 API 调用前拒绝该行并盖上客户端原因代码(、
MalformedTimestamp、InvalidLicenseArn、TooManyTags等),将其标记为InvalidDimension并发出RejectedClientSide;(2)如果一行到达 API 并在那里被拒绝,服务端对应物是匹配的UsageRecordRejected请求级异常(BatchMeterUsage、TimestampOutOfBoundsException、InvalidLicenseException、TooManyTagsException)。在解释任何拒绝时,同时说出客户端原因代码和其服务端异常对应物,而不仅仅是一个。客户端原因 ↔ 服务端异常:InvalidUsageDimensionException↔MalformedTimestamp;TimestampOutOfBoundsException↔InvalidLicenseArn;InvalidLicenseException↔TooManyTags;TooManyTagsException↔InvalidDimension。InvalidUsageDimensionException - = 部分批次结果,而非"调用接受了记录"。
UnprocessedRecords请求并未完全失败:成功的记录出现在BatchMeterUsage中,处理失败的记录出现在Results中(不计费;通常是暂时的——UnprocessedRecords/ThrottlingException)。它们可以安全地重新提交,因为计量是幂等的(按区域在InternalServiceException上首次写入优先去重)。已部署的提交器重试该记录一次;如果仍然失败,则留给重新处理,并在下次计划运行(CustomerAWSAccountId + LicenseArn + dimension + hour)时拾取——不是盲目的客户端重试循环;持久情况通过rate(5m)告警。这与以下内容不同:逐记录UsageRecordUnprocessed判定(Results[].Status/Success/CustomerNotSubscribed——最终的,不要盲目重试),以及请求级异常(DuplicateRecord/InvalidUsageDimensionException/TimestampOutOfBoundsException),后者使整个批次失败,需要通过二分隔离问题记录。InvalidLicenseException - CloudTrail 是区域性的——查询调用 BatchMeterUsage 的同一区域。约 15 分钟投递延迟。90 天保留(较旧的建议使用卖家报告)。使用 CloudTrail 事件历史(默认,无需设置),过滤 ——计量查找绝不使用 CloudTrail Lake /
eventSource = metering-marketplace.amazonaws.com。create-event-data-store - 时间戳窗口 = 24 小时(不是 1 小时),从 起算;时间戳超过 24 小时的记录超出窗口并被拒绝——不会被提交。 具体来说,30 小时前的记录已超过 24 小时边界,不在窗口内:服务端会以
now使调用失败,一个坏时间戳会拒绝整个批次;在到达 API 之前,管道的客户端护栏以原因代码TimestampOutOfBoundsException拒绝该行(标记为MalformedTimestamp,不计费),因此过期/超窗时间戳永远不会进入批次。不要告诉卖家 >24 小时(例如 30 小时)的记录"在窗口内"或"应该提交"——它会被拒绝;唯一的例外是 6 小时月边界宽限期,它接受上个月的记录直到 1 日 06:00 UTC。管道只计量RejectedClientSide中已完成的小时(绝不计量进行中/当前小时——那只能由取消供应刷新计量),最旧优先,由now-23h … now-1h门控;跳过/失败的小时发现器或MeteringLockHours提交器运行会在稍后运行中自恢复,只要该小时仍在 24 小时窗口内。详情:rate(5m)。references/scalable-metering-design.md- 最佳实践——在生成后约 1 小时内提交用量。 延迟提交是最大的真实世界失败模式:行越接近 24 小时边界,重试余量越少,超过 24 小时就永久丢失(,不计费)。及时写入/提交用量(在窗口内),而不是在边界批量提交一整天的量。
TimestampOutOfBounds - 老化是终态的并清除待处理标记。 当一行/聚合在其超过 24 小时窗口之前无法计量时,管道将其老化:它设置终态 (从未聚合的原始行为
meteringStatus,从未提交的聚合行为AggregationExpired)并清除SubmissionExpired,使该行从稀疏的meteringPendingGSI 中消失,发现器的最旧优先向后遍历停止返回它(不会无限重新处理)。这些触发metering_pending/UsageAggregationExpired告警——分页信号表明用量静默地从未计量;调查写入器/管道延迟。UsageSubmissionExpired - 约 1 小时前的时间戳通常不应得到 ——1 小时远在 24 小时回看窗口内,所以这很不寻常。不要告诉卖家"等到 HH:05"。真正原因几乎总是(a)一个坏记录使整个批次失败(同批次的格式错误/超窗/未定义维度记录使请求级异常拒绝调用中的所有记录,直到被隔离)或(b)时钟偏差(未同步的主机在未来/过去很远的时间戳,或使用本地时间而非 UTC)。修复:检查批次中的格式错误记录(二分隔离它)并验证写入器的时钟已 NTP 同步并标记整秒 UTC。
TimestampOutOfBoundsException
- 最佳实践——在生成后约 1 小时内提交用量。 延迟提交是最大的真实世界失败模式:行越接近 24 小时边界,重试余量越少,超过 24 小时就永久丢失(
- 当前(进行中)小时在正常路径上绝不计量。 管道只提交已完成的小时();当前进行中的小时保持开放,直到完成才被跳过。
now-23h … now-1h(默认 1,验证 1..20)是一个小时在聚合/提交前保持开放以接收延迟用量的小时数——默认 1 时,管道处理刚刚完成的小时。待处理的小时桶最旧优先排空,因此最旧的用量在接近 24 小时边界并老化(MeteringLockHours)之前被提交。聚合器将每个TimestampOutOfBounds组折叠为每小时恰好一条(licenseArn, customerAWSAccountId, dimension, hour)记录(计量按组按小时,首次写入优先)。唯一计量当前进行中小时的时间是取消供应的最终用量刷新(绕过锁,见事件模型)。所以"为什么这个小时还没有显示用量?" = 它仍然是当前小时和/或仍在BatchMeterUsage内——预期行为,不是 bug。MeteringLockHours - 限制是两个独立的上限:≤25 条 UsageRecord 且 ≤1 MB 请求负载。 包含 25 条有效记录的批次仍可能因大小而被拒绝,如果序列化请求超过 1 MB(大/多的
BatchMeterUsage标签集、长 ARN)——是字节大小,而非记录数。修复:每次调用放更少的记录,使每个请求保持在 1 MB 以下。(提交器已合并每调用 ≤25 条;大小拒绝意味着这些记录太大——进一步拆分。)usageAllocations - VMT(供应商计量标签)标签限制 = 每条 UsageRecord 5 个 标签。 用量行在其
usageAllocations中最多携带 5 个标签;超过 5 个无效。管道的客户端护栏在 API 调用前以原因代码usageAllocations拒绝超标签行(标记为TooManyTags,不计费,发出RejectedClientSide);如果这样的行到达 API,服务端对应物是UsageRecordRejected。当被问及 VMT 标签上限时,同时说出 5 个标签的最大值和TooManyTagsException客户端拒绝(不仅仅是数字)。VMT 基于存在性——卖家提供标签;管道绝不发明它们。TooManyTags - VMT 按组全有或全无——不要混合带标签和未标签的行。 在一个 组内,要么每条非零行都携带
(licenseArn, customerAWSAccountId, dimension, hour),要么都不携带。在同一组中混合有分配和无分配的非零行是不允许的——管道以原因代码usageAllocations在客户端拒绝它(MixedAllocatedAndUnallocated,不计费)。这不是"部分 VMT 归属"。零数量行对此检查被忽略,同一小时/批次中的不同组可以独立地有或无分配。另外按组:每行的RejectedClientSide值必须等于该行的AllocatedUsageQuantity,且每条 UsageRecord 合并的标签集 ≤2500(否则quantity/AllocationSumMismatch)。详情:TooManyAllocations、references/setup-path.md。references/client-side-guardrails.md - 计量是幂等的(按区域在 上首次写入优先去重),管道是解耦的——没有单一的计量 Lambda,也没有"预留并发 = 2"的设置。提交器(唯一调用
CustomerAWSAccountId + LicenseArn + dimension + hour的阶段)以BatchMeterUsage——单个串行提交器运行,故意串行以保持在ReservedConcurrentExecutions=1请求速率限制内(记录配额:每账户每区域 10 请求/秒;BatchMeterUsage10/s,MeterUsage5/s——参见服务配额)并避免RegisterUsage。发现器以预留并发 30 运行;聚合器和清理器通过其 SQS 事件源映射ThrottlingException扩展。SQS 吸收突发——吞吐量来自排空队列,而非提高提交器并发。如果卖家需要更高的持续计量 TPS,联系 AWS Marketplace 卖家运营,而不是将并发提高到安全水平以上。MaximumConcurrency- 处理——带抖动的指数退避。 在
ThrottlingException(或受限的ThrottlingException)时,使用带抖动的指数退避(随机化延迟——明确说出抖动;不要使用固定/同步的重试间隔,那会再次冲突)重试。保持每次调用在 ≤25 条记录(和 ≤1 MB) 限制内——不要增大批次来"追赶"。重试是安全的,因为计量是幂等的(首次写入优先):重新发送的已计费记录只会作为UnprocessedRecords返回,原始数量仍然计费。不要将DuplicateRecord视为避免重试受限/未处理记录的理由——正是幂等性保证使重试安全(唯一注意事项是绝不以不同数量重新提交同一组,那不会被计费)。DuplicateRecord
- 注册表单数据(PII)位于每区域 表中,而非订阅者表。 区域内的注册 Lambda 将允许列表的注册/KYB 字段写入
customer-profile(PKawsmp-<productCode>-customer-profile,SKlicenseArn,卖家请求的 GSI)在其运行的区域中,保持买家 PII 在区域内。配置文件行始终为已注册买家 upsert——即使零额外表单字段(按身份键控的行总是被写入)。统一的订阅者表(us-east-1)是无 PII 的——注册 Lambda 只写入非 PII 的customerAWSAccountId+ 幂等的productCode追加(无注册表单数据)——加上 CA 身份(registeredRegions、licenseArn、customerAWSAccountId)和两个生命周期状态——因此即使对于选择加入区域的产品,在 us-east-1 也是安全的。模板始终设置注册 Lambda 的agreementId环境变量;如果未设置,那是错误配置,处理器将大声失败(HTTP 500),而不是静默跳过 PII 写入。(现有卖家自己的注册处理器可能以不同方式存储数据——skill 会适应,不强制逐字使用此参考 Lambda。)CUSTOMER_PROFILE_TABLE - 旧版(CA 前)集成可能使用 +
ProductCode而非CustomerIdentifier。skill 不对它们强加自己的模板/回退(它不得呈现固定的LicenseArn回退作为其模板)——它以卖家现有集成 + 公开 AWS 文档为基础(startswith("arn:"))。references/existing-sellers.md - 免费试用——行为取决于哪个模型(不要给出笼统的"始终计量"答案);先确认模型,然后将细节基于公开文档(SaaS 免费试用、PLG 指南):
- AWS Marketplace SaaS 免费试用功能(原生、限时:买家订阅单独的试用优惠,必须在结束前升级):全程正常计量——不要扣留。AWS 应用试用定价,因此试用用量以 $0(估计收入)开票,转换后相同维度计费,无需集成更改。
$0 - 嵌入式免费层 / 按用量试用(买家在标准付费优惠上,卖家决定何时开始收费):计量什么/何时计量是卖家自己的定价决策——skill 不规定计量策略。对于纯按用量(PAYG),默认是计量所有可计费用量,让定价/优惠处理任何免费额度。仅对合同中包含数量之外计费是含消费合同(CCP)——超出范围(见下文)→ 公开文档。
- AWS Marketplace SaaS 免费试用功能(原生、限时:买家订阅单独的试用优惠,必须在结束前升级):全程正常计量——不要扣留。AWS 应用试用定价,因此试用用量以 $0(
- 用量摄取是卖家的责任。 卖家将行写入用量表(多账户时包括跨账户收集)。在默认的完整模式下,管道按小时聚合;在直接提交模式下,卖家提供最终的小时总计(见 Q9)。
- 所有时间戳均为 UTC——卖家以 UTC 标记排序键时间戳和 ;GSI 按 UTC 小时桶查询,因此本地时间写入会静默地错放/错计该行。
meteringPending - 审计时间戳(/
createdAt)——管道写入的每一行都携带updatedAt(只写一次,createdAt)和if_not_exists(每次写入刷新),ISO-8601 整秒 UTC,通过共享的戳记辅助函数统一应用。这些是仅审计元数据,不门控计量——计量决策遵循updatedAt/subscriptionStatusGSI,从不遵循metering_pending/createdAt。updatedAt
Concurrent Agreements (CA) — registration & metering model (recite when asked how CA works)
并发协议(CA)——注册与计量模型(在被问及 CA 如何工作时背诵)
All new SaaS products use Concurrent Agreements: identity is + (never the legacy ). Explain these facts explicitly whenever a CA registration, event-ordering, attribution, or lookup question comes up:
CustomerAWSAccountIdLicenseArnCustomerIdentifier- Registration → subscriber upsert is driven by the EventBridge 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 byLicense Updated+licenseArn, withcustomerAWSAccountId) settingagreementIdandagreementStatus=active— this is what makes the buyer eligible to meter.subscriptionStatus=active(called from the branded registration page) resolves the token toResolveCustomer+LicenseArn+customerAWSAccountIdand persists registration/PII to the in-region customer-profile table, but it is theproductCodeupsert (agreementStatus=active) that establishes the metering-eligible subscriber row. Do not describe CA registration without naming theLicense Updated→License Updatedupsert.agreementStatus=active - DOES return the
ResolveCustomer— the answer is YES.LicenseArnresolves theResolveCustomerand returns a top-levelx-amzn-marketplace-tokenfield, alongsideLicenseArnandcustomerAWSAccountId. Do NOT sayproductCodedoes not return a LicenseArn — it does, at the top level of the response.ResolveCustomeris 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 theCustomerIdentifierfromLicenseArnis the identifier you persist and later pass toResolveCustomer.)BatchMeterUsage - Eligibility is enforced at , independent of event arrival order. Metering eligibility is checked server-side by
BatchMeterUsageat 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 returnsBatchMeterUsage; onceCustomerNotSubscribedhas made the buyer active, the same record meters. So out-of-order or late-arriving events do not corrupt billing:License Updatedis the authority, and the row self-recovers on a laterBatchMeterUsagerun while still inside the 24h window. Do not claim the pipeline gates metering on event ordering — it gates on eligibility at submit time.rate(5m) - Metering is attributed PER LICENSE, not per account — reinforce the usage-table key model. The raw usage table is PK , SK
licenseArn. One AWS account (customerAWSAccountId#dimension#timestamp) can hold MULTIPLE agreements/licenses, and dedup + billing are scoped tocustomerAWSAccountId— so usage is aggregated and metered perCustomerAWSAccountId + LicenseArn + dimension + hour, 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).licenseArn - The GSI drives metering; the subscribers table is lifecycle-only. Discovery of what to meter comes ONLY from the sparse
metering_pendingGSI on the usage/aggregated tables (HASH =metering_pendinghour-bucket, RANGE =meteringPending, projectinglicenseArn+customerAWSAccountId). The discoverer PAGINATES through the GSI — it runs a paginateddimension(followingQueryacross 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 (LastEvaluatedKey/agreementStatus) 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.subscriptionStatus- Unified subscribers table schema (us-east-1, PII-FREE): PK , SK
licenseArn, with attributescustomerAWSAccountId,agreementId,agreementStatus,subscriptionStatus,productCode,registeredRegions(set only during a deprovisioning flush), anddeprovisioningExpiry/createdAtaudit stamps.updatedAtis a set updated idempotently (append viaregisteredRegions, no duplicates) and is reference-only — it NEVER gates metering. It carries NO registration form data / NO PII (PII lives only in the in-regionADDtable). Acustomer-profileGSI supports account-scoped lookups. WhycustomerAWSAccountIdis the PK (all THREE reasons): (1) under Concurrent Agreements one buyer can hold MULTIPLE concurrent licenses for the same product; (2) usinglicenseArnas 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), so per-agreement keying keeps them isolated. Name all three when asked whyCustomerAWSAccountId + LicenseArn + dimension + houris the partition key.licenseArn
- Unified subscribers table schema (us-east-1, PII-FREE): PK
- ONE PRODUCT PER CALL — do NOT mix products in one batch. Every
BatchMeterUsage(everyUsageRecord) in a singleLicenseArnrequest MUST belong to the SAME product. MixingBatchMeterUsages from two different products in one call returns a request-levelLicenseArnand fails the ENTIRE request (nothing meters). Make a SEPARATEInvalidLicenseExceptioncall per product. Within that single-product batch you MAY combine different dimensions and different buyers/licenses of that one product; dedup remains perBatchMeterUsage, 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.)CustomerAWSAccountId + LicenseArn + dimension + hour - 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 (transient
UnprocessedRecords/ThrottlingException, 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.InternalServiceException - 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, 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 +License Deprovisioned) 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.BatchMeterUsage
所有新的 SaaS 产品使用并发协议:身份是 + (绝不是旧的 )。每当出现 CA 注册、事件排序、归属或查找问题时,明确说明这些事实:
CustomerAWSAccountIdLicenseArnCustomerIdentifier- 注册 → 订阅者 upsert 由 EventBridge 事件驱动。 当买家订阅时,AWS Marketplace 发出
License Updated(仅 us-east-1)。事件堆栈订阅 Lambda 消费它并 UPSERT 订阅者记录(按License Updated+licenseArn键控,带customerAWSAccountId),设置agreementId和agreementStatus=active——这使买家有资格计量。subscriptionStatus=active(从品牌注册页面调用)将 token 解析为ResolveCustomer+LicenseArn+customerAWSAccountId,并将注册/PII 持久化到区域内的客户配置文件表,但正是productCodeupsert(agreementStatus=active) 建立了有计量资格的订阅者行。描述 CA 注册时,必须说出License Updated→License Updatedupsert。agreementStatus=active - 确实返回
ResolveCustomer——答案是肯定的。LicenseArn解析ResolveCustomer并返回顶级x-amzn-marketplace-token字段,以及LicenseArn和customerAWSAccountId。不要说productCode不返回 LicenseArn——它确实返回,在响应的顶级。ResolveCustomer是已弃用的旧字段,对于新的 CA 集成为 null——不要使用它或将其值视为 LicenseArn。(对于新的 SaaS 产品,来自CustomerIdentifier的ResolveCustomer是您持久化并稍后传递给LicenseArn的标识符。)BatchMeterUsage - 资格在 处强制执行,独立于事件到达顺序。 计量资格由
BatchMeterUsage在提交时根据当前协议/订阅状态在服务端检查——它不依赖于 EventBridge 事件到达的顺序。如果在订阅者激活之前(或协议结束后)提交用量,记录返回BatchMeterUsage;一旦CustomerNotSubscribed使买家激活,相同的记录就会计量。因此乱序或延迟到达的事件不会破坏计费:License Updated是权威,行在仍在 24 小时窗口内的稍后BatchMeterUsage运行中自恢复。不要说管道以事件顺序门控计量——它在提交时以资格门控。rate(5m) - 计量按许可证归属,而非按账户——强化用量表键模型。原始用量表为 PK ,SK
licenseArn。一个 AWS 账户(customerAWSAccountId#dimension#timestamp)可以持有多个协议/许可证,去重和计费范围是customerAWSAccountId——因此用量按CustomerAWSAccountId + LicenseArn + dimension + hour聚合和计量,绝不折叠为单个按账户总计。当被问"按账户还是按许可证计量?"答案是按许可证(licenseArn是分区键)。licenseArn - GSI 驱动计量;订阅者表仅用于生命周期。 发现要计量的内容仅来自用量/聚合表上的稀疏
metering_pendingGSI(HASH =metering_pending小时桶,RANGE =meteringPending,投影licenseArn+customerAWSAccountId)。发现器分页遍历 GSI——它运行分页的dimension(跨页跟随Query),而非单次读取——因此超过一页的待处理组小时也会完全排空。不查询订阅者表来查找要计量的用量——它只持有 CA 身份 + 两个生命周期状态(LastEvaluatedKey/agreementStatus),用于生命周期/取消供应(此许可证是否激活?是否已进入取消供应刷新?)。不要说管道扫描或查询订阅者表来定位待处理用量——那是 GSI 的工作。subscriptionStatus- 统一订阅者表模式(us-east-1,无 PII): PK ,SK
licenseArn,属性customerAWSAccountId、agreementId、agreementStatus、subscriptionStatus、productCode、registeredRegions(仅在取消供应刷新期间设置)以及deprovisioningExpiry/createdAt审计戳记。updatedAt是一个幂等更新的集合(通过registeredRegions追加,无重复),且仅参考——绝不门控计量。它不携带注册表单数据 / 无 PII(PII 仅在区域内的ADD表中)。customer-profileGSI 支持账户范围查找。为什么customerAWSAccountId是 PK(所有三个原因):(1)在并发协议下,一个买家可以为同一产品持有多个并发许可证;(2)使用licenseArn作为 PK 会将那些不同的协议折叠为一行(丢失用量 / 双重计费);(3)每个许可证有自己的独立生命周期和计量幂等性(去重按customerAWSAccountId),因此按协议键控保持它们隔离。被问为什么CustomerAWSAccountId + LicenseArn + dimension + hour是分区键时说出所有三个。licenseArn
- 统一订阅者表模式(us-east-1,无 PII): PK
- 一次 调用只针对一个产品——不要在一个批次中混合产品。 单个
BatchMeterUsage请求中的每条BatchMeterUsage(每个UsageRecord)必须属于同一产品。在一次调用中混合来自两个不同产品的LicenseArn会返回请求级LicenseArn并使整个请求失败(什么都不计量)。每个产品单独调用InvalidLicenseException。在该单产品批次中,您可以组合该产品的不同维度和不同买家/许可证;去重仍按BatchMeterUsage,≤25 条记录和 ≤1 MB 上限适用于批次。(已部署的提交器已按产品分组,因此它从不跨产品批处理;手动批处理的卖家必须同样操作。)CustomerAWSAccountId + LicenseArn + dimension + hour - 批次上限 = ≤25 条 UsageRecord 且 ≤1 MB(两个独立限制)——见关键事实条目;25 条记录的批次仍可能因 1 MB 负载大小而被拒绝。因此 N 个许可证 × M 个维度 = N·M 条记录 ⇒ ⌈N·M/25⌉ 次顺序调用(例如 50 个许可证 × 2 个维度 = 100 条记录 ⇒ 4 次调用,串行提交)。重试行为: 中返回的任何记录(暂时的
UnprocessedRecords/ThrottlingException,不计费)都会被重试——已部署的提交器重试一次,然后在下次计划运行中重新驱动——因为计量是幂等的(首次写入优先);不要丢弃它们。InternalServiceException - 事件堆栈(EventBridge + 订阅者表)仅在 us-east-1——计量(主)堆栈可以位于另一区域。 Marketplace 订阅事件(、
License Updated等)仅在 us-east-1 发出,因此共享事件堆栈及其无 PII 订阅者表固定在 us-east-1(与消费者订阅 Lambda 同区域)。但每区域主/计量堆栈(用量表 + 管道 Lambda +License Deprovisioned)部署在卖家计量的任何区域——只有事件+订阅者堆栈固定在 us-east-1。当计量区域 ≠ us-east-1 时,管道跨区域读取订阅者表;如果卖家也在 us-east-1 计量,则一切都在单区域。BatchMeterUsage
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 tool with AWS CLI over MCP
shellfor long-running ops.use_aws - 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".
- 首先检查范围外——如果超出范围,用一句话拒绝;不要继续。
- 知识问题(如何做、X 如何工作、哪个数据源):从本 skill 回答;不要调用 AWS API/工具。
- 操作问题(立即部署、立即查询、运行此操作):执行 AWS 命令——对于长时间运行的操作,优先使用带 AWS CLI 的 工具而非 MCP
shell。use_aws - 多区域/多产品问题是知识——解释架构;除非明确要求,否则不要部署。
- "我如何找到 X" = 知识——解释方法;仅在"给我看"/"运行它"时执行。
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 + -then-
GetEntitlement-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-firesBatchMeterUsage/ 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."License Updated - 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 + / 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."GetEntitlements - 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.
仅回复"This is outside the scope of AWS Marketplace metering" + 一行重定向;不要调用工具或加载参考:
- 实物履约 / FBA / 零售店面 → "Try Amazon Seller Central"
- 个人 AWS 账单 / 成本管理 → "Try AWS Budgets or Cost Explorer"
- 产品列表/优惠创建、Private Offers、优惠/折扣管理 → "Try AWS Marketplace Management Portal"
- 定价、合同、EULA 管理 → "Try Marketplace Catalog API docs"
- SaaS Contract / 含消费定价合同(CCP)——包含单位、超额计费以及合同生命周期(期限、续订、到期)——超出此按用量(PAYG)skill 的范围。这涵盖两者:(a)包含 N 单位/周期的合同以及在其之外计费用量(例如"每月 500 个包含 API 调用,买家使用 750 → 250 超额"),即 CCP / SaaS Contract + -then-
GetEntitlement-超额;以及(b)任何合同期限或续订问题(例如"我客户的年度合同下周续订——我需要更改计量设置吗?")。合同期限/续订是 SaaS Contract(权利) 关注点,而非 PAYG 计量关注点。不要从计量知识回答这些问题——特别是不要告诉卖家续订只是重新触发BatchMeterUsage/ 不需要计量更改,不要走 CCP 超额流程,也不要描述权利检查。该 skill 不拥有合同/权利行为,不得断言合同续订如何映射到计量事件。仅回复重定向:"SaaS Contract / 含消费定价合同——包括合同条款、续订、包含单位和超额——超出此按用量(PAYG)计量 skill 的范围。使用公开 AWS 文档:合同定价、含消费合同定价(CCP)、SaaS 合同集成 以及 通过 GetEntitlements 的权利 + 续订。"License Updated - 固定费率 / 无用量计量的 SaaS 订阅产品——验证客户是否仍然活跃/有权利(例如"固定月度订阅,无计量,如何检查客户是否仍然活跃?"):这是订阅权利管理(SaaS 订阅 + / 权利 EventBridge 事件),而非按用量(PAYG)计量。它超出范围——不要设计活跃客户检查、描述
GetEntitlements/权利事件处理,或为此重用本 skill 的计量订阅者表。仅回复重定向:"无用量计量的固定费率 SaaS 订阅超出此按用量(PAYG)计量 skill 的范围。验证活跃/有权利客户通过 AWS Marketplace SaaS 权利完成——参见公开 AWS 文档:SaaS 订阅、使用 GetEntitlements 检查权利 以及 使用 EventBridge 的订阅生命周期事件。"GetEntitlements - 预测 / 预估未来账单或用量(例如"根据我的 CloudTrail 计量趋势预测下个月的客户账单"、趋势投影、收入预测):超出范围——此 skill 仅查询历史提交(≤90 天 CloudTrail)和已计费历史(卖家报告);它不预测 → "预测未来账单/用量超出范围。此 skill 仅查询历史计量提交;对于预测,将获取的 CloudTrail/卖家报告数据交给您的主机 Agent/模型进行分析,或使用 AWS 成本预测 / 您自己对卖家报告的分析。"
- AWS 账户账单争议 → "Contact AWS Support"
- 任何不涉及 BatchMeterUsage、ResolveCustomer、计量记录或 SaaS/软件的按用量计费的内容。
SETUP PATH — Deploy New Metering Integration
设置路径——部署新的计量集成
Full walkthrough: + .
references/setup-path.mdreferences/deployment-steps.md完整演练: + 。
references/setup-path.mdreferences/deployment-steps.mdSeller 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 () 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
describe-entity(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 runtimeDescribeEntity(an undefined/misspelled/wrong-case dimension fails the wholeInvalidUsageDimensionExceptioncall). This is a validation aid only — it does NOT make the dimension list a deploy input.BatchMeterUsage - 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 , scoped to the submission region — do NOT funnel all usage into one region. Region support.
CustomerAWSAccountId + LicenseArn + dimension + hour - Q4 Stage name (MANDATORY). API Gateway stage (e.g. /
v1/live— seller's choice). No default, no fallback toprod; deploy fails fast if empty. Pass viaprod.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, 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 (elseMeteringLockHours).DuplicateRecord - Q7 TTL / retention (per table, env vars;
deploy.shdisables):0- Raw usage table (default 365) — RECOMMEND on (high-volume table). The seller's writer sets the
USAGE_TABLE_TTL_DAYSepoch attribute; the template only makes DynamoDB honor it. Floor ≥2 days that must exceedttl+24h so a row can't expire before metering.MeteringLockHours - Aggregated table (default 0 = retain) — RECOMMEND omit/large (small billing audit trail). When >0 the submitter sets
AGGREGATED_USAGE_TTL_DAYSonly onttlrows.Success - Enabling TTL needs the deployer's (already in the policy).
dynamodb:UpdateTimeToLive
- Raw usage table
- Q8 Create health dashboards? (, default true). Per-product main-stack dashboard (per-Lambda Errors/Throttles/Invocations/Duration; queue/DLQ depth + oldest-age; EMF
CREATE_DASHBOARDby reason,UsageRecordRejected,UsageRecordUnprocessed,CustomerNotSubscribedby exception,BatchMeterUsageException,UsageAggregationExpired,UsageSubmissionExpired) + a shared events-stack dashboard. Health-only; reflects the deployed mode (Q9); sellers can opt out / customize.DuplicateRecord - Q9 Full pipeline vs direct-submit (, default
DEPLOYMENT_MODE) — explain both so the seller chooses knowingly:full- full — seller emits RAW per-second events; the skill deploys the raw table + discoverer→aggregator→cleanup→submitter→submission-expiry, and the pipeline aggregates each
usagegroup to one(licenseArn, account, dimension, hour)record/hour. Best when the seller doesn't already aggregate or wants platform-owned aggregation + client-side rejection visibility.BatchMeterUsage - direct-submit — seller ALREADY produces finalized hourly totals; the skill deploys ONLY + submitter + submission-expiry + register/subscription (smaller/cheaper). Seller writes finalized records straight to
aggregated_usage(PKaggregated_usage, SKlicenseArn,customerAWSAccountId#dimension#hour,quantity=hour bucket).meteringPending - Finalize-before-insert (direct-submit, critical): any record in MAY be submitted on the next 5-min run — write a record ONLY when FINAL for its group, because a second write returns
aggregated_usageand the later quantity is NOT billed. The seller owns upstream aggregation/idempotency here.DuplicateRecord - Not a one-way door: the seller can switch modes later (a stack UPDATE that adds/removes mode-specific resources; raw table is
usage). Call out the blast radius before flipping a live stack.Retain
- full — seller emits RAW per-second events; the skill deploys the raw
- Q10 Registration fields + profile lookups () — EXPLAIN first, then ask. The in-region register Lambda persists ONLY these allowlisted fields (each length-bounded; the token is never stored) as a
ALLOWED_REGISTRATION_FIELDSmap on the in-regionregistrationDatatable (PKcustomer-profile, SKlicenseArn) — 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. bycustomerAWSAccountId) — 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. Mappingemail→the seller's internal SaaS tenant ID is the SELLER's responsibility — anchor it on the subscribers table (licenseArnfromlicenseArn, PII-free) and/or the customer-profile table (License Updatedcaptured at registration); add alicenseArnattribute + GSI if lookup-by-tenant is needed (tenantId).references/architecture.md
Product Type Gate: after credentials, verify via the Catalog API the product is SaaS + usage-based (). AMI/Container/SaaS-Contract-only → out of scope, redirect to public docs. Concurrent Agreements is the silent default — all new products useExternallyMetered+CustomerAWSAccountId; do NOT ask. A pre-existing legacy stack may useLicenseArn+ProductCode— understand it before changing (CustomerIdentifier).references/existing-sellers.md
- Q1 产品代码(或产品 ID)。 接受任一;在验证时通过 Catalog API()将产品 ID 解析为代码。维度列表不是部署输入(维度有效性由服务端权威决定);您可以仅为了卖家信息列出维度。建议在卖家将维度名称接入用量行之前,使用 Marketplace Catalog API
describe-entity(us-east-1)对照目录验证维度名称——返回的实体详细信息枚举了产品注册的维度 API 名称(区分大小写)。在设置时确认卖家的维度字符串与目录完全匹配,是避免运行时DescribeEntity(未定义/拼写错误/大小写错误的维度会使整个InvalidUsageDimensionException调用失败)的方法。这只是验证辅助——不使维度列表成为部署输入。BatchMeterUsage - Q2 您是 ISV/卖家吗? 只有卖家(发布者)或授权的 3PI 可以集成;3PI 超出范围——拒绝。确认发布账户访问权限。
- Q3 您从哪些区域计量? 事件堆栈始终是 us-east-1;在发生用量的每个区域部署主堆栈并在区域内计量。去重按 ,范围是提交区域——不要将所有用量集中到一个区域。区域支持。
CustomerAWSAccountId + LicenseArn + dimension + hour - Q4 阶段名称(强制)。 API Gateway 阶段(例如 /
v1/live——卖家选择)。无默认值,不回退到prod;为空时部署快速失败。通过prod传递。STAGE_NAME - Q5 此产品是否已存在堆栈? 如果是 → 修改/扩展:在更改任何内容之前收集上下文并检查卖家现有堆栈(可能使用任意命名)——参见 。
references/existing-sellers.md - Q6 计量锁定时长(→
METERING_LOCK_HOURS,默认 1,验证 1..20)。一个小时的用量在聚合/提交前保持开放以接收延迟用量的小时数(默认 1 = 处理刚刚完成的小时)。在上限时,一个小时后在年龄 20 时首次提交,在 24 小时可计费窗口中留下 ≥4 小时用于重试/重新驱动。月份感知: 上个月的小时保持锁定直到 1 日 03:00 UTC,然后无锁(每次运行提交)直到 06:00 UTC 截止,在此之前不会老化;03:00 之后写入的上个月记录必须已预先聚合(否则MeteringLockHours)。DuplicateRecord - Q7 TTL / 保留(每表,环境变量;
deploy.sh禁用):0- 原始用量表 (默认 365)——建议开启(高容量表)。卖家的写入器设置
USAGE_TABLE_TTL_DAYSepoch 属性;模板只让 DynamoDB 尊重它。下限 ≥2 天,必须超过ttl+24h,使行不会在计量前过期。MeteringLockHours - 聚合表 (默认 0 = 保留)——建议省略/较大(很小的计费审计跟踪)。当 >0 时,提交器仅在
AGGREGATED_USAGE_TTL_DAYS行上设置Success。ttl - 启用 TTL 需要部署者的 (已在策略中)。
dynamodb:UpdateTimeToLive
- 原始用量表
- Q8 创建健康仪表板?(,默认 true)。每产品主堆栈仪表板(每 Lambda 的 Errors/Throttles/Invocations/Duration;队列/DLQ 深度 + 最旧年龄;按原因的 EMF
CREATE_DASHBOARD、UsageRecordRejected、UsageRecordUnprocessed、按异常的CustomerNotSubscribed、BatchMeterUsageException、UsageAggregationExpired、UsageSubmissionExpired)+ 共享事件堆栈仪表板。仅健康;反映部署模式(Q9);卖家可选择退出/自定义。DuplicateRecord - Q9 完整管道 vs 直接提交(,默认
DEPLOYMENT_MODE)——解释两者,让卖家知情选择:full- full——卖家发出原始的每秒事件;skill 部署原始 表 + 发现器→聚合器→清理器→提交器→提交过期,管道将每个
usage组聚合为每小时一条(licenseArn, account, dimension, hour)记录。最适合卖家尚未聚合或希望平台拥有聚合 + 客户端拒绝可见性时。BatchMeterUsage - direct-submit——卖家已经产生最终的小时总计;skill 仅部署 + 提交器 + 提交过期 + 注册/订阅(更小/更便宜)。卖家将最终记录直接写入
aggregated_usage(PKaggregated_usage,SKlicenseArn,customerAWSAccountId#dimension#hour,quantity=小时桶)。meteringPending - 插入前完成(direct-submit,关键): 中的任何记录可能在下一次 5 分钟运行时被提交——仅当记录对其组为最终时才写入,因为第二次写入返回
aggregated_usage且后一个数量不被计费。卖家在这里拥有上游聚合/幂等性。DuplicateRecord - 不是单行道: 卖家可以稍后切换模式(堆栈 UPDATE 添加/删除模式特定资源;原始 表为
usage)。在翻转实时堆栈之前指出爆炸半径。Retain
- full——卖家发出原始的每秒事件;skill 部署原始
- Q10 注册字段 + 配置文件查找()——先解释,然后询问。区域内的注册 Lambda 仅持久化这些允许列表字段(每个长度受限;token 从不存储)作为区域**
ALLOWED_REGISTRATION_FIELDS** 表(PKcustomer-profile,SKlicenseArn)上的customerAWSAccountId映射——买家 PII 留在区域内,且不在无 PII 的 us-east-1 订阅者表中。映射中的字段可通过基础键检索,但不能独立查询。询问:(a)捕获哪些表单字段(包括任何 KYB 字段——姓名/电子邮件/公司/用例);(b)是否有任何字段必须直接查找(例如按registrationData)——如果是,则提升为顶级属性 + GSI(询问每个提升字段是 HASH-only 还是 HASH+RANGE)。保持字段可选/卖家确认;不发明 PII;在日志中掩码 PII。将email映射到卖家内部 SaaS 租户 ID 是卖家的责任——锚定在订阅者表(来自licenseArn的License Updated,无 PII)和/或客户配置文件表(注册时捕获的licenseArn);如果需要按租户查找,添加licenseArn属性 + GSI(tenantId)。references/architecture.md
产品类型门: 获得凭证后,通过 Catalog API 验证产品是 SaaS + 按用量计费()。AMI/Container/SaaS-合同-only → 超出范围,重定向到公开文档。 并发协议是静默默认——所有新产品使用ExternallyMetered+CustomerAWSAccountId;不要询问。预先存在的旧版堆栈可能使用LicenseArn+ProductCode——在更改之前理解它(CustomerIdentifier)。references/existing-sellers.md
Setup ordering (do NOT skip or reorder)
设置顺序(不要跳过或重新排序)
- Obtain ephemeral credentials (SSO → ; never long-lived keys; don't ask for keys first).
sts assume-role - Product type gate (SaaS + ExternallyMetered).
- Product visibility gate — deploy/mutate the product-specific MAIN stack ONLY when the CURRENT product is , never
Limited(provide code for the seller's own pipeline instead). Direct-deploy the main stack only when the current product isPublishedAND 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, possiblyLimited, products with live traffic — reuse its outputs); only the first onboarding creates it. SeePublished.references/existing-sellers.md - Confirm the questionnaire answers (incl. stage name).
- Only THEN generate + deploy: materialize the template(s) + Lambda handlers + in the seller's workspace and deploy THOSE on-disk files (
deploy.sh+sam build --template-file) — 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 onlysam deploysections).TODO: - 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 ) products — reuse it. Direct auto-deploy is reserved for a
Publishedproduct with NO existing stack. SeeLimited.references/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 () — verifying every expected observable outcome — and SHALL state that the integration should not carry PRODUCTION billing traffic until that review + testing passes.
references/test-plan.md
A failure at any gate stops the workflow before deployment.
- 获取临时凭证(SSO → ;绝不使用长期密钥;不要先要密钥)。
sts assume-role - 产品类型门(SaaS + ExternallyMetered)。
- 产品可见性门——仅当当前产品为 、绝不
Limited时部署/变更产品特定的主堆栈(为卖家自己的管道提供代码)。仅当当前产品为Published且没有现有的每产品堆栈(现有的为修改/扩展,卖家确认)时直接部署主堆栈。共享事件堆栈在已存在时绝不重新部署/变更(它可能服务其他、可能Limited、有实时流量的产品;重用其输出);只有首次 onboarding 创建它。参见Published。references/existing-sellers.md - 确认问卷答案(包括阶段名称)。
- 然后才生成 + 部署:在卖家工作区中物化模板 + Lambda 处理器 + 并部署这些磁盘上的文件(
deploy.sh+sam build --template-file)——绝不要内联/临时模板。修改/扩展就地编辑卖家现有文件(合并环境变量)。之后将所有工件留在工作区(代码/堆栈一致性;卖家可以版本控制/重新部署/开发,只自定义sam deploy部分)。TODO: - 现有堆栈护栏(强制执行,不要绕过)。 当此范围的堆栈已存在时,skill 不得自动应用更改:它应当(a)呈现确切的 diff/变更集供卖家审查,(b)在应用前要求卖家明确确认及其正常的代码审查 + 变更管理流程,(c)将更改范围限定为当前产品的主堆栈,以及(d)绝不重新部署/变更已服务其他(可能 )产品的共享事件堆栈——重用它。直接自动部署仅保留给没有现有堆栈的
Published产品。参见Limited。references/existing-sellers.md - 集成后,运行完整测试计划。 在设置/修改结束时,skill 应当推荐并(在可能的地方)引导卖家完成完整的动手测试计划()——验证每个预期的可观察结果——并应说明在审查 + 测试通过之前,集成不应承载生产计费流量。
references/test-plan.md
任何门失败都会在部署前停止工作流。
Stack naming
堆栈命名
| Component | Pattern | Example |
|---|---|---|
| Events stack | | Shared, us-east-1 |
| Main stack | | |
| Events stack (staged) | | |
| Main stack (staged) | | |
For a non-production sandbox, deploy stage-scoped stacks (the stage folds into every resource name) and set so the submitter never calls the real — use test accounts only. See / the setup path.
MeteringMode=dry-runBatchMeterUsagereferences/existing-sellers.md| 组件 | 模式 | 示例 |
|---|---|---|
| 事件堆栈 | | 共享,us-east-1 |
| 主堆栈 | | |
| 事件堆栈(分阶段) | | |
| 主堆栈(分阶段) | | |
对于非生产沙盒,部署阶段范围的堆栈(阶段折叠到每个资源名称中)并设置 ,使提交器从不调用真实的 ——仅使用测试账户。参见 / 设置路径。
MeteringMode=dry-runBatchMeterUsagereferences/existing-sellers.mdSupported 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 pergroup. 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:(licenseArn, account, dimension). GSI-driven discovery (never a Scan): the discoverer finds pending usage via the sparsereferences/scalable-metering-design.mdGSI — HASH =metering_pendinghour-bucket (meteringPending), RANGE =YYYY-MM-DDTHH, projectinglicenseArn+customerAWSAccountId(INCLUDE/ALL, NOTdimension) so a query returns theKEYS_ONLYtuples directly — no follow-up read to discover the group. Each group is then read with a targeted(licenseArn, customerAWSAccountId, dimension)sort-key prefix (begins_with), NOT a fullbegins_with(customerAWSAccountId#dimension#timestamp, "{account}#{dimension}#{hourPrefix}")-partition query + timestamplicenseArn.FilterExpression
SaaS 用量(PAYG)✅ | 并发协议 ✅ | VMT ✅ | 多区域 ✅ | 多产品 ✅
超出范围(→ 公开文档):CCP、AMI/Container、SaaS-合同-only(GetEntitlements)、3PI。
规模: 管道是解耦的(发现器 → 工作队列 → 聚合器 → 聚合用量 + 清理队列 → 提交器),因此设计为每小时处理数千条计量记录。秒级精度行将组限制为 ≤3600 行/小时 → 支持每组 ≤约 1 写/秒的高容量。对于更精细/更高的速率,推荐流式摄取层(例如 Kinesis Data Streams),它预聚合为每秒/每小时行(公开知识/主机模型确定,不是本 skill 的一等公民)。完整设计:(licenseArn, account, dimension)。 GSI 驱动发现(绝不用 Scan): 发现器通过稀疏references/scalable-metering-design.mdGSI——HASH =metering_pending小时桶(meteringPending),RANGE =YYYY-MM-DDTHH找到待处理用量,投影licenseArn+customerAWSAccountId(INCLUDE/ALL,不是dimension),因此查询直接返回KEYS_ONLY元组——无需后续读取来发现组。然后每个组使用目标化的(licenseArn, customerAWSAccountId, dimension)排序键前缀(begins_with)读取,而不是完整的begins_with(customerAWSAccountId#dimension#timestamp, "{account}#{dimension}#{hourPrefix}")分区查询 + 时间戳licenseArn。FilterExpression
Deployment (summary; full steps in references/deployment-steps.md
)
references/deployment-steps.md部署(摘要;完整步骤在 references/deployment-steps.md
)
references/deployment-steps.md- Generate SAM template + Lambda code (fill s).
TODO: - 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 ), NOT the raw
RegistrationUrl(smoke-test fallback only) — manual seller step. SeeRegistrationUrl.references/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, state the seller's ongoing responsibilities and guide the hands-on test (): the stacks do nothing until the seller writes conforming usage rows — whole-second sort key (matching segments) and a bucket that equals the row's own UTC timestamp hour. A mis-stamped 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 ( by reason, by exception, , /); catalog dimension changes need no redeploy. Several results are async (post-subscribe → /; hourly discoverer + submitter/expiry) — poll with bounded backoff, or manually invoke a Lambda to exercise a path immediately (say which). Recommend running the FULL test plan () 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).
references/test-plan.mdcustomerAWSAccountId#dimension#timestampmeteringPendingmeteringPendingUsageRecordRejectedBatchMeterUsageExceptionCustomerNotSubscribedUsageAggregationExpiredUsageSubmissionExpiredLicense UpdatedlicenseArnsubscriptionStatusrate(5m)references/test-plan.mdPrerequisites, credentials, and the full least-privilege deployer policy: .
references/iam-credentials.md- 生成 SAM 模板 + Lambda 代码(填写 标记)。
TODO: - 部署事件堆栈(us-east-1)——共享 EventBridge + 订阅者表。
- 部署主堆栈(卖家区域)——Lambda + DynamoDB + API Gateway + 告警。
- 将 AMMP Fulfillment URL 设置为品牌注册页面(S3+CloudFront 前端,捕获买家/KYB 字段并调用 ),而不是原始的
RegistrationUrl(仅冒烟测试回退)——手动卖家步骤。参见RegistrationUrl。references/registration-page.md - 端到端测试(订阅 → 解析 → 写入合规用量行 → 发现器 → 聚合器 → 提交器 → 验证 +
Aggregated)。Success - 通过 CloudTrail 日志验证。
部署后,说明卖家持续的责任并引导动手测试():在卖家写入合规用量行之前,堆栈什么都不做——整秒 排序键(匹配段)和 等于行自身 UTC 时间戳小时的 桶。错误标记的 是管道无法检测的唯一写入器错误(静默地从未计量)——通过测试确认。另外:为告警订阅通知并观察仪表板(按原因的 、按异常的 、、/);目录维度更改无需重新部署。几个结果是异步的(订阅后 → /;每小时发现器 + 提交器/过期)——使用有界退避轮询,或手动调用 Lambda 立即可执行路径(说明哪个)。建议在集成结束时端到端运行完整测试计划()并验证每个预期的可观察结果——加上对所有创建/更改资源的审查——在集成承载生产计费流量之前(AI 生成输出;参见上面的生产前审查提示)。
references/test-plan.mdcustomerAWSAccountId#dimension#timestampmeteringPendingmeteringPendingUsageRecordRejectedBatchMeterUsageExceptionCustomerNotSubscribedUsageAggregationExpiredUsageSubmissionExpiredLicense UpdatedlicenseArnsubscriptionStatusrate(5m)references/test-plan.md先决条件、凭证和完整最小权限部署者策略:。
references/iam-credentials.mdEvent & status model (recite when asked how cancellation/metering works)
事件与状态模型(在被问及取消/计量如何工作时背诵)
Two INDEPENDENT status fields on the subscriber row (never conflate):
- =
agreementStatus/active(agreement lifecycle).inactive - =
subscriptionStatus/active/deprovisioning(license lifecycle; there is noinactivevalue).deprovisioned
Metering follows the LICENSE lifecycle only:
- → agreement+subscription
License Updated(metering allowed).active - →
License Deprovisioned+ asubscriptionStatus=deprovisioning(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, ignoringdeprovisioningExpiryand the normal completed-hours-only gate — it does NOT wait forMeteringLockHoursto elapse and it DOES meter the current in-progress hour (the one hour the normal path always skips). Concrete mechanism/marker: theMeteringLockHourshandler setsLicense Deprovisioned+subscriptionStatus=deprovisioningon that subscriber row; the discoverer runs in a flush-deprovisioning mode that keys off that marker and enqueues the pending buckets (overdeprovisioningExpiryPLUS the current hour) for only that license, oldest-first. The bypass is SCOPED to the deprovisioning license ONLY — other active licenses keep the normalnow-23h … now-1hgate 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 toMeteringLockHoursitself and NOT aMeteringLockHoursevent. Once the window has elapsed, the events-stack deprovision-cleanup Lambda (rate(15m)) setsPurchase Agreement Endedand clears the deprovisioning markers — finalization is time-based, so it is correct across multiple hours and regions. After the server window closes,inactivereturnsBatchMeterUsage.CustomerNotSubscribed - →
Purchase Agreement EndedONLY (does NOT change subscriptionStatus, stop metering, or flush).agreementStatus=inactive - → refresh agreement metadata (stays active).
Purchase Agreement Amended - → NOT consumed by metering (no licenseArn); the seller handles it if needed (pre-provisioning, CRM), without activating metering.
Purchase Agreement Created
Do NOT invert these (, not , opens the flush window). Do NOT discard queued records before flushing. is reference-only and NEVER gates metering.
License DeprovisionedPurchase Agreement EndedregisteredRegions订阅者行上的两个独立状态字段(绝不混淆):
- =
agreementStatus/active(协议生命周期)。inactive - =
subscriptionStatus/active/deprovisioning(许可证生命周期;没有inactive值)。deprovisioned
计量仅遵循许可证生命周期:
- → 协议+订阅
License Updated(允许计量)。active - →
License Deprovisioned+subscriptionStatus=deprovisioning(事件时间 + 约 1 小时);打开约 1 小时的最终用量刷新窗口。锁绕过机制: 在此窗口期间,管道立即提交该许可证所有剩余的待处理用量,忽略deprovisioningExpiry和正常的仅已完成小时门控——它不等待MeteringLockHours过去,并且确实计量当前进行中的小时(正常路径总是跳过的那一小时)。具体机制/标记:MeteringLockHours处理器在该订阅者行上设置License Deprovisioned+subscriptionStatus=deprovisioning;发现器以刷新-取消供应模式运行,以该标记为键,仅为该许可证对待处理桶(deprovisioningExpiry加上当前小时)排队,最旧优先。绕过仅限于取消供应的许可证——其他活跃许可证保持正常的now-23h … now-1h门控和仅已完成小时行为;一个买家的取消供应绝不强制刷新另一个买家的开放小时。是取消供应刷新绕过锁,不是MeteringLockHours本身的更改,也不是MeteringLockHours事件。窗口过去后,事件堆栈取消供应清理 Lambda(rate(15m))设置Purchase Agreement Ended并清除取消供应标记——完成基于时间,因此在多个小时和区域中都是正确的。服务端窗口关闭后,inactive返回BatchMeterUsage。CustomerNotSubscribed - → 仅
Purchase Agreement Ended(不更改 subscriptionStatus、不停止计量、不刷新)。agreementStatus=inactive - → 刷新协议元数据(保持活跃)。
Purchase Agreement Amended - → 计量不消费(无 licenseArn);卖家在需要时处理它(预配置、CRM),而不激活计量。
Purchase Agreement Created
不要反转这些(打开刷新窗口的是 ,不是 )。不要在刷新前丢弃排队的记录。 仅参考且绝不门控计量。
License DeprovisionedPurchase Agreement EndedregisteredRegionsSetup references
设置参考
- Full walkthrough + writer contract + Common Mistakes + VMT/token/timestamp/schema/meter-flow:
references/setup-path.md - Deployment steps: · IAM policy:
references/deployment-steps.mdreferences/iam-credentials.md - Concurrent Agreements + data model: · Architecture + status lifecycle:
references/concurrent-agreements.mdreferences/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: · Client-side guardrails (all checks before
references/troubleshooting.md):BatchMeterUsagereferences/client-side-guardrails.md - Reference implementation: (shared) +
scripts/metering_core.py/discoverer.py/aggregator.py/cleanup.py/submitter.py(pipeline) +expiry.py/register.py;subscription.py(non-prod sandbox test-event generator, dry-run stages only);scripts/test_event_publisher.py(needsscripts/deploy.sh); SAM templatesassets/requirements.txt,assets/template-main.yamlassets/template-events.yaml
- 完整演练 + 写入器契约 + 常见错误 + VMT/token/时间戳/模式/计量流程:
references/setup-path.md - 部署步骤:· IAM 策略:
references/deployment-steps.mdreferences/iam-credentials.md - 并发协议 + 数据模型:· 架构 + 状态生命周期:
references/concurrent-agreements.mdreferences/architecture.md - 现有卖家(修改/扩展、旧版):
references/existing-sellers.md - 品牌买家页面 + 了解你的买家(KYB)捕获:
references/registration-page.md - 动手测试计划(手动步骤、范围用例、异步轮询):
references/test-plan.md - 监控与告警:· 客户端护栏(
references/troubleshooting.md前的所有检查):BatchMeterUsagereferences/client-side-guardrails.md - 参考实现:(共享)+
scripts/metering_core.py/discoverer.py/aggregator.py/cleanup.py/submitter.py(管道)+expiry.py/register.py;subscription.py(非生产沙盒测试事件生成器,仅 dry-run 阶段);scripts/test_event_publisher.py(需要scripts/deploy.sh);SAM 模板assets/requirements.txt、assets/template-main.yamlassets/template-events.yaml
Monitoring & alarms (seller-owned)
监控与告警(卖家拥有)
The deployed stack creates metrics + CloudWatch alarms in the SELLER's account covering the revenue-loss signals (per-stage Lambda / across discoverer/aggregator/cleanup/submitter/expiry, a "did not run" invocations alarm, age-out, and the EMF business-status metrics). Alarms are always created; actions wire to an OPTIONAL in-region SNS topic (). 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 s. Detail: .
awsmp-*ErrorsThrottles>24hAlertsTopicArnTODO: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 ; thresholds are seller s):
AlertsTopicArnTODO:- (alarm
UsageRecordUnprocessed) —*-usage-unprocessedreturnedBatchMeterUsagethat were STILL unprocessed after the submitter's one automatic retry. The submitter does NOT raise on this (it leaves them pending for the nextUnprocessedRecordscycle), so this metric is the ONLY alarm-able signal for therate(5m)case — it does NOT trip the LambdaUnprocessedRecordsalarm. A persistent breach = records repeatedly failing to meter (throttling, timestamp bounds, downstream issue) → investigate.Errors - (
UsageRecordRejected) — client-side guardrail rejections before metering (summed across reasons; per-reason breakdown on the dashboard); fix source rows + resubmit.*-usage-rejected(BatchMeterUsageException) — a REQUEST-level exception failed a whole call (dimension/license/etc.); pipeline bisects to isolate.*-batchmeterusage-exception(CustomerNotSubscribed) — per-record status, no active subscription (do NOT retry).*-customer-not-subscribed - Plus age-out alarms (/
UsageAggregationExpired,UsageSubmissionExpired) catch usage that silently never metered, and Lambda>24h/Errors/DLQ-depth alarms catch pipeline failures.Throttlesis a dashboard metric (informational — first submission already billed), not an alarm.DuplicateRecord
部署的堆栈在卖家账户中创建 指标 + CloudWatch 告警,涵盖收入损失信号(发现器/聚合器/清理器/提交器/过期器各阶段的 Lambda /、"未运行"调用告警、 老化以及 EMF 业务状态指标)。告警总是创建;操作连接到可选的区域内 SNS 主题()。业务状态指标使用 Lambda 日志中的 EMF(无需额外运行时 IAM)。处理告警是卖家的责任——AWS Marketplace 和此 skill 不跟踪客户端错误;部署者和任何 Lambda 都不向主题发布(CloudWatch 触发操作)。阈值/操作/主题是卖家的 。详情:。
awsmp-*ErrorsThrottles>24hAlertsTopicArnTODO:references/troubleshooting.md"哪些告警告诉我计量记录被拒绝 / 未计量?"——堆栈为每个拒绝/失败信号创建专用告警,使卖家在记录停止计量时立即被分页(全部触发到 ;阈值是卖家的 ):
AlertsTopicArnTODO:- (告警
UsageRecordUnprocessed)——*-usage-unprocessed返回的BatchMeterUsage在提交器的一次自动重试后仍未处理。提交器不会因此抛错(它将它们留待下一个UnprocessedRecords周期),因此此指标是rate(5m)情况唯一可告警的信号——它不会触发 LambdaUnprocessedRecords告警。持续违反 = 记录反复无法计量(节流、时间戳边界、下游问题)→ 调查。Errors - (
UsageRecordRejected)——计量前的客户端护栏拒绝(跨原因汇总;按原因细目在仪表板上);修复源行 + 重新提交。*-usage-rejected(BatchMeterUsageException)——请求级异常使整个调用失败(维度/许可证/等);管道二分隔离。*-batchmeterusage-exception(CustomerNotSubscribed)——逐记录状态,无活跃订阅(不要重试)。*-customer-not-subscribed - 加上老化告警(/
UsageAggregationExpired、UsageSubmissionExpired)捕获静默从未计量的用量,Lambda>24h/Errors/DLQ 深度告警捕获管道失败。Throttles是仪表板指标(信息性——首次提交已计费),不是告警。DuplicateRecord
QUERY PATH — Find, Debug, and Analyze Metering Records
查询路径——查找、调试和分析计量记录
Full routing/patterns: + .
references/query-path.mdreferences/query-patterns.mdCapability boundary (state it; don't return a bounded answer as authoritative). This query USES CloudTrail Event History () 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 , or the subscribers table) — only non-product-scoped queries (never guess attribution). Every result carries //. Before presenting a result, CONFIRM these signals: if 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 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 export both see SUBMISSIONS only (rejected/duplicate, regional, no CA ); 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 (never Lake) only for long-term raw-submission audit retention. Full boundary: .
LookupEventsadditionalEventDatacompletetruncatedwindowClippedcomplete--max-pagestrail → S3productCodetrail → S3references/query-path.mdComplex / 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 () for metering — ever.
create-event-data-store - MUST NOT present CloudTrail (Event History OR a /Athena export) as a source of billed/billing-grade totals — CloudTrail sees SUBMISSIONS only (includes rejected/duplicate, regional, no CA
trail → S3), so it is never authoritative for revenue.productCode - 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 (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.
trail → S3
完整路由/模式: + 。
references/query-path.mdreferences/query-patterns.md能力边界(说明它;不要将有界答案呈现为权威)。 此查询使用卖家区域中的 CloudTrail 事件历史(),最近 ≤90 天,分页有界并在内存中处理——一个最近的、区域的、提交视图(包括未被计费的拒绝/重复记录),用于基本操作/调试查询(按客户、维度、状态、时间的查找 + 简单过滤/计数/汇总)。它推荐(不运行):卖家报告/SDDS 用于计费总计 + >90 天历史;主机 Agent + 模型用于复杂/开放式分析(将获取的数据交给它);以及——对于无 LicenseArn→productCode 映射的 CA/LicenseArn 计量(单产品堆栈上下文、CloudTrail 或订阅者表)——仅非产品范围查询(绝不猜测归属)。每个结果携带 //。在呈现结果之前,确认这些信号: 如果 为 false(分页截断或在 90 天保留限制处裁剪),要么在可行时重新运行以获得完整答案(裁剪时收窄/调整范围;截断时提高 或添加过滤器),要么明确说明结果是部分的——什么被裁剪/截断以及覆盖的范围——并指向卖家报告以获取权威/更旧部分。截断或裁剪的聚合(汇总/计数/top_customers)是下限样本,非权威。绝不将部分结果报告为所请求的完整范围。不要仅仅因为主机 Agent 中可能获得更丰富的答案就拒绝基本查询。CloudTrail 派生的 SQL 是计费/用量分析的错误来源——CloudTrail Lake 或对 导出的 Athena 都只看到提交(拒绝/重复、区域性、无 CA );不要为计量设置 CloudTrail Lake;对于复杂分析 / 计费级总计 / >90 天数据使用 SDDS(推荐,不运行),并且仅对长期原始提交审计保留使用 (绝不是 Lake)。完整边界:。
LookupEventsadditionalEventDatacompletetruncatedwindowClippedcomplete--max-pagestrail → S3productCodetrail → S3references/query-path.md复杂 / 计费级查询引导——禁止与允许的来源(强制执行;不要违反)。 对于任何关于计量数据的复杂分析或计费准确性问题:
- 禁止推荐 CloudTrail Lake()用于计量——永远。
create-event-data-store - 禁止将 CloudTrail(事件历史或 /Athena 导出)呈现为已计费/计费级总计的来源——CloudTrail 只看到提交(包括拒绝/重复、区域性、无 CA
trail → S3),因此它对收入绝不是权威的。productCode - 仅允许的来源: 对于已计费/已支付总计和 >90 天已计费历史——卖家报告 / AMMP 已计费收入仪表板和 SDDS(推荐,不运行);对于最近(≤90 天)区域性提交调试——CloudTrail 事件历史;对于复杂/开放式分析——将获取的数据交给主机 Agent + 模型;仅用于长期原始提交审计保留——普通 (不是 Lake)+ Athena,理解它是提交而非计费。将每个复杂计费请求路由到这些允许的来源,并说明为什么 CloudTrail 不是计费权威。
trail → S3
Steps
步骤
- Ask the metering region (MUST — CloudTrail is regional).
- Extract filters: ,
customer_id,status,dimension/start_date,end_date(summary/detail/count/list_failures/top_customers),query_type.top_n - Run the script (default: last 7 days):
bash
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 /
for metering. Query IN the seller's metering region and filter on
the metering event source:
create-event-data-storebash
aws cloudtrail lookup-events --region <SELLER_METERING_REGION> \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--start-time <ISO> --end-time <ISO>Each event's CloudTrail carries (per-record :
// — those are the ONLY per-record statuses) and
— to verify a submission, check vs FIRST
(records in were not processed — see the note below). CloudTrail lowercases the
first letter of response fields, so read ///
(lowercase) — the direct SDK response uses PascalCase (//
/). An undefined dimension
is NOT a per-record status — it fails the whole call with a request-level
(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.
BatchMeterUsageresponseElementsresultsstatusSuccessCustomerNotSubscribedDuplicateRecordunprocessedRecordsresultsunprocessedRecordsunprocessedRecordsUnprocessedRecordsresultsstatusmeteringRecordIdunprocessedRecordsBatchMeterUsageResultsStatusMeteringRecordIdUnprocessedRecordsInvalidUsageDimensionException- 询问计量区域(必须——CloudTrail 是区域性的)。
- 提取过滤器:、
customer_id、status、dimension/start_date、end_date(summary/detail/count/list_failures/top_customers)、query_type。top_n - 运行脚本(默认:最近 7 天):
bash
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 事件历史配方(脚本做什么;如果手动查询可直接使用)。
事件历史默认开启——无数据存储、无需设置,计量绝不使用 CloudTrail Lake / 。在卖家的计量区域中查询并过滤计量事件源:
create-event-data-storebash
aws cloudtrail lookup-events --region <SELLER_METERING_REGION> \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--start-time <ISO> --end-time <ISO>每个 事件的 CloudTrail 携带 (逐记录 ://——这些是仅有的逐记录状态)和 ——验证提交时,首先检查 与 ( 中的记录未被处理——见下面的 说明)。CloudTrail 将响应字段的首字母小写,因此读取 ///(小写)——直接 SDK 响应使用 PascalCase(///)。未定义的维度不是逐记录状态——它以请求级 使整个调用失败(见下文)。仅 ≤90 天;对于更旧/已计费总计使用卖家报告/SDDS(推荐,不查询)。CloudTrail 不是收入——它显示提交(包括拒绝/重复、未计费)。计量用量在下个月月初(2 日/3 日)计费——例如 11 月用量出现在 12 月发票上——仪表板收入是估计的直到月份结束;使用卖家报告获取已计费/已支付金额。
BatchMeterUsageresponseElementsresultsstatusSuccessCustomerNotSubscribedDuplicateRecordunprocessedRecordsresultsunprocessedRecordsunprocessedRecordsUnprocessedRecordsresultsstatusmeteringRecordIdunprocessedRecordsBatchMeterUsageResultsStatusMeteringRecordIdUnprocessedRecordsInvalidUsageDimensionExceptionStatus meanings
状态含义
Per-record values:
Results[].Status| Status | Meaning | Action |
|---|---|---|
| Accepted and billed | None |
| No active subscription | ⚠️ Do NOT retry |
| First-write-wins and reported; a differing-quantity resubmit is NOT billed | Safe only for an identical retry; never to change a quantity (under-billing risk) |
Request-level exceptions (fail the ENTIRE 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 (, not billed), plus the server-side exception if a row reaches the API — surface BOTH when explaining a rejection:
BatchMeterUsageRejectedClientSide| Server-side exception | Client-side reason code | Meaning | Action |
|---|---|---|---|
| | A 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 |
| | A record older than 24h (e.g. a 30h-old row) or otherwise outside the window — REJECTED, not submitted | Fix the timestamp; the client-side guardrail rejects the row with reason |
| | A malformed/invalid | Shape-validate client-side (reason |
| | More than 5 | Reduce to ≤5 tags; the client-side guardrail rejects the row with reason |
Sort-key / reject-code ordering: the pipeline validates a row's shape BEFORE it can enter a batch. A row whose sort key () carries a malformed or out-of-window timestamp is rejected client-side with reason code (marked , emits ) — it never reaches , so it does NOT surface as a server-side unless a bad timestamp slips through into a batch. Always name the client-side reason code for a malformed-timestamp sort key, and its server-side equivalent.
customerAWSAccountId#dimension#timestampMalformedTimestampRejectedClientSideUsageRecordRejectedBatchMeterUsageTimestampOutOfBoundsExceptionMalformedTimestampTimestampOutOfBoundsExceptionFull reject-/status-code catalog (the ONE place to cite for any reason-code question). A question like "what does mean?" should be answered from this complete set, mapping the client-side guardrail reason code to its server-side equivalent:
UnknownDimensionClient-side reason code ( | Server-side equivalent | Meaning |
|---|---|---|
| — (never reaches API) | Sort key not |
| | Non-whole-second, unparseable, or >24h-old timestamp |
| — | Row has no |
| | Dimension not defined in the catalog (case-sensitive) — |
| — | Quantity < 0 |
| — | Quantity not a whole integer |
| | >5 |
| — | >2500 merged tag-sets in a group |
| — | |
| — | Group mixes tagged and untagged non-zero rows |
| | Malformed/invalid |
Plus the terminal per-record verdicts returned BY the API (not client-side reasons): (billed), (first-write-wins; a differing-quantity resubmit is NOT billed — under-billing risk, not "benign"), (no active subscription — do NOT retry). And (transient, retryable) — see below.
Results[].StatusSuccessDuplicateRecordCustomerNotSubscribedUnprocessedRecordsUnprocessedRecordsResults[].StatusresultsunprocessedRecordsThrottlingExceptionInternalServiceExceptionUsageRecordUnprocessedResults[].StatusData 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(excludeStatus=Success,DuplicateRecord, 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 forCustomerNotSubscribeds with DIFFERENT quantities for the sameDuplicateRecord— 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.(account, licenseArn, dimension, hour)
- BILLED history (any range) needs NO setup — the AMMP Insights "Billed Revenue" dashboard (only ; 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
aws-marketplace:GetSellerDashboard. Detail:GenerateDataSet.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 (plain trail, NOT Lake; Athena) — also from setup forward, no backfill; complementary DynamoDB PITR export of
metering-marketplace.amazonaws.com. Detail:aggregated_usage.references/query-patterns.md
逐记录 值:
Results[].Status| 状态 | 含义 | 操作 |
|---|---|---|
| 已接受并计费 | 无 |
| 无活跃订阅 | ⚠️ 不要重试 |
| 首次写入优先并已报告;不同数量的重新提交不被计费 | 仅对相同重试安全;绝不要更改数量(少计风险) |
请求级异常(使整个 调用失败,不是逐记录状态)。每个都有管道在 API 调用前拒绝该行时盖上的客户端护栏原因代码(,不计费),加上如果行到达 API 时的服务端异常——解释拒绝时同时呈现两者:
BatchMeterUsageRejectedClientSide| 服务端异常 | 客户端原因代码 | 含义 | 操作 |
|---|---|---|---|
| | 目录中未定义的维度(服务端权威;区分大小写;不计费) | 修复维度键 / 在 AMMP 中定义它。管道通过二分(深度有界,⌈log2(批次大小)⌉=5)隔离违规记录,使其同批次记录仍然计量,标记为 |
| | 超过 24 小时的记录(例如 30 小时前行)或超出窗口——被拒绝,不提交 | 修复时间戳;客户端护栏在批处理前以原因 |
| | 格式错误/无效的 | 客户端形状验证(原因 |
| | 一条 UsageRecord 上超过 5 个 | 减少到 ≤5 个标签;客户端护栏在批处理前以原因 |
排序键 / 拒绝代码顺序: 管道在行可以进入批次之前验证其形状。排序键()带有格式错误或超窗时间戳的行在客户端以原因代码 被拒绝(标记为 ,发出 )——它永远不会到达 ,因此除非坏时间戳溜进批次,否则不会作为服务端 出现。对格式错误时间戳排序键,始终说出客户端 原因代码及其服务端 对应物。
customerAWSAccountId#dimension#timestampMalformedTimestampRejectedClientSideUsageRecordRejectedBatchMeterUsageTimestampOutOfBoundsExceptionMalformedTimestampTimestampOutOfBoundsException完整拒绝/状态代码目录(任何原因代码问题的唯一引用处)。 像" 是什么意思?"这样的问题应该从这个完整集合回答,将客户端护栏原因代码映射到其服务端对应物:
UnknownDimension客户端原因代码( | 服务端对应物 | 含义 |
|---|---|---|
| —(永不到达 API) | 排序键不是 |
| | 非整秒、不可解析或 >24 小时前的时间戳 |
| — | 行没有 |
| | 目录中未定义的维度(区分大小写)—— |
| — | 数量 < 0 |
| — | 数量不是整数 |
| | 一条记录上 >5 个 |
| — | 组中 >2500 个合并标签集 |
| — | |
| — | 组混合带标签和未标签的非零行 |
| | 格式错误/无效的 |
加上 API 返回的终态逐记录 判定(不是客户端原因):(已计费)、(首次写入优先;不同数量的重新提交不被计费——少计风险,不是"良性")、(无活跃订阅——不要重试)。以及 (暂时的,可重试)——见下文。
Results[].StatusSuccessDuplicateRecordCustomerNotSubscribedUnprocessedRecordsUnprocessedRecordsResults[].StatusresultsunprocessedRecordsThrottlingExceptionInternalServiceExceptionUsageRecordUnprocessedResults[].Status数据层: CloudTrail 事件历史(≤90 天,免费,实时)是 skill 直接查询的层;它显示所有提交(包括拒绝/重复),卖家报告仅显示已计费——因此"总计不匹配" = 拒绝/重复的差值。两个不同的历史问题,不要混淆:
对账已提交(CloudTrail)与已计费(卖家报告)。 将卖家报告 / AMMP 已计费收入视为已计费的真实来源;CloudTrail 仅提交。要对账,将 CloudTrail 过滤为(排除Status=Success、DuplicateRecord和请求级异常——这些都不计费)并与计费报告比较;剩余的差距是预期的拒绝/重复差值。注意同一CustomerNotSubscribed的不同数量的(account, licenseArn, dimension, hour)——这不是良性的:首次写入优先意味着后来的(通常更大的)数量不被计费,因此可能表明少报/少计。将它们作为对账红旗而非忽略重复项。DuplicateRecord
- 已计费历史(任何范围)无需设置——AMMP Insights "Billed Revenue" 仪表板(仅 ;默认 6 个月,扩大发票日期过滤器)。仅控制台(QuickSight)+ 手动 CSV 导出——无按需计费 API。程序化已计费数据 = SDDS(每日 S3 推送 + 您的 Athena/ETL),但仅推送且从注册起(无回填)。Skill 推荐,不运行;绝不
aws-marketplace:GetSellerDashboard。详情:GenerateDataSet。references/seller-reports.md- 折扣 / Private Offers 不是计量或卖家报告关注点——重定向到公开 AWS Marketplace 渠道(AMMP)。 如果卖家询问为什么由于折扣或 Private Offer 已计费金额与计量用量不同,或如何应用/更改一个,不要试图从计量数据解释或对账,也不要停留在"超出计量范围"。将计量数量转化为已计费收入的定价(公开价格、Private Offers、折扣、EULA 条款)在 AWS Marketplace 管理门户(AMMP) 中管理——说明折扣/Private Offer 差值符合预期(卖家报告显示任何协商折扣后的已计费收入,而 CloudTrail 显示原始计量提交),并将折扣/Private Offer 创建和管理重定向到 AMMP → AWS Marketplace 管理门户 / Private Offers。
- 超过 90 天的原始提交历史没有开箱即用的来源(卖家报告/SDDS 仅计费;事件历史保留 90 天)。通过自管理的 CloudTrail trail → S3 在 上保留(普通 trail,不是 Lake;Athena)——也从设置起,无回填;补充
metering-marketplace.amazonaws.com的 DynamoDB PITR 导出。详情:aggregated_usage。references/query-patterns.md
Query references
查询参考
- Routing + parameters: · CloudTrail CLI patterns + pagination:
references/query-path.mdreferences/query-patterns.md - Seller Reports reconciliation + SDDS automation boundary: · Dimension reference:
references/seller-reports.mdreferences/dimension-reference.md
- 路由 + 参数:· CloudTrail CLI 模式 + 分页:
references/query-path.mdreferences/query-patterns.md - 卖家报告对账 + SDDS 自动化边界:· 维度参考:
references/seller-reports.mdreferences/dimension-reference.md
Key APIs
关键 API
| API | Region | Purpose |
|---|---|---|
| ResolveCustomer | any region (seller's choice) | Resolve token → LicenseArn + customerAWSAccountId + productCode at registration; regional, from the publishing account; independent of the BatchMeterUsage region |
| BatchMeterUsage | seller's metering region | Submit usage records (hourly) |
| DescribeEntity | us-east-1 | Validate product / type / visibility |
| LookupEvents | seller's region | Query metering records (≤90 days) |
IAM action names: (meter), (register), (reports). Namespace is , NOT . When diagnosing AccessDenied, name the exact action and check the execution role, condition keys, and Resource-ARN mismatches.
aws-marketplace:BatchMeterUsageaws-marketplace:ResolveCustomeraws-marketplace:GetSellerDashboardaws-marketplace:meteringmarketplace:*| API | 区域 | 用途 |
|---|---|---|
| ResolveCustomer | 任意区域(卖家选择) | 注册时解析 token → LicenseArn + customerAWSAccountId + productCode;区域性,从发布账户调用;独立于 BatchMeterUsage 区域 |
| BatchMeterUsage | 卖家的计量区域 | 提交用量记录(每小时) |
| DescribeEntity | us-east-1 | 验证产品 / 类型 / 可见性 |
| LookupEvents | 卖家区域 | 查询计量记录(≤90 天) |
IAM 操作名称:(计量)、(注册)、(报告)。命名空间是 ,不是 。诊断 AccessDenied 时,说出确切的操作并检查执行角色、条件键和 Resource-ARN 不匹配。
aws-marketplace:BatchMeterUsageaws-marketplace:ResolveCustomeraws-marketplace:GetSellerDashboardaws-marketplace:meteringmarketplace:*Security
安全
- Least privilege: execution roles grant ONLY the actions used — meter path: + scoped DynamoDB
aws-marketplace:BatchMeterUsage/Queryon its tables + theUpdateItemindex; register:metering_pending+ scopedaws-marketplace:ResolveCustomeron theQueryGSI + item ops. NocustomerAWSAccountId(lookups use GSIdynamodb:Scan); neverQueryor broaddynamodb:*.aws-marketplace:* - No swallowed auth errors: subscriber/license lookups do NOT catch as "not found" — permission/config errors surface (Lambda error / 500 / DLQ).
AccessDeniedException - Encryption at rest: SQS (), SNS (
SqsManagedSseEnabled), DynamoDB (alias/aws/sns); Lambda + API log groups are explicit resources withSSEEnabled+ optionalRetentionInDays(they contain buyer account IDs / license ARNs).KmsKeyId - Public registration API: intentionally open (Marketplace POSTs the token unauthenticated) → template attaches a baseline WAF WebACL by default (rate-based + common-exploit; overrides); API Gateway throttling + access logging always on; register Lambda returns security headers and persists ONLY an allowlist of length-bounded fields (
WebAclArn).AllowedRegistrationFields - Credentials (ephemeral-first): SSO → ; no long-lived keys, don't ask for keys first. Deployer role uses a conditioned trust policy (not open
sts assume-role) + permissions-boundary-bounded:root.iam:CreateRole - Stage name: has no default + an
StageNamerejecting empty; mandatory questionnaire answer;AllowedPatternandsam deployfail fast (nodeploy.shfallback).prod - Event scoping: EventBridge rule scoped to ; the EventBridge→SQS policy via
source: ["aws.agreement-marketplace"].aws:SourceArn - Data classification: customer AWS account IDs + agreement metadata are sensitive — avoid logging full identifiers; encrypt log groups that contain them.
- 最小权限: 执行角色仅授予使用的操作——计量路径:+ 其表上的作用域 DynamoDB
aws-marketplace:BatchMeterUsage/Query+UpdateItem索引;注册:metering_pending+aws-marketplace:ResolveCustomerGSI 上的作用域customerAWSAccountId+ 项目操作。无Query(查找使用 GSIdynamodb:Scan);绝不Query或广泛的dynamodb:*。aws-marketplace:* - 不吞没认证错误: 订阅者/许可证查找不将 捕获为"未找到"——权限/配置错误会浮出水面(Lambda 错误 / 500 / DLQ)。
AccessDeniedException - 静态加密: SQS()、SNS(
SqsManagedSseEnabled)、DynamoDB(alias/aws/sns);Lambda + API 日志组是显式资源,带SSEEnabled+ 可选RetentionInDays(它们包含买家账户 ID / 许可证 ARN)。KmsKeyId - 公开注册 API: 故意开放(Marketplace 未认证地 POST token)→ 模板默认附加基线 WAF WebACL(基于速率 + 常见漏洞利用;可覆盖);API Gateway 节流 + 访问日志始终开启;注册 Lambda 返回安全头并仅持久化长度受限字段的允许列表(
WebAclArn)。AllowedRegistrationFields - 凭证(临时优先): SSO → ;无长期密钥,不要先要密钥。部署者角色使用条件化信任策略(不是开放的
sts assume-role)+ 权限边界受限的:root。iam:CreateRole - 阶段名称: 无默认值 + 拒绝为空的
StageName;强制问卷答案;AllowedPattern和sam deploy快速失败(无deploy.sh回退)。prod - 事件范围: EventBridge 规则范围限定为 ;EventBridge→SQS 策略通过
source: ["aws.agreement-marketplace"]。aws:SourceArn - 数据分类: 客户 AWS 账户 ID + 协议元数据敏感——避免记录完整标识符;加密包含它们的日志组。