aws-marketplace-metering

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

AWS 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,
deploy.sh
) and everything it deploys to a seller's AWS account MUST be treated as a proposal to review, not a trusted artifact. Before any deployed stack carries PRODUCTION metering traffic (real billing), the seller MUST:
  • Thoroughly review every resource the skill created or changed — templates, Lambda code, IAM roles/policies (least-privilege, no unintended grants), tables/indexes, queues, alarms — in their own account, and confirm it matches their intent and their org's standards. Metering errors directly affect revenue (under- or over-billing), so this review is not optional.
  • Put the generated code through the seller's normal code review + change-management process (do not ship AI output straight to production), and
  • Run the full hands-on test plan (
    references/test-plan.md
    ) end-to-end and verify each expected observable outcome BEFORE relying on the integration for real billing. The skill SHALL surface this review-before-production expectation to the seller, and SHALL NOT imply that a successful deploy means the integration is production-ready without this review + test.
完整的 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 策略、
deploy.sh
)以及它部署到卖家 AWS 账户的所有内容都必须视为待审查的提案,而非可信工件。在任何已部署堆栈承载生产计量流量(真实计费)之前,卖家必须:
  • 彻底审查 skill 创建或更改的每个资源——模板、Lambda 代码、IAM 角色/策略(最小权限、无意外授权)、表/索引、队列、告警——在其自己的账户中,并确认符合其意图和组织标准。计量错误直接影响收入(少计或多计),因此此审查不可省略。
  • 将生成的代码经过卖家正常的代码审查 + 变更管理流程(不要将 AI 输出直接投入生产),并且
  • 完整运行动手测试计划(
    references/test-plan.md
    )并验证每个预期的可观察结果,然后才依赖该集成进行真实计费。 该 skill 应向卖家传达此"生产前审查"预期,且不得暗示成功部署即意味着无需审查和测试即可投入生产。

Using this skill's packaged resources

使用此 skill 打包的资源

The packaged
scripts/
,
assets/
,
references/
show how to structure a metering stack — a reference, NOT content to copy verbatim.
  • Get every seller-specific value from the seller (product code, region(s), stage name, VMT tags, alarm actions/thresholds, retention/KMS) and fill the
    TODO:
    markers. If a required value is unknown, ask — do not assume. VMT is presence-based: the seller writes each usage row's
    usageAllocations
    (their real tags); the pipeline (aggregator) merges them and never invents tags (no
    VmtEnabled
    /
    VmtTagAttribute
    parameter).
  • Do not change the architecture or
    awsmp-*
    naming
    (two-stack layout, table/GSI/event wiring are fixed). Sellers customize only the marked
    TODO:
    sections. A request that would break the architecture/naming is explained as a constraint, not silently accommodated.
打包的
scripts/
、
assets/
、
references/
展示了如何构建计量堆栈——作为参考,而非逐字复制的内容。
  • 从卖家获取每个卖家特定的值(产品代码、区域、阶段名称、VMT 标签、告警操作/阈值、保留/KMS),并填写
    TODO:
    标记。如果必需值未知,请询问——不要假设。VMT 基于存在性:卖家写入每个用量行的
    usageAllocations
    (其真实标签);管道(聚合器)合并它们,绝不发明标签(无
    VmtEnabled
    /
    VmtTagAttribute
    参数)。
  • 不要更改架构或
    awsmp-*
    命名
    (双堆栈布局、表/GSI/事件接线是固定的)。卖家只自定义标记的
    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
    retrieve_skill
    :
    the skill is NOT on the local filesystem — fetch each reference/script via
    retrieve_skill
    with the
    file
    parameter (e.g.
    file="references/deployment-steps.md"
    ); do NOT
    file_read
    these paths.
  • Installed locally: read/run files from the local skill dir via relative paths.
This applies only to the skill's own files; user data/session artifacts are always in the user's working directory.
在读取参考或运行脚本之前,确定 skill 的加载方式:
  • AWS MCP
    retrieve_skill
    :
    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.
  • ResolveCustomer
    resolves the buyer's
    LicenseArn
    ,
    customerAWSAccountId
    , and
    productCode
    from the
    x-amzn-marketplace-token
    when the buyer is redirected from the Marketplace console to the SaaS registration site; the SaaS then collects additional registration data and persists it (the PII fields) in the in-region customer-profile table (the PII-free subscribers table gets only the CA identity + statuses +
    registeredRegions
    ). It returns a top-level
    LicenseArn
    field —
    CustomerIdentifier
    is a deprecated legacy field, null for new SaaS integrations (do NOT use it and do NOT treat its value as the LicenseArn). It is a regional API, but the region is the seller's free choice — it does NOT have to match where the buyer uses the software, and it does NOT gate metering.
    BatchMeterUsage
    can be called in ANY supported region independently of where
    ResolveCustomer
    was called. It is NOT a us-east-1-only API (only the EventBridge subscription events like
    License Updated
    are us-east-1-only). It MUST be called from the account that PUBLISHED the SaaS product (else the token will not resolve). The
    x-amzn-marketplace-token
    is NOT one-time-use — it is REUSABLE until it expires (~4 hours)
    : re-resolving the same token before expiry returns the same result, so a buyer who errored on the registration page can retry with their original link while it is still within the ~4h window. After expiry
    ResolveCustomer
    returns
    ExpiredTokenException
    (an already-consumed/invalid token returns
    InvalidTokenException
    ) and the buyer re-clicks "Set up your account" for a FRESH token (no resubscribe). A retry days later = expired token → mint a new one. (
    references/troubleshooting.md
    .)
  • DuplicateRecord = first-write-wins and REPORTED (
    Status = DuplicateRecord
    , not
    Success
    ); the original quantity stays billed. A resubmit with a different quantity is NOT billed (under-billing risk). Safe only for an identical retry — never a way to correct a quantity; never call it "benign".
  • CustomerNotSubscribed = real failure, do NOT retry. Causes: (1) invalid identifier, (2) agreement ended/expired, (3) buyer account suspended.
  • A bad
    LicenseArn
    is a REQUEST-level
    InvalidLicenseException
    that fails the ENTIRE
    BatchMeterUsage
    batch
    — NOT a per-record
    Status
    in
    Results
    (unlike
    CustomerNotSubscribed
    ). Mitigation: shape-validate the LicenseArn client-side before batching (reason
    InvalidLicenseArn
    ) so a malformed ARN never enters a batch, and on a request-level
    ClientError
    bisect-and-retry to isolate the poison record so its co-batched records still meter.
  • BatchMeterUsage
    (SaaS) is REGIONAL, but the SELLER CHOOSES the region — there is NO
    InvalidEndpointRegionException
    .
    Call it in the region where usage occurs (the seller's choice; nearest supported region for on-prem/other-cloud); deploy a stack per metering region; dedup is per region. It is supported in MOST commercial regions but not all — where the API is unavailable in a chosen region the call returns
    DisabledApiException
    (meter in a supported region instead). There is no
    InvalidEndpointRegionException
    for SaaS
    BatchMeterUsage
    ; the seller picks the region and per-region dedup makes multi-region metering correct, not an error.
    • Do NOT conflate this with AMI/Container
      MeterUsage
      .
      Metered AMI/Container products are OUT OF SCOPE for this skill (redirect to public docs), and their region rule is the OPPOSITE:
      MeterUsage
      MUST be called in the SAME region as the running EC2 instance / ECS task — you CANNOT call it from us-east-1 (or any other region) regardless of where the instance runs; a mismatched region returns
      InvalidEndpointRegionException
      .
      Such products authenticate with an IAM instance/task role (EC2 instance profile, ECS task role, or EKS IRSA), never embedded/long-lived access keys. This same-region enforcement +
      InvalidEndpointRegionException
      applies ONLY to AMI/Container
      MeterUsage
      , NOT to SaaS
      BatchMeterUsage
      . See the public AWS docs for AMI/Container metering.
  • Dimension validity is server-authoritative. The pipeline is NOT configured with a dimension list and does NOT reject client-side; an undefined dimension fails the
    BatchMeterUsage
    call with a REQUEST-level
    InvalidUsageDimensionException
    (case-sensitive, not billed) — it is NOT a per-record
    Results[].Status
    . The pipeline isolates the offending record by bisection (depth-bounded to ⌈log2(batch size)⌉ = 5 so a many-failure batch can't overload the submitter), marks it
    RejectedClientSide
    (reason = the exception name), and emits the
    BatchMeterUsageException
    metric. Adding/renaming a catalog dimension needs no redeploy.
  • Rejection reason codes come in a client-side ↔ server-side PAIR — always surface BOTH. A row can be rejected two ways and the rubric expects the named code for each: (1) the pipeline's client-side guardrail rejects the row BEFORE the API call and stamps a client-side reason code (
    MalformedTimestamp
    ,
    InvalidLicenseArn
    ,
    TooManyTags
    ,
    InvalidDimension
    , etc.), marking it
    RejectedClientSide
    and emitting
    UsageRecordRejected
    ; (2) if a row reaches the API and is rejected there, the server-side equivalent is the matching
    BatchMeterUsage
    request-level exception (
    TimestampOutOfBoundsException
    ,
    InvalidLicenseException
    ,
    TooManyTagsException
    ,
    InvalidUsageDimensionException
    ). When explaining any rejection, name BOTH the client-side reason code AND its server-side exception equivalent, not just one. Client-side reasons ↔ server-side exceptions:
    MalformedTimestamp
    ↔
    TimestampOutOfBoundsException
    ;
    InvalidLicenseArn
    ↔
    InvalidLicenseException
    ;
    TooManyTags
    ↔
    TooManyTagsException
    ;
    InvalidDimension
    ↔
    InvalidUsageDimensionException
    .
  • UnprocessedRecords
    = a PARTIAL-BATCH outcome, NOT "the call accepted the record."
    The
    BatchMeterUsage
    request did NOT fail completely: records that succeeded appear in
    Results
    , records that failed to process appear in
    UnprocessedRecords
    (not billed; typically TRANSIENT —
    ThrottlingException
    /
    InternalServiceException
    ). They are safe to re-submit because metering is idempotent (first-write-wins dedup on
    CustomerAWSAccountId + LicenseArn + dimension + hour
    , per region). The deployed submitter RETRIES the record ONCE; if it still fails it is left for reprocessing and picked up on the NEXT SCHEDULED run (
    rate(5m)
    ) — NOT a blind client-side retry loop; the persistent case is alarmed via
    UsageRecordUnprocessed
    . This is DISTINCT from: per-record
    Results[].Status
    verdicts
    (
    Success
    /
    CustomerNotSubscribed
    /
    DuplicateRecord
    — FINAL, do NOT blindly retry), and request-level exceptions (
    InvalidUsageDimensionException
    /
    TimestampOutOfBoundsException
    /
    InvalidLicenseException
    ) that fail the ENTIRE batch and require isolating the poison record by bisection.
  • CloudTrail is regional — query the same region BatchMeterUsage is called in. ~15 min delivery latency. 90-day retention (recommend Seller Reports for older). Use CloudTrail Event History (default, no setup) filtered on
    eventSource = metering-marketplace.amazonaws.com
    — never CloudTrail Lake /
    create-event-data-store
    for metering lookups.
  • Timestamp window = 24 HOURS (not 1h), measured from
    now
    ; a record whose timestamp is older than 24h is outside the window and REJECTED — it is NOT submitted. Concretely, a 30-hour-old record is past the 24h boundary and does NOT fall inside the window: server-side it fails the call with
    TimestampOutOfBoundsException
    and one bad timestamp rejects the ENTIRE batch; before it ever reaches the API the pipeline's client-side guardrail rejects the row with reason code
    MalformedTimestamp
    (marked
    RejectedClientSide
    , not billed) so a stale/out-of-window timestamp never enters a batch. Do NOT tell a seller a >24h (e.g. 30h) record is "within the window" or "should be submitted" — it is rejected; the only exception is the 6-hour month-boundary grace, which accepts previous-month records until 06:00 UTC on the 1st. The pipeline meters only COMPLETED hours in
    now-23h … now-1h
    (never the in-progress/current hour — that is only metered by a deprovisioning flush), oldest-first, gated by
    MeteringLockHours
    ; a skipped/failed hourly-discoverer or
    rate(5m)
    -submitter run self-recovers on a later run while the hour is still in the 24h window. Detail:
    references/scalable-metering-design.md
    .
    • Best practice — submit usage within ~1 hour of generation. Late submission is a TOP real-world failure mode: the closer a row gets to the 24h boundary the less retry headroom it has, and past 24h it is permanently lost (
      TimestampOutOfBounds
      , not billed). Write/submit usage promptly (well inside the window) rather than batching a day's worth at the edge.
    • Age-out is TERMINAL and clears the pending marker. When a row/aggregation cannot be metered before it exceeds the 24h window, the pipeline ages it out: it sets a terminal
      meteringStatus
      (
      AggregationExpired
      for a raw row that never aggregated,
      SubmissionExpired
      for an aggregated row never submitted)
      AND CLEARS
      meteringPending
      so the row drops off the sparse
      metering_pending
      GSI and the discoverer's oldest-first backward walk STOPS returning it (no infinite re-processing). These fire the
      UsageAggregationExpired
      /
      UsageSubmissionExpired
      alarms — a paged signal that usage silently never metered; investigate the writer/pipeline delay.
    • A ~1-hour-old timestamp should NOT normally get
      TimestampOutOfBoundsException
      — 1h is well inside the 24h lookback, so this is UNUSUAL. Do NOT tell the seller to "just wait until HH:05". The real cause is almost always (a) one bad record failing the WHOLE batch (a co-batched malformed/out-of-window/undefined-dimension record makes the request-level exception reject every record in the call until it is isolated) or (b) clock skew (an unsynchronized host stamping timestamps in the future / far past, or local time instead of UTC). Fix: inspect the batch for the malformed record (bisect to isolate it) and verify the writer's clock is NTP-synced and stamping whole-second UTC.
  • Current (in-progress) hour is NEVER metered on the normal path. The pipeline only submits COMPLETED hours (
    now-23h … now-1h
    ); the hour currently in progress stays open and is skipped until it completes.
    MeteringLockHours
    (default 1, validated 1..20) is how many hours an hour stays open for late usage before it is aggregated/submitted — with the default of 1, the pipeline processes the just-completed hour. Pending hour-buckets are drained OLDEST-FIRST so the oldest usage is submitted before it approaches the 24h boundary and ages out (
    TimestampOutOfBounds
    ). The aggregator collapses each
    (licenseArn, customerAWSAccountId, dimension, hour)
    group to exactly ONE
    BatchMeterUsage
    record per hour
    (metering is per-group-per-hour, first-write-wins). The ONLY time the current in-progress hour is metered is the deprovisioning final-usage flush (lock-bypassed, see the event model). So "why isn't this hour showing usage yet?" = it is still the current hour and/or still inside
    MeteringLockHours
    — expected, not a bug.
  • BatchMeterUsage
    limits are TWO independent caps: ≤25 UsageRecords AND ≤1 MB request payload.
    A batch with 25 VALID records can still be rejected for SIZE if the serialized request exceeds 1 MB (large/many
    usageAllocations
    tag-sets, long ARNs) — it is the byte size, not the record count. Fix: put fewer records per call so each request stays under 1 MB. (The submitter already coalesces ≤25 per call; a size rejection means those records are too large — split further.)
  • VMT (vendor metered tagging) tag limit = 5
    usageAllocations
    tags per UsageRecord.
    A usage row may carry at most 5 tags in its
    usageAllocations
    ; more than 5 is invalid. The pipeline's client-side guardrail rejects the over-tagged row BEFORE the API call with reason code
    TooManyTags
    (marked
    RejectedClientSide
    , not billed, emits
    UsageRecordRejected
    ); the server-side equivalent if such a row reached the API is
    TooManyTagsException
    . When asked about the VMT tag cap, name BOTH the 5-tag maximum AND the
    TooManyTags
    client-side rejection (not just the number). VMT is presence-based — the seller supplies the tags; the pipeline never invents them.
  • VMT is ALL-OR-NOTHING per group — do NOT mix tagged and untagged rows. Within one
    (licenseArn, customerAWSAccountId, dimension, hour)
    group, either EVERY non-zero row carries
    usageAllocations
    or NONE does. Mixing allocated and unallocated non-zero rows in the same group is NOT allowed — the pipeline rejects it client-side with reason code
    MixedAllocatedAndUnallocated
    (
    RejectedClientSide
    , not billed). It is NOT "partial VMT attribution." Zero-quantity rows are ignored for this check, and DIFFERENT groups in the same hour/batch may independently have or omit allocations. Also per group: each row's
    AllocatedUsageQuantity
    values must sum to that row's
    quantity
    , and ≤2500 merged tag-sets per UsageRecord (else
    AllocationSumMismatch
    /
    TooManyAllocations
    ). Detail:
    references/setup-path.md
    ,
    references/client-side-guardrails.md
    .
  • Metering is idempotent (first-write-wins dedup per
    CustomerAWSAccountId + LicenseArn + dimension + hour
    , per region)
    , and the pipeline is DECOUPLED — there is no single meter Lambda and no "reserved concurrency = 2" knob. The submitter (the ONLY stage that calls
    BatchMeterUsage
    ) runs
    ReservedConcurrentExecutions=1
    — a single SERIAL submitter
    , deliberately serial to stay within the
    BatchMeterUsage
    request-rate limit (documented quota: 10 requests/second per account per region;
    MeterUsage
    10/s,
    RegisterUsage
    5/s — see service quotas) and avoid
    ThrottlingException
    . The discoverer runs at reserved concurrency 30; the aggregator and cleanup scale via their SQS event-source-mapping
    MaximumConcurrency
    . SQS absorbs bursts — throughput comes from draining the queues, not from raising submitter concurrency. If a seller needs higher sustained metering TPS, contact AWS Marketplace Seller Operations rather than raising concurrency past a safe level.
    • ThrottlingException
      handling — exponential backoff WITH JITTER.
      On
      ThrottlingException
      (or a throttled
      UnprocessedRecords
      ), retry with exponential backoff plus JITTER (randomized delay — name jitter explicitly; do NOT use a fixed/synchronized retry interval, which re-collides). Keep each call within the ≤25-record (and ≤1 MB) limit — do not grow the batch to "catch up." Retries are SAFE because metering is idempotent (first-write-wins): a re-sent already-billed record simply comes back as
      DuplicateRecord
      with the original quantity still billed. Do NOT treat
      DuplicateRecord
      as a reason to avoid retrying a throttled/unprocessed record — it is the idempotency guarantee that MAKES retry safe (the only caveat is never resubmit the same group with a DIFFERENT quantity, which would not be billed).
  • Registration form data (PII) lives in a per-Region
    customer-profile
    table, NOT the subscribers table.
    The in-region register Lambda writes the allowlisted registration/KYB fields to
    awsmp-<productCode>-customer-profile
    (PK
    licenseArn
    , SK
    customerAWSAccountId
    , seller-requested GSIs) IN the Region it runs in, keeping buyer PII in-region. The profile row is ALWAYS upserted for a registered buyer — even with ZERO extra form fields (the row keyed by identity is always written). The unified subscribers table (us-east-1) is PII-FREE — the register Lambda writes only the non-PII
    productCode
    + an idempotent
    registeredRegions
    append (NO registration form data) — plus the CA identity (
    licenseArn
    ,
    customerAWSAccountId
    ,
    agreementId
    ) and the two lifecycle statuses — so it is safe in us-east-1 even for opt-in-Region products. The template ALWAYS sets the register Lambda's
    CUSTOMER_PROFILE_TABLE
    env; if it is unset that is a MISCONFIGURATION and the handler fails loud (HTTP 500) rather than silently skipping the PII write. (An existing seller's own registration handler may store data differently — the skill adapts and does not force this reference Lambda verbatim.)
  • Legacy (pre-CA) integrations may use
    ProductCode
    +
    CustomerIdentifier
    instead of
    LicenseArn
    . The skill does NOT impose its own template/fallback on them (it must NOT present a fixed
    startswith("arn:")
    fallback as its template) — it grounds legacy behavior in the seller's EXISTING integration + public AWS docs (
    references/existing-sellers.md
    ).
  • Free trials — behavior depends on WHICH model (do not give a blanket "always meter" answer); confirm the model first and ground specifics in the public docs (SaaS free trials, PLG guide):
    • AWS Marketplace SaaS free trial feature (native, time-bound: the buyer subscribes to a separate trial offer and must upgrade before it ends): meter normally throughout — do NOT withhold. AWS applies the trial pricing so trial usage invoices at $0 (
      $0
      estimated revenue), and after conversion the same dimensions bill with no integration change.
    • Embedded free tier / usage-based trial (buyer on the STANDARD paid offer, seller decides when charges begin): WHEN/WHAT to meter is the seller's own pricing decision — the skill does NOT prescribe a metering strategy. For plain usage-based (PAYG) the default is meter all billable usage and let pricing/offers handle any free allowance. Billing only BEYOND a contract-included quantity is Contract with Consumption (CCP) — OUT OF SCOPE (see below) → public docs.
  • Usage ingestion is the seller's responsibility. The seller writes rows into the usage table (incl. cross-account collection if multi-account). In the default full mode the PIPELINE aggregates hourly; in direct-submit mode the seller supplies finalized hourly totals (see Q9).
  • All timestamps are UTC — the seller stamps the sort-key timestamp AND
    meteringPending
    in UTC; the GSI is queried by UTC hour bucket, so a local-time write mis-files/mis-meters the row silently.
  • Audit timestamps (
    createdAt
    /
    updatedAt
    )
    — every row the pipeline writes carries
    createdAt
    (write-once,
    if_not_exists
    ) and
    updatedAt
    (refreshed each write), ISO-8601 whole-second UTC, applied uniformly via a shared stamp helper. These are audit-only metadata and do NOT gate metering — metering decisions follow
    subscriptionStatus
    / the
    metering_pending
    GSI
    , never
    createdAt
    /
    updatedAt
    .
  • CustomerAWSAccountId(而非 CustomerIdentifier)+ 每个 UsageRecord 的 LicenseArn,用于新的(并发协议)产品。
  • 订阅事件使用 EventBridge(而非 SNS)——仅在 us-east-1 发出。
  • ResolveCustomer
    在买家从 Marketplace 控制台重定向到 SaaS 注册站点时,从
    x-amzn-marketplace-token
    解析买家的
    LicenseArn
    、
    customerAWSAccountId
    和
    productCode
    ;然后 SaaS 收集额外的注册数据并将其持久化(PII 字段)到区域内 customer-profile 表(无 PII 的订阅者表仅获得 CA 身份 + 状态 +
    registeredRegions
    )。它返回顶级
    LicenseArn
    字段——
    CustomerIdentifier
    是已弃用的旧字段,对于新的 SaaS 集成为 null(不要使用它,也不要将其值视为 LicenseArn)。它是区域性 API,但该区域是卖家的自由选择——不必与买家使用软件的位置匹配,也不限制计量。
    BatchMeterUsage
    可以在任何受支持的区域调用,独立于调用
    ResolveCustomer
    的区域。它不是仅限 us-east-1 的 API(只有
    License Updated
    等 EventBridge 订阅事件是仅限 us-east-1 的)。必须从发布 SaaS 产品的账户调用(否则 token 将无法解析)。
    x-amzn-marketplace-token
    不是一次性的——它可重复使用直到过期(约 4 小时)
    :在过期前重新解析同一 token 会返回相同结果,因此注册页面出错的买家可以在约 4 小时窗口内使用原始链接重试。过期后
    ResolveCustomer
    返回
    ExpiredTokenException
    (已消费/无效 token 返回
    InvalidTokenException
    ),买家重新点击"Set up your account"以获取新的 token(无需重新订阅)。几天后重试 = token 过期 → 生成新的。(
    references/troubleshooting.md
    。)
  • DuplicateRecord = 首次写入优先并已报告(
    Status = DuplicateRecord
    ,而非
    Success
    );原始数量保持已计费。以不同数量重新提交不会被计费(少计风险)。仅对相同重试安全——绝不是更正数量的方法;绝不可称之为"良性"。
  • CustomerNotSubscribed = 真实失败,不要重试。原因:(1)标识符无效,(2)协议已结束/过期,(3)买家账户被暂停。
  • 错误的
    LicenseArn
    是请求级的
    InvalidLicenseException
    ,会使整个
    BatchMeterUsage
    批次失败
    ——不是
    Results
    中的逐记录
    Status
    (不同于
    CustomerNotSubscribed
    )。缓解措施:在批处理前于客户端进行 LicenseArn 形状验证(原因
    InvalidLicenseArn
    ),使格式错误的 ARN 永远不会进入批次;在请求级
    ClientError
    时二分并重试以隔离问题记录,使其同批次的记录仍然计量。
  • BatchMeterUsage
    (SaaS)是区域性的,但卖家选择区域——没有
    InvalidEndpointRegionException
    。
    在用量的区域调用它(卖家选择;本地部署/其他云选择最近的支持区域);每个计量区域部署一个堆栈;去重按区域进行。它在大多数商业区域受支持,但并非全部——在所选区域 API 不可用时调用返回
    DisabledApiException
    (改为在受支持的区域计量)。SaaS
    BatchMeterUsage
    没有
    InvalidEndpointRegionException
    ;卖家选择区域,按区域去重使多区域计量正确,而非错误。
    • 不要将其与 AMI/Container
      MeterUsage
      混淆。
      计量 AMI/Container 产品超出此 skill 的范围(重定向到公开文档),其区域规则相反:
      MeterUsage
      必须在运行中的 EC2 实例 / ECS 任务所在的同一区域调用——无论实例运行在哪里,都不能从 us-east-1(或任何其他区域)调用;区域不匹配返回
      InvalidEndpointRegionException
      。
      此类产品使用 IAM 实例/任务角色(EC2 实例配置文件、ECS 任务角色或 EKS IRSA)进行认证,绝不使用嵌入式/长期访问密钥。此同区域强制 +
      InvalidEndpointRegionException
      仅适用于 AMI/Container
      MeterUsage
      ,不适用于 SaaS
      BatchMeterUsage
      。参见公开 AWS 文档了解 AMI/Container 计量。
  • 维度有效性由服务端权威决定。 管道未配置维度列表,也不在客户端拒绝;未定义的维度会使
    BatchMeterUsage
    调用以请求级
    InvalidUsageDimensionException
    失败(区分大小写,不计费)——它不是逐记录
    Results[].Status
    。管道通过二分(深度限制为 ⌈log2(批次大小)⌉ = 5,因此大量失败的批次不会使提交器过载)隔离违规记录,将其标记为
    RejectedClientSide
    (原因 = 异常名称),并发出
    BatchMeterUsageException
    指标。添加/重命名目录维度无需重新部署。
  • 拒绝原因代码以客户端 ↔ 服务端成对出现——始终同时呈现两者。 一行可能以两种方式被拒绝,规则期望每个都有命名的代码:(1)管道的客户端护栏在 API 调用前拒绝该行并盖上客户端原因代码(
    MalformedTimestamp
    、
    InvalidLicenseArn
    、
    TooManyTags
    、
    InvalidDimension
    等),将其标记为
    RejectedClientSide
    并发出
    UsageRecordRejected
    ;(2)如果一行到达 API 并在那里被拒绝,服务端对应物是匹配的
    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 事件历史(默认,无需设置),过滤
    eventSource = metering-marketplace.amazonaws.com
    ——计量查找绝不使用 CloudTrail Lake /
    create-event-data-store
    。
  • 时间戳窗口 = 24 小时(不是 1 小时),从
    now
    起算;时间戳超过 24 小时的记录超出窗口并被拒绝——不会被提交。 具体来说,30 小时前的记录已超过 24 小时边界,不在窗口内:服务端会以
    TimestampOutOfBoundsException
    使调用失败,一个坏时间戳会拒绝整个批次;在到达 API 之前,管道的客户端护栏以原因代码
    MalformedTimestamp
    拒绝该行(标记为
    RejectedClientSide
    ,不计费),因此过期/超窗时间戳永远不会进入批次。不要告诉卖家 >24 小时(例如 30 小时)的记录"在窗口内"或"应该提交"——它会被拒绝;唯一的例外是 6 小时月边界宽限期,它接受上个月的记录直到 1 日 06:00 UTC。管道只计量
    now-23h … now-1h
    中已完成的小时(绝不计量进行中/当前小时——那只能由取消供应刷新计量),最旧优先,由
    MeteringLockHours
    门控;跳过/失败的小时发现器或
    rate(5m)
    提交器运行会在稍后运行中自恢复,只要该小时仍在 24 小时窗口内。详情:
    references/scalable-metering-design.md
    。
    • 最佳实践——在生成后约 1 小时内提交用量。 延迟提交是最大的真实世界失败模式:行越接近 24 小时边界,重试余量越少,超过 24 小时就永久丢失(
      TimestampOutOfBounds
      ,不计费)。及时写入/提交用量(在窗口内),而不是在边界批量提交一整天的量。
    • 老化是终态的并清除待处理标记。 当一行/聚合在其超过 24 小时窗口之前无法计量时,管道将其老化:它设置终态
      meteringStatus
      (从未聚合的原始行为
      AggregationExpired
      ,从未提交的聚合行为
      SubmissionExpired
      )并清除
      meteringPending
      ,使该行从稀疏的
      metering_pending
      GSI 中消失,发现器的最旧优先向后遍历停止返回它(不会无限重新处理)。这些触发
      UsageAggregationExpired
      /
      UsageSubmissionExpired
      告警——分页信号表明用量静默地从未计量;调查写入器/管道延迟。
    • 约 1 小时前的时间戳通常不应得到
      TimestampOutOfBoundsException
      ——1 小时远在 24 小时回看窗口内,所以这很不寻常。不要告诉卖家"等到 HH:05"。真正原因几乎总是(a)一个坏记录使整个批次失败(同批次的格式错误/超窗/未定义维度记录使请求级异常拒绝调用中的所有记录,直到被隔离)或(b)时钟偏差(未同步的主机在未来/过去很远的时间戳,或使用本地时间而非 UTC)。修复:检查批次中的格式错误记录(二分隔离它)并验证写入器的时钟已 NTP 同步并标记整秒 UTC。
  • 当前(进行中)小时在正常路径上绝不计量。 管道只提交已完成的小时(
    now-23h … now-1h
    );当前进行中的小时保持开放,直到完成才被跳过。
    MeteringLockHours
    (默认 1,验证 1..20)是一个小时在聚合/提交前保持开放以接收延迟用量的小时数——默认 1 时,管道处理刚刚完成的小时。待处理的小时桶最旧优先排空,因此最旧的用量在接近 24 小时边界并老化(
    TimestampOutOfBounds
    )之前被提交。聚合器将每个
    (licenseArn, customerAWSAccountId, dimension, hour)
    组折叠为每小时恰好一条
    BatchMeterUsage
    记录
    (计量按组按小时,首次写入优先)。唯一计量当前进行中小时的时间是取消供应的最终用量刷新(绕过锁,见事件模型)。所以"为什么这个小时还没有显示用量?" = 它仍然是当前小时和/或仍在
    MeteringLockHours
    内——预期行为,不是 bug。
  • BatchMeterUsage
    限制是两个独立的上限:≤25 条 UsageRecord 且 ≤1 MB 请求负载。
    包含 25 条有效记录的批次仍可能因大小而被拒绝,如果序列化请求超过 1 MB(大/多的
    usageAllocations
    标签集、长 ARN)——是字节大小,而非记录数。修复:每次调用放更少的记录,使每个请求保持在 1 MB 以下。(提交器已合并每调用 ≤25 条;大小拒绝意味着这些记录太大——进一步拆分。)
  • VMT(供应商计量标签)标签限制 = 每条 UsageRecord 5 个
    usageAllocations
    标签。
    用量行在其
    usageAllocations
    中最多携带 5 个标签;超过 5 个无效。管道的客户端护栏在 API 调用前以原因代码
    TooManyTags
    拒绝超标签行(标记为
    RejectedClientSide
    ,不计费,发出
    UsageRecordRejected
    );如果这样的行到达 API,服务端对应物是
    TooManyTagsException
    。当被问及 VMT 标签上限时,同时说出 5 个标签的最大值和
    TooManyTags
    客户端拒绝(不仅仅是数字)。VMT 基于存在性——卖家提供标签;管道绝不发明它们。
  • VMT 按组全有或全无——不要混合带标签和未标签的行。 在一个
    (licenseArn, customerAWSAccountId, dimension, hour)
    组内,要么每条非零行都携带
    usageAllocations
    ,要么都不携带。在同一组中混合有分配和无分配的非零行是不允许的——管道以原因代码
    MixedAllocatedAndUnallocated
    在客户端拒绝它(
    RejectedClientSide
    ,不计费)。这不是"部分 VMT 归属"。零数量行对此检查被忽略,同一小时/批次中的不同组可以独立地有或无分配。另外按组:每行的
    AllocatedUsageQuantity
    值必须等于该行的
    quantity
    ,且每条 UsageRecord 合并的标签集 ≤2500(否则
    AllocationSumMismatch
    /
    TooManyAllocations
    )。详情:
    references/setup-path.md
    、
    references/client-side-guardrails.md
    。
  • 计量是幂等的(按区域在
    CustomerAWSAccountId + LicenseArn + dimension + hour
    上首次写入优先去重)
    ,管道是解耦的——没有单一的计量 Lambda,也没有"预留并发 = 2"的设置。提交器(唯一调用
    BatchMeterUsage
    的阶段)以
    ReservedConcurrentExecutions=1
    ——单个串行提交器
    运行,故意串行以保持在
    BatchMeterUsage
    请求速率限制内(记录配额:每账户每区域 10 请求/秒;
    MeterUsage
    10/s,
    RegisterUsage
    5/s——参见服务配额)并避免
    ThrottlingException
    。发现器以预留并发 30 运行;聚合器和
    清理器
    通过其 SQS 事件源映射
    MaximumConcurrency
    扩展。SQS 吸收突发——吞吐量来自排空队列,而非提高提交器并发。如果卖家需要更高的持续计量 TPS,联系 AWS Marketplace 卖家运营,而不是将并发提高到安全水平以上。
    • ThrottlingException
      处理——带抖动的指数退避。
      在
      ThrottlingException
      (或受限的
      UnprocessedRecords
      )时,使用带抖动的指数退避(随机化延迟——明确说出抖动;不要使用固定/同步的重试间隔,那会再次冲突)重试。保持每次调用在 ≤25 条记录(和 ≤1 MB) 限制内——不要增大批次来"追赶"。重试是安全的,因为计量是幂等的(首次写入优先):重新发送的已计费记录只会作为
      DuplicateRecord
      返回,原始数量仍然计费。不要将
      DuplicateRecord
      视为避免重试受限/未处理记录的理由——正是幂等性保证使重试安全(唯一注意事项是绝不以不同数量重新提交同一组,那不会被计费)。
  • 注册表单数据(PII)位于每区域
    customer-profile
    表中,而非订阅者表。
    区域内的注册 Lambda 将允许列表的注册/KYB 字段写入
    awsmp-<productCode>-customer-profile
    (PK
    licenseArn
    ,SK
    customerAWSAccountId
    ,卖家请求的 GSI)在其运行的区域中,保持买家 PII 在区域内。配置文件行始终为已注册买家 upsert——即使零额外表单字段(按身份键控的行总是被写入)。统一的订阅者表(us-east-1)是无 PII 的——注册 Lambda 只写入非 PII 的
    productCode
    + 幂等的
    registeredRegions
    追加(无注册表单数据)——加上 CA 身份(
    licenseArn
    、
    customerAWSAccountId
    、
    agreementId
    )和两个生命周期状态——因此即使对于选择加入区域的产品,在 us-east-1 也是安全的。模板始终设置注册 Lambda 的
    CUSTOMER_PROFILE_TABLE
    环境变量;如果未设置,那是错误配置,处理器将大声失败(HTTP 500),而不是静默跳过 PII 写入。(现有卖家自己的注册处理器可能以不同方式存储数据——skill 会适应,不强制逐字使用此参考 Lambda。)
  • 旧版(CA 前)集成可能使用
    ProductCode
    +
    CustomerIdentifier
    而非
    LicenseArn
    。skill 不对它们强加自己的模板/回退(它不得呈现固定的
    startswith("arn:")
    回退作为其模板)——它以卖家现有集成 + 公开 AWS 文档为基础(
    references/existing-sellers.md
    )。
  • 免费试用——行为取决于哪个模型(不要给出笼统的"始终计量"答案);先确认模型,然后将细节基于公开文档(SaaS 免费试用、PLG 指南):
    • AWS Marketplace SaaS 免费试用功能(原生、限时:买家订阅单独的试用优惠,必须在结束前升级):全程正常计量——不要扣留。AWS 应用试用定价,因此试用用量以 $0(
      $0
      估计收入)开票,转换后相同维度计费,无需集成更改。
    • 嵌入式免费层 / 按用量试用(买家在标准付费优惠上,卖家决定何时开始收费):计量什么/何时计量是卖家自己的定价决策——skill 不规定计量策略。对于纯按用量(PAYG),默认是计量所有可计费用量,让定价/优惠处理任何免费额度。仅对合同中包含数量之外计费是含消费合同(CCP)——超出范围(见下文)→ 公开文档。
  • 用量摄取是卖家的责任。 卖家将行写入用量表(多账户时包括跨账户收集)。在默认的完整模式下,管道按小时聚合;在直接提交模式下,卖家提供最终的小时总计(见 Q9)。
  • 所有时间戳均为 UTC——卖家以 UTC 标记排序键时间戳和
    meteringPending
    ;GSI 按 UTC 小时桶查询,因此本地时间写入会静默地错放/错计该行。
  • 审计时间戳(
    createdAt
    /
    updatedAt
    )
    ——管道写入的每一行都携带
    createdAt
    (只写一次,
    if_not_exists
    )和
    updatedAt
    (每次写入刷新),ISO-8601 整秒 UTC,通过共享的戳记辅助函数统一应用。这些是仅审计元数据,不门控计量——计量决策遵循
    subscriptionStatus
    /
    metering_pending
    GSI
    ,从不遵循
    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
CustomerAWSAccountId
+
LicenseArn
(never the legacy
CustomerIdentifier
). Explain these facts explicitly whenever a CA registration, event-ordering, attribution, or lookup question comes up:
  • Registration → subscriber upsert is driven by the EventBridge
    License Updated
    event.
    When a buyer subscribes, AWS Marketplace emits
    License Updated
    (us-east-1 only). The events-stack subscription Lambda consumes it and UPSERTS the subscriber record (keyed by
    licenseArn
    +
    customerAWSAccountId
    , with
    agreementId
    ) setting
    agreementStatus=active
    and
    subscriptionStatus=active
    — this is what makes the buyer eligible to meter.
    ResolveCustomer
    (called from the branded registration page) resolves the token to
    LicenseArn
    +
    customerAWSAccountId
    +
    productCode
    and persists registration/PII to the in-region customer-profile table, but it is the
    License Updated
    upsert (agreementStatus=active)
    that establishes the metering-eligible subscriber row. Do not describe CA registration without naming the
    License Updated
    →
    agreementStatus=active
    upsert.
  • ResolveCustomer
    DOES return the
    LicenseArn
    — the answer is YES.
    ResolveCustomer
    resolves the
    x-amzn-marketplace-token
    and returns a top-level
    LicenseArn
    field, alongside
    customerAWSAccountId
    and
    productCode
    . Do NOT say
    ResolveCustomer
    does not return a LicenseArn — it does, at the top level of the response.
    CustomerIdentifier
    is a deprecated legacy field that is null for new CA integrations — do NOT use it or treat its value as the LicenseArn. (For a new SaaS product the
    LicenseArn
    from
    ResolveCustomer
    is the identifier you persist and later pass to
    BatchMeterUsage
    .)
  • Eligibility is enforced at
    BatchMeterUsage
    , independent of event arrival order.
    Metering eligibility is checked server-side by
    BatchMeterUsage
    at submit time against the current agreement/subscription state — it does NOT depend on the order in which EventBridge events arrived. If usage is submitted before the subscriber is active (or after the agreement ended) the record returns
    CustomerNotSubscribed
    ; once
    License Updated
    has made the buyer active, the same record meters. So out-of-order or late-arriving events do not corrupt billing:
    BatchMeterUsage
    is the authority, and the row self-recovers on a later
    rate(5m)
    run while still inside the 24h window. Do not claim the pipeline gates metering on event ordering — it gates on eligibility at submit time.
  • Metering is attributed PER LICENSE, not per account — reinforce the usage-table key model. The raw usage table is PK
    licenseArn
    , SK
    customerAWSAccountId#dimension#timestamp
    . One AWS account (
    customerAWSAccountId
    ) can hold MULTIPLE agreements/licenses, and dedup + billing are scoped to
    CustomerAWSAccountId + LicenseArn + dimension + hour
    — so usage is aggregated and metered per
    licenseArn
    , never collapsed to a single per-account total. When asked "meter per account or per license?" the answer is per license (the
    licenseArn
    is the partition key).
  • The
    metering_pending
    GSI drives metering; the subscribers table is lifecycle-only.
    Discovery of what to meter comes ONLY from the sparse
    metering_pending
    GSI
    on the usage/aggregated tables (HASH =
    meteringPending
    hour-bucket, RANGE =
    licenseArn
    , projecting
    customerAWSAccountId
    +
    dimension
    ). The discoverer PAGINATES through the GSI — it runs a paginated
    Query
    (following
    LastEvaluatedKey
    across pages), NOT a single read — so an hour with more pending groups than one page still fully drains. The subscribers table is NOT consulted to find usage to meter — it holds only CA identity + the two lifecycle statuses (
    agreementStatus
    /
    subscriptionStatus
    ) and is used for lifecycle/deprovisioning (is this license active? has it entered the deprovisioning flush?). Do not say the pipeline scans or queries the subscribers table to locate pending usage — that is the GSI's job.
    • Unified subscribers table schema (us-east-1, PII-FREE): PK
      licenseArn
      , SK
      customerAWSAccountId
      , with attributes
      agreementId
      ,
      agreementStatus
      ,
      subscriptionStatus
      ,
      productCode
      ,
      registeredRegions
      ,
      deprovisioningExpiry
      (set only during a deprovisioning flush), and
      createdAt
      /
      updatedAt
      audit stamps.
      registeredRegions
      is a set updated idempotently (append via
      ADD
      , no duplicates) and is reference-only — it NEVER gates metering. It carries NO registration form data / NO PII (PII lives only in the in-region
      customer-profile
      table). A
      customerAWSAccountId
      GSI supports account-scoped lookups. Why
      licenseArn
      is the PK (all THREE reasons):
      (1) under Concurrent Agreements one buyer can hold MULTIPLE concurrent licenses for the same product; (2) using
      customerAWSAccountId
      as the PK would COLLAPSE those distinct agreements into one row (lost usage / double-billing); (3) each license has its own INDEPENDENT lifecycle and metering idempotency (dedup is per
      CustomerAWSAccountId + LicenseArn + dimension + hour
      ), so per-agreement keying keeps them isolated. Name all three when asked why
      licenseArn
      is the partition key.
  • ONE PRODUCT PER
    BatchMeterUsage
    CALL — do NOT mix products in one batch.
    Every
    UsageRecord
    (every
    LicenseArn
    ) in a single
    BatchMeterUsage
    request MUST belong to the SAME product. Mixing
    LicenseArn
    s from two different products in one call returns a request-level
    InvalidLicenseException
    and fails the ENTIRE request (nothing meters). Make a SEPARATE
    BatchMeterUsage
    call per product. Within that single-product batch you MAY combine different dimensions and different buyers/licenses of that one product; dedup remains per
    CustomerAWSAccountId + LicenseArn + dimension + hour
    , and the ≤25-record and ≤1 MB caps apply to the batch. (The deployed submitter already groups by product so it never co-batches products; a seller batching by hand must do the same.)
  • Batch cap = ≤25 UsageRecords AND ≤1 MB (two independent limits) — see the Critical Facts entry; a 25-record batch can still be rejected for the 1 MB payload size. So N licenses × M dimensions = N·M records ⇒ ⌈N·M/25⌉ sequential calls (e.g. 50 licenses × 2 dimensions = 100 records ⇒ 4 calls, submitted serially). Retry behavior: any records returned in
    UnprocessedRecords
    (transient
    ThrottlingException
    /
    InternalServiceException
    , not billed) are retried — the deployed submitter retries once then re-drives them on the next scheduled run — because metering is idempotent (first-write-wins); do not drop them.
  • The events stack (EventBridge + subscribers table) is in us-east-1 only — the metering (main) stack can live in ANOTHER region. Marketplace subscription events (
    License Updated
    ,
    License Deprovisioned
    , etc.) are emitted ONLY in us-east-1, so the shared events stack and its PII-free subscribers table are pinned to us-east-1 (co-located with the consumer subscription Lambda). But the per-region main/metering stack (usage table + pipeline Lambdas +
    BatchMeterUsage
    ) is deployed in whatever region(s) the seller meters from
    — only the events+subscribers stack is pinned to us-east-1. When the metering region ≠ us-east-1 the pipeline reads the subscribers table cross-region; if the seller meters in us-east-1 too, everything is single-region.
所有新的 SaaS 产品使用并发协议:身份是
CustomerAWSAccountId
+
LicenseArn
(绝不是旧的
CustomerIdentifier
)。每当出现 CA 注册、事件排序、归属或查找问题时,明确说明这些事实:
  • 注册 → 订阅者 upsert 由 EventBridge
    License Updated
    事件驱动。
    当买家订阅时,AWS Marketplace 发出
    License Updated
    (仅 us-east-1)。事件堆栈订阅 Lambda 消费它并 UPSERT 订阅者记录(按
    licenseArn
    +
    customerAWSAccountId
    键控,带
    agreementId
    ),设置
    agreementStatus=active
    和
    subscriptionStatus=active
    ——这使买家有资格计量。
    ResolveCustomer
    (从品牌注册页面调用)将 token 解析为
    LicenseArn
    +
    customerAWSAccountId
    +
    productCode
    ,并将注册/PII 持久化到区域内的客户配置文件表,但正是
    License Updated
    upsert(agreementStatus=active)
    建立了有计量资格的订阅者行。描述 CA 注册时,必须说出
    License Updated
    →
    agreementStatus=active
    upsert。
  • ResolveCustomer
    确实返回
    LicenseArn
    ——答案是肯定的。
    ResolveCustomer
    解析
    x-amzn-marketplace-token
    并返回顶级
    LicenseArn
    字段,以及
    customerAWSAccountId
    和
    productCode
    。不要说
    ResolveCustomer
    不返回 LicenseArn——它确实返回,在响应的顶级。
    CustomerIdentifier
    是已弃用的旧字段,对于新的 CA 集成为 null——不要使用它或将其值视为 LicenseArn。(对于新的 SaaS 产品,来自
    ResolveCustomer
    的
    LicenseArn
    是您持久化并稍后传递给
    BatchMeterUsage
    的标识符。)
  • 资格在
    BatchMeterUsage
    处强制执行,独立于事件到达顺序。
    计量资格由
    BatchMeterUsage
    在提交时根据当前协议/订阅状态在服务端检查——它不依赖于 EventBridge 事件到达的顺序。如果在订阅者激活之前(或协议结束后)提交用量,记录返回
    CustomerNotSubscribed
    ;一旦
    License Updated
    使买家激活,相同的记录就会计量。因此乱序或延迟到达的事件不会破坏计费:
    BatchMeterUsage
    是权威,行在仍在 24 小时窗口内的稍后
    rate(5m)
    运行中自恢复。不要说管道以事件顺序门控计量——它在提交时以资格门控。
  • 计量按许可证归属,而非按账户——强化用量表键模型。原始用量表为 PK
    licenseArn
    ,SK
    customerAWSAccountId#dimension#timestamp
    。一个 AWS 账户(
    customerAWSAccountId
    )可以持有多个协议/许可证,去重和计费范围是
    CustomerAWSAccountId + LicenseArn + dimension + hour
    ——因此用量按
    licenseArn
    聚合和计量,绝不折叠为单个按账户总计。当被问"按账户还是按许可证计量?"答案是按许可证(
    licenseArn
    是分区键)。
  • metering_pending
    GSI 驱动计量;订阅者表仅用于生命周期。
    发现要计量的内容仅来自用量/聚合表上的稀疏
    metering_pending
    GSI
    (HASH =
    meteringPending
    小时桶,RANGE =
    licenseArn
    ,投影
    customerAWSAccountId
    +
    dimension
    )。发现器分页遍历 GSI——它运行分页的
    Query
    (跨页跟随
    LastEvaluatedKey
    ),而非单次读取——因此超过一页的待处理组小时也会完全排空。不查询订阅者表来查找要计量的用量——它只持有 CA 身份 + 两个生命周期状态(
    agreementStatus
    /
    subscriptionStatus
    ),用于生命周期/取消供应(此许可证是否激活?是否已进入取消供应刷新?)。不要说管道扫描或查询订阅者表来定位待处理用量——那是 GSI 的工作。
    • 统一订阅者表模式(us-east-1,无 PII): PK
      licenseArn
      ,SK
      customerAWSAccountId
      ,属性
      agreementId
      、
      agreementStatus
      、
      subscriptionStatus
      、
      productCode
      、
      registeredRegions
      、
      deprovisioningExpiry
      (仅在取消供应刷新期间设置)以及
      createdAt
      /
      updatedAt
      审计戳记。
      registeredRegions
      是一个幂等更新的集合(通过
      ADD
      追加,无重复),且仅参考——绝不门控计量。它不携带注册表单数据 / 无 PII(PII 仅在区域内的
      customer-profile
      表中)。
      customerAWSAccountId
      GSI 支持账户范围查找。为什么
      licenseArn
      是 PK(所有三个原因):
      (1)在并发协议下,一个买家可以为同一产品持有多个并发许可证;(2)使用
      customerAWSAccountId
      作为 PK 会将那些不同的协议折叠为一行(丢失用量 / 双重计费);(3)每个许可证有自己的独立生命周期和计量幂等性(去重按
      CustomerAWSAccountId + LicenseArn + dimension + hour
      ),因此按协议键控保持它们隔离。被问为什么
      licenseArn
      是分区键时说出所有三个。
  • 一次
    BatchMeterUsage
    调用只针对一个产品——不要在一个批次中混合产品。
    单个
    BatchMeterUsage
    请求中的每条
    UsageRecord
    (每个
    LicenseArn
    )必须属于同一产品。在一次调用中混合来自两个不同产品的
    LicenseArn
    会返回请求级
    InvalidLicenseException
    并使整个请求失败(什么都不计量)。每个产品单独调用
    BatchMeterUsage
    。在该单产品批次中,您可以组合该产品的不同维度和不同买家/许可证;去重仍按
    CustomerAWSAccountId + LicenseArn + dimension + hour
    ,≤25 条记录和 ≤1 MB 上限适用于批次。(已部署的提交器已按产品分组,因此它从不跨产品批处理;手动批处理的卖家必须同样操作。)
  • 批次上限 = ≤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
    、
    License Deprovisioned
    等)仅在 us-east-1 发出,因此共享事件堆栈及其无 PII 订阅者表固定在 us-east-1(与消费者订阅 Lambda 同区域)。但每区域主/计量堆栈(用量表 + 管道 Lambda +
    BatchMeterUsage
    )部署在卖家计量的任何区域
    ——只有事件+订阅者堆栈固定在 us-east-1。当计量区域 ≠ us-east-1 时,管道跨区域读取订阅者表;如果卖家也在 us-east-1 计量,则一切都在单区域。

Response Rules — MANDATORY

响应规则——强制

  1. Check Out of Scope FIRST — if out of scope, decline in one sentence; do NOT proceed.
  2. KNOWLEDGE questions (how do I, how does X work, which data source): answer from this skill; do NOT call AWS APIs/tools.
  3. ACTION questions (deploy now, query now, run this): execute AWS commands — prefer the
    shell
    tool with AWS CLI over MCP
    use_aws
    for long-running ops.
  4. Multi-region/multi-product questions are KNOWLEDGE — explain the architecture; do NOT deploy unless explicitly asked.
  5. "How do I find X" = KNOWLEDGE — explain the method; execute only on "show me"/"run it".
  1. 首先检查范围外——如果超出范围,用一句话拒绝;不要继续。
  2. 知识问题(如何做、X 如何工作、哪个数据源):从本 skill 回答;不要调用 AWS API/工具。
  3. 操作问题(立即部署、立即查询、运行此操作):执行 AWS 命令——对于长时间运行的操作,优先使用带 AWS CLI 的
    shell
    工具而非 MCP
    use_aws
    。
  4. 多区域/多产品问题是知识——解释架构;除非明确要求,否则不要部署。
  5. "我如何找到 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 +
    GetEntitlement
    -then-
    BatchMeterUsage
    -overage; AND (b) any contract-term or renewal question (e.g. "my customer's ANNUAL CONTRACT is renewing next week — do I change my metering setup?"). A contract term/renewal is a SaaS Contract (entitlement) concern, not a PAYG metering concern. Do NOT answer these from metering knowledge — in particular, do NOT tell the seller that a renewal just re-fires
    License Updated
    / needs no metering change, do NOT walk through the CCP overage flow, and do NOT describe entitlement checks. The skill does not own contract/entitlement behavior and must not assert how a contract renewal maps to metering events. Reply ONLY with the redirect: "SaaS Contract / Contract with Consumption Pricing — including contract terms, renewals, included units, and overage — is out of scope for this usage-based (PAYG) metering skill. Use the public AWS documentation: Contract pricing, Contract pricing with consumption (CCP), SaaS contract integration, and entitlements + renewals via GetEntitlements."
  • Flat-rate / SaaS Subscription products with NO usage metering — verifying a customer is still active/entitled (e.g. "flat monthly subscription, no metering, how do I check customers are still active?"): this is subscription entitlement management (SaaS Subscriptions +
    GetEntitlements
    / entitlement EventBridge events), NOT usage-based (PAYG) metering. It is OUT OF SCOPE — do NOT design an active-customer check, describe
    GetEntitlements
    /entitlement-event handling, or repurpose this skill's metering subscriber table for it. Reply ONLY with the redirect: "A flat-rate SaaS Subscription with no usage metering is out of scope for this usage-based (PAYG) metering skill. Verifying active/entitled customers is done via AWS Marketplace SaaS entitlements — see the public AWS documentation: SaaS subscriptions, checking entitlements with GetEntitlements, and subscription lifecycle events with EventBridge."
  • Forecasting / predicting FUTURE bills or usage (e.g. "forecast next month's customer bills from my CloudTrail metering trend", trend projection, revenue prediction): OUT OF SCOPE — this skill queries only HISTORICAL submissions (≤90-day CloudTrail) and billed history (Seller Reports); it does NOT forecast → "Forecasting future bills/usage is out of scope. This skill queries only historical metering submissions; for projections, hand the fetched CloudTrail/Seller-Reports data to your host agent/model for analysis, or use AWS Cost forecasting / your own analytics over Seller Reports."
  • AWS account billing disputes → "Contact AWS Support"
  • Anything not involving BatchMeterUsage, ResolveCustomer, metering records, or usage-based billing for SaaS/software.

仅回复"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 +
    GetEntitlement
    -then-
    BatchMeterUsage
    -超额;以及(b)任何合同期限或续订问题(例如"我客户的年度合同下周续订——我需要更改计量设置吗?")。合同期限/续订是 SaaS Contract(权利) 关注点,而非 PAYG 计量关注点。不要从计量知识回答这些问题——特别是不要告诉卖家续订只是重新触发
    License Updated
    / 不需要计量更改,不要走 CCP 超额流程,也不要描述权利检查。该 skill 不拥有合同/权利行为,不得断言合同续订如何映射到计量事件。仅回复重定向:"SaaS Contract / 含消费定价合同——包括合同条款、续订、包含单位和超额——超出此按用量(PAYG)计量 skill 的范围。使用公开 AWS 文档:合同定价、含消费合同定价(CCP)、SaaS 合同集成 以及 通过 GetEntitlements 的权利 + 续订。"
  • 固定费率 / 无用量计量的 SaaS 订阅产品——验证客户是否仍然活跃/有权利(例如"固定月度订阅,无计量,如何检查客户是否仍然活跃?"):这是订阅权利管理(SaaS 订阅 +
    GetEntitlements
    / 权利 EventBridge 事件),而非按用量(PAYG)计量。它超出范围——不要设计活跃客户检查、描述
    GetEntitlements
    /权利事件处理,或为此重用本 skill 的计量订阅者表。仅回复重定向:"无用量计量的固定费率 SaaS 订阅超出此按用量(PAYG)计量 skill 的范围。验证活跃/有权利客户通过 AWS Marketplace SaaS 权利完成——参见公开 AWS 文档:SaaS 订阅、使用 GetEntitlements 检查权利 以及 使用 EventBridge 的订阅生命周期事件。"
  • 预测 / 预估未来账单或用量(例如"根据我的 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.md
+
references/deployment-steps.md
.
完整演练:
references/setup-path.md
+
references/deployment-steps.md
。

Seller Questionnaire (ask ALL before generating code)

卖家问卷(在生成代码前询问所有)

  • Q1 Product code (or product ID). Accept either; resolve a product ID to the code via the Catalog API (
    describe-entity
    ) at validation. The dimension list is NOT a deploy input (dimension validity is server-authoritative); you may list dimensions for the seller's info only. Recommend validating dimension NAMES against the catalog with the Marketplace Catalog API
    DescribeEntity
    (us-east-1)
    before the seller wires them into usage rows — the returned entity details enumerate the product's registered dimension API names (case-sensitive). Confirming the seller's dimension strings match the catalog exactly at setup time is the way to avoid the runtime
    InvalidUsageDimensionException
    (an undefined/misspelled/wrong-case dimension fails the whole
    BatchMeterUsage
    call). This is a validation aid only — it does NOT make the dimension list a deploy input.
  • Q2 Are you the ISV/seller? Only the seller (publisher) or an authorized 3PI may integrate; a 3PI is out of scope — decline. Confirm publishing-account access.
  • Q3 Which region(s) do you meter from? Events stack is always us-east-1; deploy a main stack in EACH region where usage occurs and meter in-region. Dedup is per
    CustomerAWSAccountId + LicenseArn + dimension + hour
    , scoped to the submission region — do NOT funnel all usage into one region. Region support.
  • Q4 Stage name (MANDATORY). API Gateway stage (e.g.
    v1
    /
    live
    /
    prod
    — seller's choice). No default, no fallback to
    prod
    ; deploy fails fast if empty. Pass via
    STAGE_NAME
    .
  • Q5 Does a stack already exist for this product? If yes → modify/extend: gather context and inspect the seller's existing stack (may use arbitrary naming) before changing anything — see
    references/existing-sellers.md
    .
  • Q6 Metering lock hours (
    METERING_LOCK_HOURS
    →
    MeteringLockHours
    , default 1, validated 1..20). How many hours an hour stays open for late usage before it is aggregated/submitted (default 1 = process the just-completed hour). At the cap, an hour is first submitted at age 20, leaving ≥4h in the 24h billable window for retries/re-drive. Month-aware: previous-month hours keep the lock until 03:00 UTC on the 1st, then no-lock (submit each run) until the 06:00 UTC cutoff, and aren't aged out before then; a previous-month record written after 03:00 must already be pre-aggregated (else
    DuplicateRecord
    ).
  • Q7 TTL / retention (per table,
    deploy.sh
    env vars;
    0
    disables):
    • Raw usage table
      USAGE_TABLE_TTL_DAYS
      (default 365) — RECOMMEND on (high-volume table). The seller's writer sets the
      ttl
      epoch attribute; the template only makes DynamoDB honor it. Floor ≥2 days that must exceed
      MeteringLockHours
      +24h so a row can't expire before metering.
    • Aggregated table
      AGGREGATED_USAGE_TTL_DAYS
      (default 0 = retain) — RECOMMEND omit/large (small billing audit trail). When >0 the submitter sets
      ttl
      only on
      Success
      rows.
    • Enabling TTL needs the deployer's
      dynamodb:UpdateTimeToLive
      (already in the policy).
  • Q8 Create health dashboards? (
    CREATE_DASHBOARD
    , default true). Per-product main-stack dashboard (per-Lambda Errors/Throttles/Invocations/Duration; queue/DLQ depth + oldest-age; EMF
    UsageRecordRejected
    by reason,
    UsageRecordUnprocessed
    ,
    CustomerNotSubscribed
    ,
    BatchMeterUsageException
    by exception,
    UsageAggregationExpired
    ,
    UsageSubmissionExpired
    ,
    DuplicateRecord
    ) + a shared events-stack dashboard. Health-only; reflects the deployed mode (Q9); sellers can opt out / customize.
  • Q9 Full pipeline vs direct-submit (
    DEPLOYMENT_MODE
    , default
    full
    ) — explain both so the seller chooses knowingly:
    • full — seller emits RAW per-second events; the skill deploys the raw
      usage
      table + discoverer→aggregator→cleanup→submitter→submission-expiry, and the pipeline aggregates each
      (licenseArn, account, dimension, hour)
      group to one
      BatchMeterUsage
      record/hour. Best when the seller doesn't already aggregate or wants platform-owned aggregation + client-side rejection visibility.
    • direct-submit — seller ALREADY produces finalized hourly totals; the skill deploys ONLY
      aggregated_usage
      + submitter + submission-expiry + register/subscription (smaller/cheaper). Seller writes finalized records straight to
      aggregated_usage
      (PK
      licenseArn
      , SK
      customerAWSAccountId#dimension#hour
      ,
      quantity
      ,
      meteringPending
      =hour bucket).
    • Finalize-before-insert (direct-submit, critical): any record in
      aggregated_usage
      MAY be submitted on the next 5-min run — write a record ONLY when FINAL for its group, because a second write returns
      DuplicateRecord
      and the later quantity is NOT billed. The seller owns upstream aggregation/idempotency here.
    • Not a one-way door: the seller can switch modes later (a stack UPDATE that adds/removes mode-specific resources; raw
      usage
      table is
      Retain
      ). Call out the blast radius before flipping a live stack.
  • Q10 Registration fields + profile lookups (
    ALLOWED_REGISTRATION_FIELDS
    ) — EXPLAIN first, then ask. The in-region register Lambda persists ONLY these allowlisted fields (each length-bounded; the token is never stored) as a
    registrationData
    map
    on the in-region
    customer-profile
    table (PK
    licenseArn
    , SK
    customerAWSAccountId
    ) — buyer PII stays in-region and OUT of the PII-free us-east-1 subscribers table. Fields in the map are retrievable by the base key but NOT independently queryable. Ask: (a) which form fields to capture (incl. any KYB fields — name/email/company/use-case); (b) whether any field must be looked up directly (e.g. by
    email
    ) — if so it is promoted to a top-level attribute + a GSI (ask HASH-only or HASH+RANGE per promoted field). Keep fields OPTIONAL/seller-confirmed; invent no PII; mask PII in logs. Mapping
    licenseArn
    →the seller's internal SaaS tenant ID is the SELLER's responsibility
    — anchor it on the subscribers table (
    licenseArn
    from
    License Updated
    , PII-free) and/or the customer-profile table (
    licenseArn
    captured at registration); add a
    tenantId
    attribute + GSI if lookup-by-tenant is needed (
    references/architecture.md
    ).
Product Type Gate: after credentials, verify via the Catalog API the product is SaaS + usage-based (
ExternallyMetered
). AMI/Container/SaaS-Contract-only → out of scope, redirect to public docs. Concurrent Agreements is the silent default — all new products use
CustomerAWSAccountId
+
LicenseArn
; do NOT ask. A pre-existing legacy stack may use
ProductCode
+
CustomerIdentifier
— understand it before changing (
references/existing-sellers.md
).
  • Q1 产品代码(或产品 ID)。 接受任一;在验证时通过 Catalog API(
    describe-entity
    )将产品 ID 解析为代码。维度列表不是部署输入(维度有效性由服务端权威决定);您可以仅为了卖家信息列出维度。建议在卖家将维度名称接入用量行之前,使用 Marketplace Catalog API
    DescribeEntity
    (us-east-1)对照目录验证维度名称
    ——返回的实体详细信息枚举了产品注册的维度 API 名称(区分大小写)。在设置时确认卖家的维度字符串与目录完全匹配,是避免运行时
    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
    →
    MeteringLockHours
    ,默认 1,验证 1..20)。一个小时的用量在聚合/提交前保持开放以接收延迟用量的小时数(默认 1 = 处理刚刚完成的小时)。在上限时,一个小时后在年龄 20 时首次提交,在 24 小时可计费窗口中留下 ≥4 小时用于重试/重新驱动。月份感知: 上个月的小时保持锁定直到 1 日 03:00 UTC,然后无锁(每次运行提交)直到 06:00 UTC 截止,在此之前不会老化;03:00 之后写入的上个月记录必须已预先聚合(否则
    DuplicateRecord
    )。
  • Q7 TTL / 保留(每表,
    deploy.sh
    环境变量;
    0
    禁用):
    • 原始用量表
      USAGE_TABLE_TTL_DAYS
      (默认 365)——建议开启(高容量表)。卖家的写入器设置
      ttl
      epoch 属性;模板只让 DynamoDB 尊重它。下限 ≥2 天,必须超过
      MeteringLockHours
      +24h,使行不会在计量前过期。
    • 聚合表
      AGGREGATED_USAGE_TTL_DAYS
      (默认 0 = 保留)——建议省略/较大(很小的计费审计跟踪)。当 >0 时,提交器仅在
      Success
      行上设置
      ttl
      。
    • 启用 TTL 需要部署者的
      dynamodb:UpdateTimeToLive
      (已在策略中)。
  • Q8 创建健康仪表板?(
    CREATE_DASHBOARD
    ,默认 true)。每产品主堆栈仪表板(每 Lambda 的 Errors/Throttles/Invocations/Duration;队列/DLQ 深度 + 最旧年龄;按原因的 EMF
    UsageRecordRejected
    、
    UsageRecordUnprocessed
    、
    CustomerNotSubscribed
    、按异常的
    BatchMeterUsageException
    、
    UsageAggregationExpired
    、
    UsageSubmissionExpired
    、
    DuplicateRecord
    )+ 共享事件堆栈仪表板。仅健康;反映部署模式(Q9);卖家可选择退出/自定义。
  • Q9 完整管道 vs 直接提交(
    DEPLOYMENT_MODE
    ,默认
    full
    )——解释两者,让卖家知情选择:
    • full——卖家发出原始的每秒事件;skill 部署原始
      usage
      表 + 发现器→聚合器→清理器→提交器→提交过期,管道将每个
      (licenseArn, account, dimension, hour)
      组聚合为每小时一条
      BatchMeterUsage
      记录。最适合卖家尚未聚合或希望平台拥有聚合 + 客户端拒绝可见性时。
    • direct-submit——卖家已经产生最终的小时总计;skill 仅部署
      aggregated_usage
      + 提交器 + 提交过期 + 注册/订阅(更小/更便宜)。卖家将最终记录直接写入
      aggregated_usage
      (PK
      licenseArn
      ,SK
      customerAWSAccountId#dimension#hour
      ,
      quantity
      ,
      meteringPending
      =小时桶)。
    • 插入前完成(direct-submit,关键):
      aggregated_usage
      中的任何记录可能在下一次 5 分钟运行时被提交——仅当记录对其组为最终时才写入,因为第二次写入返回
      DuplicateRecord
      且后一个数量不被计费。卖家在这里拥有上游聚合/幂等性。
    • 不是单行道: 卖家可以稍后切换模式(堆栈 UPDATE 添加/删除模式特定资源;原始
      usage
      表为
      Retain
      )。在翻转实时堆栈之前指出爆炸半径。
  • Q10 注册字段 + 配置文件查找(
    ALLOWED_REGISTRATION_FIELDS
    )——先解释,然后询问。区域内的注册 Lambda 仅持久化这些允许列表字段(每个长度受限;token 从不存储)作为区域**
    customer-profile
    ** 表(PK
    licenseArn
    ,SK
    customerAWSAccountId
    )上的
    registrationData
    映射——买家 PII 留在区域内,且不在无 PII 的 us-east-1 订阅者表中。映射中的字段可通过基础键检索,但不能独立查询。询问:(a)捕获哪些表单字段(包括任何 KYB 字段——姓名/电子邮件/公司/用例);(b)是否有任何字段必须直接查找(例如按
    email
    )——如果是,则提升为顶级属性 + GSI(询问每个提升字段是 HASH-only 还是 HASH+RANGE)。保持字段可选/卖家确认;不发明 PII;在日志中掩码 PII。将
    licenseArn
    映射到卖家内部 SaaS 租户 ID 是卖家的责任
    ——锚定在订阅者表(来自
    License Updated
    的
    licenseArn
    ,无 PII)和/或客户配置文件表(注册时捕获的
    licenseArn
    );如果需要按租户查找,添加
    tenantId
    属性 + GSI(
    references/architecture.md
    )。
产品类型门: 获得凭证后,通过 Catalog API 验证产品是 SaaS + 按用量计费(
ExternallyMetered
)。AMI/Container/SaaS-合同-only → 超出范围,重定向到公开文档。 并发协议是静默默认——所有新产品使用
CustomerAWSAccountId
+
LicenseArn
;不要询问。预先存在的旧版堆栈可能使用
ProductCode
+
CustomerIdentifier
——在更改之前理解它(
references/existing-sellers.md
)。

Setup ordering (do NOT skip or reorder)

设置顺序(不要跳过或重新排序)

  1. Obtain ephemeral credentials (SSO →
    sts assume-role
    ; never long-lived keys; don't ask for keys first).
  2. Product type gate (SaaS + ExternallyMetered).
  3. Product visibility gate — deploy/mutate the product-specific MAIN stack ONLY when the CURRENT product is
    Limited
    , never
    Published
    (provide code for the seller's own pipeline instead). Direct-deploy the main stack only when the current product is
    Limited
    AND there is no existing per-product stack (an existing one is modify/extend, seller-confirmed). The SHARED events stack is never redeployed/mutated when it already exists (it may serve other, possibly
    Published
    , products with live traffic — reuse its outputs); only the first onboarding creates it. See
    references/existing-sellers.md
    .
  4. Confirm the questionnaire answers (incl. stage name).
  5. Only THEN generate + deploy: materialize the template(s) + Lambda handlers +
    deploy.sh
    in the seller's workspace and deploy THOSE on-disk files
    (
    sam build --template-file
    +
    sam deploy
    ) — never an inline/transient template. Modify/extend edits the seller's existing files in place (merge env vars). Leave all artifacts in the workspace afterward (code/stack parity; seller can version/re-deploy/develop, customizing only
    TODO:
    sections).
  6. EXISTING-STACK guardrails (ENFORCE, do not bypass). WHEN a stack already exists for this scope, the skill SHALL NOT auto-apply changes: it SHALL (a) present the exact diff/change set for the seller's review, (b) require the seller's EXPLICIT confirmation and their normal CODE REVIEW + change-management process before applying, (c) scope changes to the CURRENT product's main stack, and (d) never redeploy/mutate the shared events stack that already serves other (possibly
    Published
    ) products — reuse it. Direct auto-deploy is reserved for a
    Limited
    product with NO existing stack. See
    references/existing-sellers.md
    .
  7. After integration, run the full test plan. At the END of setup/modify, the skill SHALL recommend and (where it can) walk the seller through the complete hands-on test plan (
    references/test-plan.md
    ) — verifying every expected observable outcome — and SHALL state that the integration should not carry PRODUCTION billing traffic until that review + testing passes.
A failure at any gate stops the workflow before deployment.
  1. 获取临时凭证(SSO →
    sts assume-role
    ;绝不使用长期密钥;不要先要密钥)。
  2. 产品类型门(SaaS + ExternallyMetered)。
  3. 产品可见性门——仅当当前产品为
    Limited
    、绝不
    Published
    时部署/变更产品特定的主堆栈(为卖家自己的管道提供代码)。仅当当前产品为
    Limited
    且没有现有的每产品堆栈(现有的为修改/扩展,卖家确认)时直接部署主堆栈。共享事件堆栈在已存在时绝不重新部署/变更(它可能服务其他、可能
    Published
    、有实时流量的产品;重用其输出);只有首次 onboarding 创建它。参见
    references/existing-sellers.md
    。
  4. 确认问卷答案(包括阶段名称)。
  5. 然后才生成 + 部署:在卖家工作区中物化模板 + Lambda 处理器 +
    deploy.sh
    并部署这些磁盘上的文件
    (
    sam build --template-file
    +
    sam deploy
    )——绝不要内联/临时模板。修改/扩展就地编辑卖家现有文件(合并环境变量)。之后将所有工件留在工作区(代码/堆栈一致性;卖家可以版本控制/重新部署/开发,只自定义
    TODO:
    部分)。
  6. 现有堆栈护栏(强制执行,不要绕过)。 当此范围的堆栈已存在时,skill 不得自动应用更改:它应当(a)呈现确切的 diff/变更集供卖家审查,(b)在应用前要求卖家明确确认及其正常的代码审查 + 变更管理流程,(c)将更改范围限定为当前产品的主堆栈,以及(d)绝不重新部署/变更已服务其他(可能
    Published
    )产品的共享事件堆栈——重用它。直接自动部署仅保留给没有现有堆栈的
    Limited
    产品。参见
    references/existing-sellers.md
    。
  7. 集成后,运行完整测试计划。 在设置/修改结束时,skill 应当推荐并(在可能的地方)引导卖家完成完整的动手测试计划(
    references/test-plan.md
    )——验证每个预期的可观察结果——并应说明在审查 + 测试通过之前,集成不应承载生产计费流量。
任何门失败都会在部署前停止工作流。

Stack naming

堆栈命名

ComponentPatternExample
Events stack
awsmp-events-stack
Shared, us-east-1
Main stack
awsmp-<productCode>-metering
awsmp-4ml54db8vrmjuykaw1psroool-metering
Events stack (staged)
awsmp-events-<stageName>-stack
awsmp-events-beta-stack
Main stack (staged)
awsmp-<productCode>-<stageName>-metering
awsmp-4ml54db8vrmjuykaw1psroool-beta-metering
For a non-production sandbox, deploy stage-scoped stacks (the stage folds into every resource name) and set
MeteringMode=dry-run
so the submitter never calls the real
BatchMeterUsage
— use test accounts only. See
references/existing-sellers.md
/ the setup path.
组件模式示例
事件堆栈
awsmp-events-stack
共享,us-east-1
主堆栈
awsmp-<productCode>-metering
awsmp-4ml54db8vrmjuykaw1psroool-metering
事件堆栈(分阶段)
awsmp-events-<stageName>-stack
awsmp-events-beta-stack
主堆栈(分阶段)
awsmp-<productCode>-<stageName>-metering
awsmp-4ml54db8vrmjuykaw1psroool-beta-metering
对于非生产沙盒,部署阶段范围的堆栈(阶段折叠到每个资源名称中)并设置
MeteringMode=dry-run
,使提交器从不调用真实的
BatchMeterUsage
——仅使用测试账户。参见
references/existing-sellers.md
/ 设置路径。

Supported features

支持的功能

SaaS Usage (PAYG) ✅ | Concurrent Agreements ✅ | VMT ✅ | Multi-region ✅ | Multi-product ✅ Out of scope (→ public docs): CCP, AMI/Container, SaaS-Contract-only (GetEntitlements), 3PI.
Scale: the pipeline is decoupled (discoverer → work queue → aggregator → aggregated-usage + cleanup queue → submitter) so it is designed to handle thousands of metering records per hour. Second-precision rows bound a group at ≤3600 rows/hour → supports high volume at ≤~1 write/sec per
(licenseArn, account, dimension)
group. For finer/higher rates, recommend a streaming ingestion tier (e.g. Kinesis Data Streams) that pre-aggregates to per-second/hour rows (public-knowledge/host-model-determined, not first-class to this skill). Full design:
references/scalable-metering-design.md
. GSI-driven discovery (never a Scan): the discoverer finds pending usage via the sparse
metering_pending
GSI — HASH =
meteringPending
hour-bucket (
YYYY-MM-DDTHH
), RANGE =
licenseArn
, projecting
customerAWSAccountId
+
dimension
(INCLUDE/ALL, NOT
KEYS_ONLY
) so a query returns the
(licenseArn, customerAWSAccountId, dimension)
tuples directly — no follow-up read to discover the group. Each group is then read with a targeted
begins_with
sort-key prefix
(
begins_with(customerAWSAccountId#dimension#timestamp, "{account}#{dimension}#{hourPrefix}")
), NOT a full
licenseArn
-partition query + timestamp
FilterExpression
.
SaaS 用量(PAYG)✅ | 并发协议 ✅ | VMT ✅ | 多区域 ✅ | 多产品 ✅ 超出范围(→ 公开文档):CCP、AMI/Container、SaaS-合同-only(GetEntitlements)、3PI。
规模: 管道是解耦的(发现器 → 工作队列 → 聚合器 → 聚合用量 + 清理队列 → 提交器),因此设计为每小时处理数千条计量记录。秒级精度行将组限制为 ≤3600 行/小时 → 支持每
(licenseArn, account, dimension)
组 ≤约 1 写/秒的高容量。对于更精细/更高的速率,推荐流式摄取层(例如 Kinesis Data Streams),它预聚合为每秒/每小时行(公开知识/主机模型确定,不是本 skill 的一等公民)。完整设计:
references/scalable-metering-design.md
。 GSI 驱动发现(绝不用 Scan): 发现器通过稀疏
metering_pending
GSI——HASH =
meteringPending
小时桶(
YYYY-MM-DDTHH
),RANGE =
licenseArn
找到待处理用量,投影
customerAWSAccountId
+
dimension
(INCLUDE/ALL,不是
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
)

  1. Generate SAM template + Lambda code (fill
    TODO:
    s).
  2. Deploy events stack (us-east-1) — shared EventBridge + subscribers table.
  3. Deploy main stack (seller's region) — Lambdas + DynamoDB + API Gateway + alarms.
  4. Set the AMMP Fulfillment URL to the branded registration page (S3+CloudFront front-end that captures buyer/KYB fields and calls
    RegistrationUrl
    ), NOT the raw
    RegistrationUrl
    (smoke-test fallback only) — manual seller step. See
    references/registration-page.md
    .
  5. End-to-end test (subscribe → resolve → write a conforming usage row → discoverer → aggregator → submitter → verify
    Aggregated
    +
    Success
    ).
  6. Validate via CloudTrail logs.
After deploy, state the seller's ongoing responsibilities and guide the hands-on test (
references/test-plan.md
): the stacks do nothing until the seller writes conforming usage rows — whole-second
customerAWSAccountId#dimension#timestamp
sort key (matching segments) and a
meteringPending
bucket that equals the row's own UTC timestamp hour. A mis-stamped
meteringPending
is the ONE writer error the pipeline cannot detect
(silently never metered) — confirm via the test. Also: subscribe a notification to the alarms + watch the dashboards (
UsageRecordRejected
by reason,
BatchMeterUsageException
by exception,
CustomerNotSubscribed
,
UsageAggregationExpired
/
UsageSubmissionExpired
); catalog dimension changes need no redeploy. Several results are async (post-subscribe
License Updated
→
licenseArn
/
subscriptionStatus
; hourly discoverer +
rate(5m)
submitter/expiry) — poll with bounded backoff, or manually invoke a Lambda to exercise a path immediately (say which). Recommend running the FULL test plan (
references/test-plan.md
) end-to-end at the end of the integration and verifying every expected observable outcome — plus a review of all created/changed resources — BEFORE the integration carries production billing traffic
(AI-generated output; see the review-before-production callout above).
Prerequisites, credentials, and the full least-privilege deployer policy:
references/iam-credentials.md
.
  1. 生成 SAM 模板 + Lambda 代码(填写
    TODO:
    标记)。
  2. 部署事件堆栈(us-east-1)——共享 EventBridge + 订阅者表。
  3. 部署主堆栈(卖家区域)——Lambda + DynamoDB + API Gateway + 告警。
  4. 将 AMMP Fulfillment URL 设置为品牌注册页面(S3+CloudFront 前端,捕获买家/KYB 字段并调用
    RegistrationUrl
    ),而不是原始的
    RegistrationUrl
    (仅冒烟测试回退)——手动卖家步骤。参见
    references/registration-page.md
    。
  5. 端到端测试(订阅 → 解析 → 写入合规用量行 → 发现器 → 聚合器 → 提交器 → 验证
    Aggregated
    +
    Success
    )。
  6. 通过 CloudTrail 日志验证。
部署后,说明卖家持续的责任并引导动手测试(
references/test-plan.md
):在卖家写入合规用量行之前,堆栈什么都不做——整秒
customerAWSAccountId#dimension#timestamp
排序键(匹配段)和 等于行自身 UTC 时间戳小时的
meteringPending
桶。错误标记的
meteringPending
是管道无法检测的唯一写入器错误
(静默地从未计量)——通过测试确认。另外:为告警订阅通知并观察仪表板(按原因的
UsageRecordRejected
、按异常的
BatchMeterUsageException
、
CustomerNotSubscribed
、
UsageAggregationExpired
/
UsageSubmissionExpired
);目录维度更改无需重新部署。几个结果是异步的(订阅后
License Updated
→
licenseArn
/
subscriptionStatus
;每小时发现器 +
rate(5m)
提交器/过期)——使用有界退避轮询,或手动调用 Lambda 立即可执行路径(说明哪个)。建议在集成结束时端到端运行完整测试计划(
references/test-plan.md
)并验证每个预期的可观察结果——加上对所有创建/更改资源的审查——在集成承载生产计费流量之前
(AI 生成输出;参见上面的生产前审查提示)。
先决条件、凭证和完整最小权限部署者策略:
references/iam-credentials.md
。

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

事件与状态模型(在被问及取消/计量如何工作时背诵)

Two INDEPENDENT status fields on the subscriber row (never conflate):
  • agreementStatus
    =
    active
    /
    inactive
    (agreement lifecycle).
  • subscriptionStatus
    =
    active
    /
    deprovisioning
    /
    inactive
    (license lifecycle; there is no
    deprovisioned
    value
    ).
Metering follows the LICENSE lifecycle only:
  • License Updated
    → agreement+subscription
    active
    (metering allowed).
  • License Deprovisioned
    →
    subscriptionStatus=deprovisioning
    + a
    deprovisioningExpiry
    (event time + ~1h); OPENS the ~1-hour final-usage flush window. The lock-bypass mechanism: during this window the pipeline submits ALL remaining pending usage for that license IMMEDIATELY, ignoring
    MeteringLockHours
    and the normal completed-hours-only gate
    — it does NOT wait for
    MeteringLockHours
    to elapse and it DOES meter the current in-progress hour (the one hour the normal path always skips). Concrete mechanism/marker: the
    License Deprovisioned
    handler sets
    subscriptionStatus=deprovisioning
    +
    deprovisioningExpiry
    on that subscriber row; the discoverer runs in a flush-deprovisioning mode that keys off that marker and enqueues the pending buckets (over
    now-23h … now-1h
    PLUS the current hour) for only that license, oldest-first. The bypass is SCOPED to the deprovisioning license ONLY — other active licenses keep the normal
    MeteringLockHours
    gate and completed-hours-only behavior; one buyer's deprovisioning never force-flushes another buyer's open hour. It is the deprovisioning flush that bypasses the lock, NOT a change to
    MeteringLockHours
    itself and NOT a
    Purchase Agreement Ended
    event. Once the window has elapsed, the events-stack deprovision-cleanup Lambda (rate(15m)) sets
    inactive
    and clears the deprovisioning markers — finalization is time-based, so it is correct across multiple hours and regions. After the server window closes,
    BatchMeterUsage
    returns
    CustomerNotSubscribed
    .
  • Purchase Agreement Ended
    →
    agreementStatus=inactive
    ONLY (does NOT change subscriptionStatus, stop metering, or flush).
  • Purchase Agreement Amended
    → refresh agreement metadata (stays active).
  • Purchase Agreement Created
    → NOT consumed by metering (no licenseArn); the seller handles it if needed (pre-provisioning, CRM), without activating metering.
Do NOT invert these (
License Deprovisioned
, not
Purchase Agreement Ended
, opens the flush window). Do NOT discard queued records before flushing.
registeredRegions
is reference-only and NEVER gates metering.
订阅者行上的两个独立状态字段(绝不混淆):
  • agreementStatus
    =
    active
    /
    inactive
    (协议生命周期)。
  • subscriptionStatus
    =
    active
    /
    deprovisioning
    /
    inactive
    (许可证生命周期;没有
    deprovisioned
    值
    )。
计量仅遵循许可证生命周期:
  • License Updated
    → 协议+订阅
    active
    (允许计量)。
  • License Deprovisioned
    →
    subscriptionStatus=deprovisioning
    +
    deprovisioningExpiry
    (事件时间 + 约 1 小时);打开约 1 小时的最终用量刷新窗口。锁绕过机制: 在此窗口期间,管道立即提交该许可证所有剩余的待处理用量,忽略
    MeteringLockHours
    和正常的仅已完成小时门控
    ——它不等待
    MeteringLockHours
    过去,并且确实计量当前进行中的小时(正常路径总是跳过的那一小时)。具体机制/标记:
    License Deprovisioned
    处理器在该订阅者行上设置
    subscriptionStatus=deprovisioning
    +
    deprovisioningExpiry
    ;发现器以刷新-取消供应模式运行,以该标记为键,仅为该许可证对待处理桶(
    now-23h … now-1h
    加上当前小时)排队,最旧优先。绕过仅限于取消供应的许可证——其他活跃许可证保持正常的
    MeteringLockHours
    门控和仅已完成小时行为;一个买家的取消供应绝不强制刷新另一个买家的开放小时。是取消供应刷新绕过锁,不是
    MeteringLockHours
    本身的更改,也不是
    Purchase Agreement Ended
    事件。窗口过去后,事件堆栈取消供应清理 Lambda(rate(15m))设置
    inactive
    并清除取消供应标记——完成基于时间,因此在多个小时和区域中都是正确的。服务端窗口关闭后,
    BatchMeterUsage
    返回
    CustomerNotSubscribed
    。
  • Purchase Agreement Ended
    → 仅
    agreementStatus=inactive
    (不更改 subscriptionStatus、不停止计量、不刷新)。
  • Purchase Agreement Amended
    → 刷新协议元数据(保持活跃)。
  • Purchase Agreement Created
    → 计量不消费(无 licenseArn);卖家在需要时处理它(预配置、CRM),而不激活计量。
不要反转这些(打开刷新窗口的是
License Deprovisioned
,不是
Purchase Agreement Ended
)。不要在刷新前丢弃排队的记录。
registeredRegions
仅参考且绝不门控计量。

Setup references

设置参考

  • Full walkthrough + writer contract + Common Mistakes + VMT/token/timestamp/schema/meter-flow:
    references/setup-path.md
  • Deployment steps:
    references/deployment-steps.md
    · IAM policy:
    references/iam-credentials.md
  • Concurrent Agreements + data model:
    references/concurrent-agreements.md
    · Architecture + status lifecycle:
    references/architecture.md
  • Existing sellers (modify/extend, legacy):
    references/existing-sellers.md
  • Branded buyer page + Know Your Buyer (KYB) capture:
    references/registration-page.md
  • Hands-on test plan (manual steps, scoped cases, async polling):
    references/test-plan.md
  • Monitoring & alarms:
    references/troubleshooting.md
    · Client-side guardrails (all checks before
    BatchMeterUsage
    ):
    references/client-side-guardrails.md
  • Reference implementation:
    scripts/metering_core.py
    (shared) +
    discoverer.py
    /
    aggregator.py
    /
    cleanup.py
    /
    submitter.py
    /
    expiry.py
    (pipeline) +
    register.py
    /
    subscription.py
    ;
    scripts/test_event_publisher.py
    (non-prod sandbox test-event generator, dry-run stages only);
    scripts/deploy.sh
    (needs
    assets/requirements.txt
    ); SAM templates
    assets/template-main.yaml
    ,
    assets/template-events.yaml
  • 完整演练 + 写入器契约 + 常见错误 + VMT/token/时间戳/模式/计量流程:
    references/setup-path.md
  • 部署步骤:
    references/deployment-steps.md
    · IAM 策略:
    references/iam-credentials.md
  • 并发协议 + 数据模型:
    references/concurrent-agreements.md
    · 架构 + 状态生命周期:
    references/architecture.md
  • 现有卖家(修改/扩展、旧版):
    references/existing-sellers.md
  • 品牌买家页面 + 了解你的买家(KYB)捕获:
    references/registration-page.md
  • 动手测试计划(手动步骤、范围用例、异步轮询):
    references/test-plan.md
  • 监控与告警:
    references/troubleshooting.md
    · 客户端护栏(
    BatchMeterUsage
    前的所有检查):
    references/client-side-guardrails.md
  • 参考实现:
    scripts/metering_core.py
    (共享)+
    discoverer.py
    /
    aggregator.py
    /
    cleanup.py
    /
    submitter.py
    /
    expiry.py
    (管道)+
    register.py
    /
    subscription.py
    ;
    scripts/test_event_publisher.py
    (非生产沙盒测试事件生成器,仅 dry-run 阶段);
    scripts/deploy.sh
    (需要
    assets/requirements.txt
    );SAM 模板
    assets/template-main.yaml
    、
    assets/template-events.yaml

Monitoring & alarms (seller-owned)

监控与告警(卖家拥有)

The deployed stack creates
awsmp-*
metrics + CloudWatch alarms in the SELLER's account covering the revenue-loss signals (per-stage Lambda
Errors
/
Throttles
across discoverer/aggregator/cleanup/submitter/expiry, a "did not run" invocations alarm,
>24h
age-out, and the EMF business-status metrics). Alarms are always created; actions wire to an OPTIONAL in-region SNS topic (
AlertsTopicArn
). Business-status metrics use EMF from the Lambda logs (no extra runtime IAM). Handling alarms is the seller's responsibility — AWS Marketplace and this skill do not track client-side errors; neither the deployer nor any Lambda publishes to the topic (CloudWatch fires the action). Thresholds/actions/topic are seller
TODO:
s. Detail:
references/troubleshooting.md
.
"What alarms tell me metering records are being REJECTED / not metered?" — the stack creates a dedicated alarm on each rejection/failure signal so a seller is paged the moment records stop metering (all fire into
AlertsTopicArn
; thresholds are seller
TODO:
s):
  • UsageRecordUnprocessed
    (alarm
    *-usage-unprocessed
    ) —
    BatchMeterUsage
    returned
    UnprocessedRecords
    that were STILL unprocessed after the submitter's one automatic retry. The submitter does NOT raise on this (it leaves them pending for the next
    rate(5m)
    cycle), so this metric is the ONLY alarm-able signal for the
    UnprocessedRecords
    case — it does NOT trip the Lambda
    Errors
    alarm. A persistent breach = records repeatedly failing to meter (throttling, timestamp bounds, downstream issue) → investigate.
  • UsageRecordRejected
    (
    *-usage-rejected
    ) — client-side guardrail rejections before metering (summed across reasons; per-reason breakdown on the dashboard); fix source rows + resubmit.
    BatchMeterUsageException
    (
    *-batchmeterusage-exception
    ) — a REQUEST-level exception failed a whole call (dimension/license/etc.); pipeline bisects to isolate.
    CustomerNotSubscribed
    (
    *-customer-not-subscribed
    ) — per-record status, no active subscription (do NOT retry).
  • Plus age-out alarms (
    UsageAggregationExpired
    /
    UsageSubmissionExpired
    ,
    >24h
    ) catch usage that silently never metered, and Lambda
    Errors
    /
    Throttles
    /DLQ-depth alarms catch pipeline failures.
    DuplicateRecord
    is a dashboard metric (informational — first submission already billed), not an alarm.

部署的堆栈在卖家账户中创建
awsmp-*
指标 + CloudWatch 告警,涵盖收入损失信号(发现器/聚合器/清理器/提交器/过期器各阶段的 Lambda
Errors
/
Throttles
、"未运行"调用告警、
>24h
老化以及 EMF 业务状态指标)。告警总是创建;操作连接到可选的区域内 SNS 主题(
AlertsTopicArn
)。业务状态指标使用 Lambda 日志中的 EMF(无需额外运行时 IAM)。处理告警是卖家的责任——AWS Marketplace 和此 skill 不跟踪客户端错误;部署者和任何 Lambda 都不向主题发布(CloudWatch 触发操作)。阈值/操作/主题是卖家的
TODO:
。详情:
references/troubleshooting.md
。
"哪些告警告诉我计量记录被拒绝 / 未计量?"——堆栈为每个拒绝/失败信号创建专用告警,使卖家在记录停止计量时立即被分页(全部触发到
AlertsTopicArn
;阈值是卖家的
TODO:
):
  • UsageRecordUnprocessed
    (告警
    *-usage-unprocessed
    )——
    BatchMeterUsage
    返回的
    UnprocessedRecords
    在提交器的一次自动重试后仍未处理。提交器不会因此抛错(它将它们留待下一个
    rate(5m)
    周期),因此此指标是
    UnprocessedRecords
    情况唯一可告警的信号——它不会触发 Lambda
    Errors
    告警。持续违反 = 记录反复无法计量(节流、时间戳边界、下游问题)→ 调查。
  • UsageRecordRejected
    (
    *-usage-rejected
    )——计量前的客户端护栏拒绝(跨原因汇总;按原因细目在仪表板上);修复源行 + 重新提交。
    BatchMeterUsageException
    (
    *-batchmeterusage-exception
    )——请求级异常使整个调用失败(维度/许可证/等);管道二分隔离。
    CustomerNotSubscribed
    (
    *-customer-not-subscribed
    )——逐记录状态,无活跃订阅(不要重试)。
  • 加上老化告警(
    UsageAggregationExpired
    /
    UsageSubmissionExpired
    、
    >24h
    )捕获静默从未计量的用量,Lambda
    Errors
    /
    Throttles
    /DLQ 深度告警捕获管道失败。
    DuplicateRecord
    是仪表板指标(信息性——首次提交已计费),不是告警。

QUERY PATH — Find, Debug, and Analyze Metering Records

查询路径——查找、调试和分析计量记录

Full routing/patterns:
references/query-path.md
+
references/query-patterns.md
.
Capability boundary (state it; don't return a bounded answer as authoritative). This query USES CloudTrail Event History (
LookupEvents
) in the seller's region, last ≤90 days, page-bounded and processed in memory — a recent, regional, SUBMISSIONS view (includes rejected/duplicate records that were NOT billed) for BASIC operational/debug queries (lookups + simple filters/counts/summaries by customer, dimension, status, time). It RECOMMENDS (does not run): Seller Reports/SDDS for billed totals + >90-day history; the host agent + model for complex/open-ended analytics (hand it the fetched data); and — for CA/LicenseArn metering with no LicenseArn→productCode mapping (single-product-stack context, CloudTrail
additionalEventData
, or the subscribers table) — only non-product-scoped queries (never guess attribution). Every result carries
complete
/
truncated
/
windowClipped
. Before presenting a result, CONFIRM these signals: if
complete
is false (page-truncated OR clipped at the 90-day retention limit), either re-run for a complete answer where feasible (narrow/adjust range for a clip; raise
--max-pages
or add filters for truncation) or state explicitly that the result is PARTIAL — what was clipped/truncated and the covered range — and point to Seller Reports for the authoritative/older portion. A truncated or clipped aggregate (summary/count/top_customers) is a lower-bound sample, NOT authoritative. Never report a partial result as the full requested range. Do not refuse a basic query just because a richer answer is possible in the host agent. CloudTrail-derived SQL is the WRONG source for billing/usage analytics — CloudTrail Lake OR Athena over a
trail → S3
export both see SUBMISSIONS only (rejected/duplicate, regional, no CA
productCode
); do NOT set up CloudTrail Lake for metering; for complex analytics / billing-grade totals / >90-day data use SDDS (recommend, don't run), and use a
trail → S3
(never Lake) only for long-term raw-submission audit retention. Full boundary:
references/query-path.md
.
Complex / billing-grade query steering — MUST NOT vs allowed sources (enforce; do not violate). For any complex-analytics or billing-accuracy question over metering data:
  • MUST NOT recommend CloudTrail Lake (
    create-event-data-store
    ) for metering — ever.
  • MUST NOT present CloudTrail (Event History OR a
    trail → S3
    /Athena export) as a source of billed/billing-grade totals — CloudTrail sees SUBMISSIONS only (includes rejected/duplicate, regional, no CA
    productCode
    ), so it is never authoritative for revenue.
  • Allowed sources only: for billed/disbursed totals and >90-day billed history → Seller Reports / AMMP Billed Revenue dashboard and SDDS (recommend, do not run); for recent (≤90-day) regional SUBMISSION debugging → CloudTrail Event History; for complex/open-ended analytics → hand the fetched data to the host agent + model; for long-term RAW-submission audit retention only → a plain
    trail → S3
    (NOT Lake) + Athena, understanding it is submissions-not-billing. Route every complex-billing ask to these allowed sources and name why CloudTrail is not billing-authoritative.
完整路由/模式:
references/query-path.md
+
references/query-patterns.md
。
能力边界(说明它;不要将有界答案呈现为权威)。 此查询使用卖家区域中的 CloudTrail 事件历史(
LookupEvents
),最近 ≤90 天,分页有界并在内存中处理——一个最近的、区域的、提交视图(包括未被计费的拒绝/重复记录),用于基本操作/调试查询(按客户、维度、状态、时间的查找 + 简单过滤/计数/汇总)。它推荐(不运行):卖家报告/SDDS 用于计费总计 + >90 天历史;主机 Agent + 模型用于复杂/开放式分析(将获取的数据交给它);以及——对于无 LicenseArn→productCode 映射的 CA/LicenseArn 计量(单产品堆栈上下文、CloudTrail
additionalEventData
或订阅者表)——仅非产品范围查询(绝不猜测归属)。每个结果携带
complete
/
truncated
/
windowClipped
。在呈现结果之前,确认这些信号: 如果
complete
为 false(分页截断或在 90 天保留限制处裁剪),要么在可行时重新运行以获得完整答案(裁剪时收窄/调整范围;截断时提高
--max-pages
或添加过滤器),要么明确说明结果是部分的——什么被裁剪/截断以及覆盖的范围——并指向卖家报告以获取权威/更旧部分。截断或裁剪的聚合(汇总/计数/top_customers)是下限样本,非权威。绝不将部分结果报告为所请求的完整范围。不要仅仅因为主机 Agent 中可能获得更丰富的答案就拒绝基本查询。CloudTrail 派生的 SQL 是计费/用量分析的错误来源——CloudTrail Lake 或对
trail → S3
导出的 Athena 都只看到提交(拒绝/重复、区域性、无 CA
productCode
);不要为计量设置 CloudTrail Lake;对于复杂分析 / 计费级总计 / >90 天数据使用 SDDS(推荐,不运行),并且仅对长期原始提交审计保留使用
trail → S3
(绝不是 Lake)。完整边界:
references/query-path.md
。
复杂 / 计费级查询引导——禁止与允许的来源(强制执行;不要违反)。 对于任何关于计量数据的复杂分析或计费准确性问题:
  • 禁止推荐 CloudTrail Lake(
    create-event-data-store
    )用于计量——永远。
  • 禁止将 CloudTrail(事件历史或
    trail → S3
    /Athena 导出)呈现为已计费/计费级总计的来源——CloudTrail 只看到提交(包括拒绝/重复、区域性、无 CA
    productCode
    ),因此它对收入绝不是权威的。
  • 仅允许的来源: 对于已计费/已支付总计和 >90 天已计费历史——卖家报告 / AMMP 已计费收入仪表板和 SDDS(推荐,不运行);对于最近(≤90 天)区域性提交调试——CloudTrail 事件历史;对于复杂/开放式分析——将获取的数据交给主机 Agent + 模型;仅用于长期原始提交审计保留——普通
    trail → S3
    (不是 Lake)+ Athena,理解它是提交而非计费。将每个复杂计费请求路由到这些允许的来源,并说明为什么 CloudTrail 不是计费权威。

Steps

步骤

  1. Ask the metering region (MUST — CloudTrail is regional).
  2. Extract filters:
    customer_id
    ,
    status
    ,
    dimension
    ,
    start_date
    /
    end_date
    ,
    query_type
    (summary/detail/count/list_failures/top_customers),
    top_n
    .
  3. Run the script (default: last 7 days):
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 /
create-event-data-store
for metering. Query IN the seller's metering region and filter on the metering event source:
bash
aws cloudtrail lookup-events --region <SELLER_METERING_REGION> \
  --lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
  --start-time <ISO> --end-time <ISO>
Each
BatchMeterUsage
event's CloudTrail
responseElements
carries
results
(per-record
status
:
Success
/
CustomerNotSubscribed
/
DuplicateRecord
— those are the ONLY per-record statuses) and
unprocessedRecords
— to verify a submission, check
results
vs
unprocessedRecords
FIRST (records in
unprocessedRecords
were not processed — see the
UnprocessedRecords
note below). CloudTrail lowercases the first letter of response fields, so read
results
/
status
/
meteringRecordId
/
unprocessedRecords
(lowercase) — the direct SDK
BatchMeterUsage
response uses PascalCase (
Results
/
Status
/
MeteringRecordId
/
UnprocessedRecords
). An undefined dimension is NOT a per-record status — it fails the whole call with a request-level
InvalidUsageDimensionException
(see below). ≤90 days only; for older/billed totals use Seller Reports/SDDS (recommended, not queried). CloudTrail is NOT revenue — it shows submissions (incl. rejected/duplicate, not billed). Metered usage bills at the START of the following month (the 2nd/3rd) — e.g. November usage lands on the December invoice — and dashboard revenue is estimated until the month closes; use Seller Reports for billed/disbursed amounts.
  1. 询问计量区域(必须——CloudTrail 是区域性的)。
  2. 提取过滤器:
    customer_id
    、
    status
    、
    dimension
    、
    start_date
    /
    end_date
    、
    query_type
    (summary/detail/count/list_failures/top_customers)、
    top_n
    。
  3. 运行脚本(默认:最近 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-store
。在卖家的计量区域中查询并过滤计量事件源:
bash
aws cloudtrail lookup-events --region <SELLER_METERING_REGION> \
  --lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
  --start-time <ISO> --end-time <ISO>
每个
BatchMeterUsage
事件的 CloudTrail
responseElements
携带
results
(逐记录
status
:
Success
/
CustomerNotSubscribed
/
DuplicateRecord
——这些是仅有的逐记录状态)和
unprocessedRecords
——验证提交时,首先检查
results
与
unprocessedRecords
(
unprocessedRecords
中的记录未被处理——见下面的
UnprocessedRecords
说明)。CloudTrail 将响应字段的首字母小写,因此读取
results
/
status
/
meteringRecordId
/
unprocessedRecords
(小写)——直接 SDK
BatchMeterUsage
响应使用 PascalCase(
Results
/
Status
/
MeteringRecordId
/
UnprocessedRecords
)。未定义的维度不是逐记录状态——它以请求级
InvalidUsageDimensionException
使整个调用失败(见下文)。仅 ≤90 天;对于更旧/已计费总计使用卖家报告/SDDS(推荐,不查询)。CloudTrail 不是收入——它显示提交(包括拒绝/重复、未计费)。计量用量在下个月月初(2 日/3 日)计费——例如 11 月用量出现在 12 月发票上——仪表板收入是估计的直到月份结束;使用卖家报告获取已计费/已支付金额。

Status meanings

状态含义

Per-record
Results[].Status
values:
StatusMeaningAction
Success
Accepted and billedNone
CustomerNotSubscribed
No active subscription⚠️ Do NOT retry
DuplicateRecord
First-write-wins and reported; a differing-quantity resubmit is NOT billedSafe only for an identical retry; never to change a quantity (under-billing risk)
Request-level exceptions (fail the ENTIRE
BatchMeterUsage
call, NOT a per-record status). Each has a client-side guardrail reason code the pipeline stamps when it rejects the row BEFORE the API call (
RejectedClientSide
, not billed), plus the server-side exception if a row reaches the API — surface BOTH when explaining a rejection:
Server-side exceptionClient-side reason codeMeaningAction
InvalidUsageDimensionException
InvalidDimension
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
RejectedClientSide
(reason = the exception name), and emits the
BatchMeterUsageException
metric.
TimestampOutOfBoundsException
MalformedTimestamp
A record older than 24h (e.g. a 30h-old row) or otherwise outside the window — REJECTED, not submittedFix the timestamp; the client-side guardrail rejects the row with reason
MalformedTimestamp
before batching, and one bad timestamp that reaches the API rejects the whole batch until isolated.
InvalidLicenseException
InvalidLicenseArn
A malformed/invalid
LicenseArn
Shape-validate client-side (reason
InvalidLicenseArn
); a server-side
InvalidLicenseException
is isolated by (bounded) bisection and marked
RejectedClientSide
with the exception name.
TooManyTagsException
TooManyTags
More than 5
usageAllocations
tags on a UsageRecord
Reduce to ≤5 tags; the client-side guardrail rejects the row with reason
TooManyTags
before batching.
Sort-key / reject-code ordering: the pipeline validates a row's shape BEFORE it can enter a batch. A row whose sort key (
customerAWSAccountId#dimension#timestamp
) carries a malformed or out-of-window timestamp is rejected client-side with reason code
MalformedTimestamp
(marked
RejectedClientSide
, emits
UsageRecordRejected
) — it never reaches
BatchMeterUsage
, so it does NOT surface as a server-side
TimestampOutOfBoundsException
unless a bad timestamp slips through into a batch. Always name the client-side
MalformedTimestamp
reason code for a malformed-timestamp sort key, and its server-side
TimestampOutOfBoundsException
equivalent.
Full reject-/status-code catalog (the ONE place to cite for any reason-code question). A question like "what does
UnknownDimension
mean?" should be answered from this complete set, mapping the client-side guardrail reason code to its server-side equivalent:
Client-side reason code (
RejectedClientSide
, pre-API, not billed)
Server-side equivalentMeaning
MalformedSortKey
/
SortKeyMismatch
— (never reaches API)Sort key not
customerAWSAccountId#dimension#timestamp
, or its segments don't match the row's own attributes
MalformedTimestamp
/
TimestampOutOfWindow
TimestampOutOfBoundsException
Non-whole-second, unparseable, or >24h-old timestamp
MissingDimension
—Row has no
dimension
InvalidDimension
/
UnknownDimension
InvalidUsageDimensionException
Dimension not defined in the catalog (case-sensitive) —
UnknownDimension
/
InvalidDimension
are the same "dimension not in the AMMP catalog" condition
NegativeQuantity
—Quantity < 0
NonIntegerQuantity
—Quantity not a whole integer
TooManyTags
TooManyTagsException
>5
usageAllocations
tags on a record
TooManyAllocations
—>2500 merged tag-sets in a group
AllocationSumMismatch
—
AllocatedUsageQuantity
values don't sum to the row
quantity
MixedAllocatedAndUnallocated
—Group mixes tagged and untagged non-zero rows
InvalidLicenseArn
InvalidLicenseException
Malformed/invalid
LicenseArn
(also fires when products are mixed in one batch)
Plus the terminal per-record
Results[].Status
verdicts returned BY the API (not client-side reasons):
Success
(billed),
DuplicateRecord
(first-write-wins; a differing-quantity resubmit is NOT billed — under-billing risk, not "benign"),
CustomerNotSubscribed
(no active subscription — do NOT retry). And
UnprocessedRecords
(transient, retryable) — see below.
UnprocessedRecords
(a THIRD, distinct outcome — neither a per-record
Results[].Status
verdict nor a request-level exception):
a PARTIAL-BATCH result (succeeded records in
results
; failed-to-process ones in
unprocessedRecords
— NOT billed, typically TRANSIENT
ThrottlingException
/
InternalServiceException
), SAFE to re-submit (idempotent); the submitter retries ONCE then leaves it for the NEXT scheduled run (alarmed via
UsageRecordUnprocessed
). Do NOT treat it as accepted/billed or confuse it with the FINAL
Results[].Status
verdicts above.
Data tiers: CloudTrail Event History (≤90 days, free, real-time) is the tier the skill queries directly; it shows ALL submissions (incl. rejected/duplicate), Seller Reports show only billed — so "totals don't match" = the rejected/duplicate delta. Two separate history questions, don't conflate them:
Reconciling SUBMITTED (CloudTrail) vs BILLED (Seller Reports). Treat Seller Reports / AMMP Billed Revenue as the billed source of truth; CloudTrail is submissions only. To reconcile, filter CloudTrail to
Status=Success
(exclude
DuplicateRecord
,
CustomerNotSubscribed
, and request-level exceptions — none of those bill) and compare that to the billed report; the leftover gap is the expected rejected/duplicate delta. Watch for
DuplicateRecord
s with DIFFERENT quantities
for the same
(account, licenseArn, dimension, hour)
— that is NOT benign: first-write-wins means the later (often larger) quantity was NOT billed, so it can indicate under-reporting/under-billing. Surface those as a reconciliation red flag rather than dismissing duplicates.
  • BILLED history (any range) needs NO setup — the AMMP Insights "Billed Revenue" dashboard (only
    aws-marketplace:GetSellerDashboard
    ; default 6 months, widen the invoice-date filter). Console-only (QuickSight) + manual CSV export — no on-demand billed API. Programmatic billed data = SDDS (daily S3 push + your Athena/ETL), but push-only and enrollment-forward (no backfill). Skill recommends, doesn't run; never
    GenerateDataSet
    . Detail:
    references/seller-reports.md
    .
    • Discounts / private offers are NOT a metering or Seller-Reports concern — redirect to the public AWS Marketplace channel (AMMP). If a seller asks why billed amounts differ from metered usage because of a discount or private offer, or how to apply/change one, do NOT try to explain or reconcile it from metering data and do NOT stop at "out of metering scope." The pricing that turns metered quantity into billed revenue (public price, private offers, discounts, EULA terms) is managed in the AWS Marketplace Management Portal (AMMP) — state that the discount/private-offer delta is expected (Seller Reports show BILLED revenue after any negotiated discount, while CloudTrail shows raw metered SUBMISSIONS) and redirect discount/private-offer creation and management to AMMP → AWS Marketplace Management Portal / private offers.
  • RAW SUBMISSION history beyond 90 days has NO out-of-the-box source (Seller Reports/SDDS are billed-only; Event History keeps 90 days). Retain via a self-managed CloudTrail trail → S3 on
    metering-marketplace.amazonaws.com
    (plain trail, NOT Lake; Athena) — also from setup forward, no backfill; complementary DynamoDB PITR export of
    aggregated_usage
    . Detail:
    references/query-patterns.md
    .
逐记录
Results[].Status
值:
状态含义操作
Success
已接受并计费无
CustomerNotSubscribed
无活跃订阅⚠️ 不要重试
DuplicateRecord
首次写入优先并已报告;不同数量的重新提交不被计费仅对相同重试安全;绝不要更改数量(少计风险)
请求级异常(使整个
BatchMeterUsage
调用失败,不是逐记录状态)。每个都有管道在 API 调用前拒绝该行时盖上的客户端护栏原因代码(
RejectedClientSide
,不计费),加上如果行到达 API 时的服务端异常——解释拒绝时同时呈现两者:
服务端异常客户端原因代码含义操作
InvalidUsageDimensionException
InvalidDimension
目录中未定义的维度(服务端权威;区分大小写;不计费)修复维度键 / 在 AMMP 中定义它。管道通过二分(深度有界,⌈log2(批次大小)⌉=5)隔离违规记录,使其同批次记录仍然计量,标记为
RejectedClientSide
(原因 = 异常名称),并发出
BatchMeterUsageException
指标。
TimestampOutOfBoundsException
MalformedTimestamp
超过 24 小时的记录(例如 30 小时前行)或超出窗口——被拒绝,不提交修复时间戳;客户端护栏在批处理前以原因
MalformedTimestamp
拒绝该行,一个到达 API 的坏时间戳会在被隔离前拒绝整个批次。
InvalidLicenseException
InvalidLicenseArn
格式错误/无效的
LicenseArn
客户端形状验证(原因
InvalidLicenseArn
);服务端
InvalidLicenseException
通过(有界)二分隔离并标记为
RejectedClientSide
并带异常名称。
TooManyTagsException
TooManyTags
一条 UsageRecord 上超过 5 个
usageAllocations
标签
减少到 ≤5 个标签;客户端护栏在批处理前以原因
TooManyTags
拒绝该行。
排序键 / 拒绝代码顺序: 管道在行可以进入批次之前验证其形状。排序键(
customerAWSAccountId#dimension#timestamp
)带有格式错误或超窗时间戳的行在客户端以原因代码
MalformedTimestamp
被拒绝
(标记为
RejectedClientSide
,发出
UsageRecordRejected
)——它永远不会到达
BatchMeterUsage
,因此除非坏时间戳溜进批次,否则不会作为服务端
TimestampOutOfBoundsException
出现。对格式错误时间戳排序键,始终说出客户端
MalformedTimestamp
原因代码及其服务端
TimestampOutOfBoundsException
对应物。
完整拒绝/状态代码目录(任何原因代码问题的唯一引用处)。 像"
UnknownDimension
是什么意思?"这样的问题应该从这个完整集合回答,将客户端护栏原因代码映射到其服务端对应物:
客户端原因代码(
RejectedClientSide
,API 前,不计费)
服务端对应物含义
MalformedSortKey
/
SortKeyMismatch
—(永不到达 API)排序键不是
customerAWSAccountId#dimension#timestamp
,或其段与行自身属性不匹配
MalformedTimestamp
/
TimestampOutOfWindow
TimestampOutOfBoundsException
非整秒、不可解析或 >24 小时前的时间戳
MissingDimension
—行没有
dimension
InvalidDimension
/
UnknownDimension
InvalidUsageDimensionException
目录中未定义的维度(区分大小写)——
UnknownDimension
/
InvalidDimension
是相同的"维度不在 AMMP 目录中"条件
NegativeQuantity
—数量 < 0
NonIntegerQuantity
—数量不是整数
TooManyTags
TooManyTagsException
一条记录上 >5 个
usageAllocations
标签
TooManyAllocations
—组中 >2500 个合并标签集
AllocationSumMismatch
—
AllocatedUsageQuantity
值之和不等于行的
quantity
MixedAllocatedAndUnallocated
—组混合带标签和未标签的非零行
InvalidLicenseArn
InvalidLicenseException
格式错误/无效的
LicenseArn
(在一个批次中混合产品时也会触发)
加上 API 返回的终态逐记录
Results[].Status
判定(不是客户端原因):
Success
(已计费)、
DuplicateRecord
(首次写入优先;不同数量的重新提交不被计费——少计风险,不是"良性")、
CustomerNotSubscribed
(无活跃订阅——不要重试)。以及
UnprocessedRecords
(暂时的,可重试)——见下文。
UnprocessedRecords
(第三种、不同的结果——既不是逐记录
Results[].Status
判定也不是请求级异常):
部分批次结果(成功的记录在
results
中;处理失败的记录在
unprocessedRecords
中——不计费,通常是暂时的
ThrottlingException
/
InternalServiceException
),可安全重新提交(幂等);提交器重试一次,然后留给下次计划运行(通过
UsageRecordUnprocessed
告警)。不要将其视为已接受/已计费,也不要与上面终态的
Results[].Status
判定混淆。
数据层: CloudTrail 事件历史(≤90 天,免费,实时)是 skill 直接查询的层;它显示所有提交(包括拒绝/重复),卖家报告仅显示已计费——因此"总计不匹配" = 拒绝/重复的差值。两个不同的历史问题,不要混淆:
对账已提交(CloudTrail)与已计费(卖家报告)。 将卖家报告 / AMMP 已计费收入视为已计费的真实来源;CloudTrail 仅提交。要对账,将 CloudTrail 过滤为
Status=Success
(排除
DuplicateRecord
、
CustomerNotSubscribed
和请求级异常——这些都不计费)并与计费报告比较;剩余的差距是预期的拒绝/重复差值。注意同一
(account, licenseArn, dimension, hour)
的
不同数量的
DuplicateRecord
——这不是良性的:首次写入优先意味着后来的(通常更大的)数量不被计费,因此可能表明少报/少计。将它们作为对账红旗而非忽略重复项。
  • 已计费历史(任何范围)无需设置——AMMP Insights "Billed Revenue" 仪表板(仅
    aws-marketplace:GetSellerDashboard
    ;默认 6 个月,扩大发票日期过滤器)。仅控制台(QuickSight)+ 手动 CSV 导出——无按需计费 API。程序化已计费数据 = SDDS(每日 S3 推送 + 您的 Athena/ETL),但仅推送且从注册起(无回填)。Skill 推荐,不运行;绝不
    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 在
    metering-marketplace.amazonaws.com
    上保留(普通 trail,不是 Lake;Athena)——也从设置起,无回填;补充
    aggregated_usage
    的 DynamoDB PITR 导出。详情:
    references/query-patterns.md
    。

Query references

查询参考

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

  • 路由 + 参数:
    references/query-path.md
    · CloudTrail CLI 模式 + 分页:
    references/query-patterns.md
  • 卖家报告对账 + SDDS 自动化边界:
    references/seller-reports.md
    · 维度参考:
    references/dimension-reference.md

Key APIs

关键 API

APIRegionPurpose
ResolveCustomerany region (seller's choice)Resolve token → LicenseArn + customerAWSAccountId + productCode at registration; regional, from the publishing account; independent of the BatchMeterUsage region
BatchMeterUsageseller's metering regionSubmit usage records (hourly)
DescribeEntityus-east-1Validate product / type / visibility
LookupEventsseller's regionQuery metering records (≤90 days)
IAM action names:
aws-marketplace:BatchMeterUsage
(meter),
aws-marketplace:ResolveCustomer
(register),
aws-marketplace:GetSellerDashboard
(reports). Namespace is
aws-marketplace:
, NOT
meteringmarketplace:*
. When diagnosing AccessDenied, name the exact action and check the execution role, condition keys, and Resource-ARN mismatches.
API区域用途
ResolveCustomer任意区域(卖家选择)注册时解析 token → LicenseArn + customerAWSAccountId + productCode;区域性,从发布账户调用;独立于 BatchMeterUsage 区域
BatchMeterUsage卖家的计量区域提交用量记录(每小时)
DescribeEntityus-east-1验证产品 / 类型 / 可见性
LookupEvents卖家区域查询计量记录(≤90 天)
IAM 操作名称:
aws-marketplace:BatchMeterUsage
(计量)、
aws-marketplace:ResolveCustomer
(注册)、
aws-marketplace:GetSellerDashboard
(报告)。命名空间是
aws-marketplace:
,不是
meteringmarketplace:*
。诊断 AccessDenied 时,说出确切的操作并检查执行角色、条件键和 Resource-ARN 不匹配。

Security

安全

  • Least privilege: execution roles grant ONLY the actions used — meter path:
    aws-marketplace:BatchMeterUsage
    + scoped DynamoDB
    Query
    /
    UpdateItem
    on its tables + the
    metering_pending
    index; register:
    aws-marketplace:ResolveCustomer
    + scoped
    Query
    on the
    customerAWSAccountId
    GSI + item ops. No
    dynamodb:Scan
    (lookups use GSI
    Query
    ); never
    dynamodb:*
    or broad
    aws-marketplace:*
    .
  • No swallowed auth errors: subscriber/license lookups do NOT catch
    AccessDeniedException
    as "not found" — permission/config errors surface (Lambda error / 500 / DLQ).
  • Encryption at rest: SQS (
    SqsManagedSseEnabled
    ), SNS (
    alias/aws/sns
    ), DynamoDB (
    SSEEnabled
    ); Lambda + API log groups are explicit resources with
    RetentionInDays
    + optional
    KmsKeyId
    (they contain buyer account IDs / license ARNs).
  • Public registration API: intentionally open (Marketplace POSTs the token unauthenticated) → template attaches a baseline WAF WebACL by default (rate-based + common-exploit;
    WebAclArn
    overrides); API Gateway throttling + access logging always on; register Lambda returns security headers and persists ONLY an allowlist of length-bounded fields (
    AllowedRegistrationFields
    ).
  • Credentials (ephemeral-first): SSO →
    sts assume-role
    ; no long-lived keys, don't ask for keys first. Deployer role uses a conditioned trust policy (not open
    :root
    ) + permissions-boundary-bounded
    iam:CreateRole
    .
  • Stage name:
    StageName
    has no default + an
    AllowedPattern
    rejecting empty; mandatory questionnaire answer;
    sam deploy
    and
    deploy.sh
    fail fast (no
    prod
    fallback).
  • Event scoping: EventBridge rule scoped to
    source: ["aws.agreement-marketplace"]
    ; the EventBridge→SQS policy via
    aws:SourceArn
    .
  • Data classification: customer AWS account IDs + agreement metadata are sensitive — avoid logging full identifiers; encrypt log groups that contain them.
  • 最小权限: 执行角色仅授予使用的操作——计量路径:
    aws-marketplace:BatchMeterUsage
    + 其表上的作用域 DynamoDB
    Query
    /
    UpdateItem
    +
    metering_pending
    索引;注册:
    aws-marketplace:ResolveCustomer
    +
    customerAWSAccountId
    GSI 上的作用域
    Query
    + 项目操作。无
    dynamodb:Scan
    (查找使用 GSI
    Query
    );绝不
    dynamodb:*
    或广泛的
    aws-marketplace:*
    。
  • 不吞没认证错误: 订阅者/许可证查找不将
    AccessDeniedException
    捕获为"未找到"——权限/配置错误会浮出水面(Lambda 错误 / 500 / DLQ)。
  • 静态加密: SQS(
    SqsManagedSseEnabled
    )、SNS(
    alias/aws/sns
    )、DynamoDB(
    SSEEnabled
    );Lambda + API 日志组是显式资源,带
    RetentionInDays
    + 可选
    KmsKeyId
    (它们包含买家账户 ID / 许可证 ARN)。
  • 公开注册 API: 故意开放(Marketplace 未认证地 POST token)→ 模板默认附加基线 WAF WebACL(基于速率 + 常见漏洞利用;
    WebAclArn
    可覆盖);API Gateway 节流 + 访问日志始终开启;注册 Lambda 返回安全头并仅持久化长度受限字段的允许列表(
    AllowedRegistrationFields
    )。
  • 凭证(临时优先): SSO →
    sts assume-role
    ;无长期密钥,不要先要密钥。部署者角色使用条件化信任策略(不是开放的
    :root
    )+ 权限边界受限的
    iam:CreateRole
    。
  • 阶段名称:
    StageName
    无默认值 + 拒绝为空的
    AllowedPattern
    ;强制问卷答案;
    sam deploy
    和
    deploy.sh
    快速失败(无
    prod
    回退)。
  • 事件范围: EventBridge 规则范围限定为
    source: ["aws.agreement-marketplace"]
    ;EventBridge→SQS 策略通过
    aws:SourceArn
    。
  • 数据分类: 客户 AWS 账户 ID + 协议元数据敏感——避免记录完整标识符;加密包含它们的日志组。