AWS WAF AI Traffic MonetizationでBot UAを再現して402 Payment Requiredを返してみた

AWS WAF AI Traffic MonetizationでBot UAを再現して402 Payment Requiredを返してみた

AWS WAFに追加されたAI Traffic Monetization機能を検証しました。Bot UAを指定した模擬リクエストや、Rate-basedルールでしきい値を超えたリクエストに対して、402 Payment Requiredとx402 price manifestが返る動作を確認しています。
2026.06.16

はじめに

2026年6月、AWS WAFにAI Traffic Monetization機能が追加されました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/06/aws-waf-ai-traffic-monetization/

条件に合致し、必要な支払い情報がないリクエストに対してHTTP 402 Payment Requiredとx402 price manifestを返す機能です。Botトラフィックに「ブロック」ではなく「支払いを要求する」形で対応する選択肢が増えました。

x402プロトコルの背景は以下の記事を参照してください。

https://dev.classmethod.jp/articles/lets-change-the-world-with-x402-questioning-ai-company-ethics/

従来のBot対応アクションとの比較です(Block / Challenge列は一般的な挙動の整理で、本記事の検証対象はMonetizeです)。

観点 Block Challenge Monetize (402)
HTTP 応答 403 Forbidden(既定) トークンなし/無効時に202(Challengeインタースティシャル) 402 Payment Required
Bot 側の挙動 アクセス不可 突破 or 離脱 支払い後にアクセス可(*)
コンテンツ提供 なし 突破時のみ 対価と引き換え
ユースケース 悪意ある Bot 排除 人間/Bot 判別 AI クローラーへの支払い要求

(*) 支払い完了後のアクセス確認は今回の検証対象外です。

検証環境

項目
AWS CLI 2.35.5
ウェブ ACL waf-402-evaluation(CLOUDFRONT スコープ)
CloudFront 検証用ディストリビューション(検証完了後に削除済み)
Bot Control Common(マネージドルールグループ、Count モード)
Wallet Base Sepolia testnet(テスト専用)
Currency Mode TEST
Base Price 0.001 USDC

CurrencyMode: TESTではテストネットトークンを使用するため、x402の支払い自体に実費は発生しません。ただしAWS WAF、Bot Control、CloudFront等のAWS利用料は通常どおり発生します。

https://aws.amazon.com/waf/pricing/

事前準備: ウォレットアドレス

Monetizeアクションの設定には、x402の支払い先となるEthereumアドレスが必要です。CurrencyMode: TESTで402レスポンスの返却を確認するだけなら、有効なアドレス形式の文字列があれば十分で、残高は不要です。

最も簡単な方法は openssl で生成する方法です。

echo "0x$(openssl rand -hex 20)"

秘密鍵も得たい場合はPythonで生成できます。

import os, ecdsa
from Crypto.Hash import keccak

private_key = os.urandom(32)
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
pk = sk.get_verifying_key().to_string()
k = keccak.new(digest_bits=256)
k.update(pk)
addr = k.digest()[-20:]
print(f"Address: 0x{addr.hex()}")
print(f"Private Key: 0x{private_key.hex()}")

今回は使い捨てアドレスをPythonで生成し、MonetizationConfigToken に指定しました。

ルール構成

4つのルールを以下の優先順位で設定しました。

Priority Name 条件 Action
0 BotControlManagedRuleGroup Bot Control (Common) Count(ラベル付与のみ)
5 RateLabelSearchPage RateBased 100req/60s, AggregateKeyType: IP, ScopeDown: IP Set AND /search パス Count + Label: custom:rate-exceeded:search-page
10 MonetizeRateExceeded Label custom:rate-exceeded:search-page AND /search パス Monetize (PriceMultiplier: 10)
20 MonetizeBotTraffic Namespace awswaf:managed:aws:bot-control:bot: Monetize (PriceMultiplier: 1)

MonetizationConfig

Web ACLのトップレベルに設定する、Monetizeアクション共通の支払い先情報です。

{
  "MonetizationConfig": {
    "PaymentMethodTokens": [
      {
        "TokenType": "CRYPTO_WALLET_ADDRESS",
        "Token": "<wallet address>"
      }
    ],
    "BasePrice": {
      "Amount": 0.001,
      "Currency": "USDC"
    },
    "CurrencyMode": "TEST",
    "Network": "BASE_SEPOLIA"
  }
}

Monetizeアクションを含むルール例

MonetizeBotTraffic(Priority 20)の定義です。

{
  "Name": "MonetizeBotTraffic",
  "Priority": 20,
  "Statement": {
    "LabelMatchStatement": {
      "Scope": "NAMESPACE",
      "Key": "awswaf:managed:aws:bot-control:bot:"
    }
  },
  "Action": {
    "Monetize": {
      "PriceMultiplier": 1
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "MonetizeBotTraffic"
  }
}

ラベル連鎖の流れは次のとおりです。

  • Bot Controlが awswaf:managed:aws:bot-control:bot:category:ai 等のラベルを付与
    • → Priority 20がNAMESPACEマッチでMonetize発動
  • Priority 5がScopeDown条件にマッチする /search リクエストをIP単位で集計し、しきい値を超えたIPからの対象リクエストへラベルを付与
    • → Priority 10がLABELマッチ + パス条件でMonetize発動

LabelMatchStatementには LABEL(完全一致)と NAMESPACE(prefixマッチ)の2種類があります。Bot Controlのラベルはフルパスで付与されるため、bot: namespace配下のラベルをまとめて対象にするには NAMESPACE スコープを使います。

Monetizeは終端アクションのため、複数ルールの条件に該当し得る場合は優先順位の高い(Priorityの小さい)ルールが適用され、後続のMonetizeルールは評価されません。したがってPriceMultiplierが合算されることはありません。

ScopeDownStatementは、Rate-basedルールで集計・評価するリクエスト範囲を絞る条件です。今回の構成では /search にマッチするリクエストを対象としてレートを集計し、しきい値超過でラベルを付与しています。しきい値超過後もラベル付与の対象はScopeDown条件に一致する /search リクエストです。そのためMonetizeルール側でも /search のパス条件をANDで追加し、402を返す対象パスを明確化しています。

Bot UA で 402 を確認する

User-Agent: GPTBot/1.0 を指定してリクエストを送信しました。実在クローラーの模倣ではなく、Bot Controlのラベル付与動作を確認する目的です。

# Bot UA でリクエスト
curl -s -D - -H "User-Agent: GPTBot/1.0" \
  "https://<CloudFront ドメイン>/"

# 通常ブラウザ UA でリクエスト
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  "https://<CloudFront ドメイン>/"

結果

UA HTTP Status 備考
GPTBot/1.0 402 x402 price manifest 返却
Mozilla/5.0 (Chrome) 200 正常通過

402 レスポンスの中身

payment-required ヘッダーにbase64エンコードされたprice manifestが含まれていました。デコード結果は以下のとおりです。

{
  "accepts": [{
    "amount": "1000",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "extra": { "name": "USDC", "version": "2" },
    "maxTimeoutSeconds": 300,
    "network": "eip155:84532",
    "payTo": "<wallet address>",
    "scheme": "exact"
  }],
  "error": "PAYMENT-SIGNATURE header is required",
  "extensions": { "payment-identifier": { "info": { "required": false } } },
  "resource": { "url": "https://<CloudFront ドメイン>/" },
  "x402Version": 2
}

amount: "1000" はUSDCの最小単位(6桁)で表現された0.001 USDCです。Base Price 0.001 × PriceMultiplier 1の結果です。

レートベースルールで UA に依存しない 402 を返す

UAに依存せず、同一IPから対象パスへ短時間に一定数以上のリクエストが発生した場合に402を返す動作を確認しました。

# 1. /search にバーストリクエスト送信 (150リクエスト, IPv4 強制)
for i in $(seq 1 150); do
  curl -4 -s -o /dev/null "https://<CloudFront ドメイン>/search?burst=${i}" &
  [ $((i % 50)) -eq 0 ] && wait
done
wait

# 2. レートルール評価の反映を待機
sleep 60

# 3. 通常 UA で /search を再確認
curl -4 -s -D - \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://<CloudFront ドメイン>/search?q=confirm"

結果

条件 HTTP Status 備考
レート未超過(通常UA、/search) 200 正常通過
レート超過後(通常UA、/search) 402 PriceMultiplier: 10 適用

レート超過後の402レスポンスでは amount: "10000"(0.01 USDC = Base Price × PriceMultiplier 10)が返されました。Bot Controlのラベルが付かない通常UAのアクセスでも、IP + レート条件に合致した場合はMonetizeを発動できます。

パス限定で /search のみ 402 を返す

最後に、特定パスのみをMonetize対象にするシナリオを検証しました。今回の検証環境ではこのブラウザーUAでPriority 20(Bot NAMESPACEマッチ)が発動しなかったため、Priority 10(レートベース + パス条件)の動作を確認できました。

前述と同様に /search パスへ150リクエストを送信し、60秒待機したあと各パスを確認しました。

curl -4 -s -o /dev/null -w "/search   -> HTTP %{http_code}\n" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  "https://<CloudFront ドメイン>/search?q=test"

curl -4 -s -o /dev/null -w "/          -> HTTP %{http_code}\n" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  "https://<CloudFront ドメイン>/"

curl -4 -s -o /dev/null -w "/articles  -> HTTP %{http_code}\n" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  "https://<CloudFront ドメイン>/articles/sample"

結果

パス HTTP Status 備考
/search?q=test 402 Monetize 発動
/ 200 影響なし
/articles/sample 404 オリジン応答。Monetize非発動

/articles/sample はオリジンに該当ファイルがないため404でしたが、402ではなくMonetizeは発動していません。

/articles が402にならないのは、Rate-basedルールのScopeDownStatementが /search に限定されているためです。さらに MonetizeRateExceeded ルール側にも /search のByteMatch条件をANDで入れており、対象パスを明確にしています。

まとめ

CloudflareのPay Per Crawl / x402対応に続き、AWS WAFでもMonetizeアクションが利用可能になりました。402 Payment Requiredとx402 price manifestを返せます。Bot ControlのラベルやRate-basedルールと組み合わせ、「どのトラフィックに」「いくらで」支払いを要求するかを制御できます(Base PriceとPriceMultiplier)。Block・Challenge以外に「支払いを要求する」という選択肢が加わりました。AIエージェントが自律的に支払いを行うユースケースへの一歩であり、今後の動向に注目していきたいと思います。

参考リンク

https://docs.aws.amazon.com/waf/latest/developerguide/waf-ai-traffic-monetization.html

https://www.x402.org/

参考: Web ACL 全体定義

Web ACL JSON(クリックで展開)
{
  "Name": "waf-402-evaluation",
  "Scope": "CLOUDFRONT",
  "DefaultAction": { "Allow": {} },
  "MonetizationConfig": {
    "PaymentMethodTokens": [
      {
        "TokenType": "CRYPTO_WALLET_ADDRESS",
        "Token": "<wallet address>"
      }
    ],
    "BasePrice": {
      "Amount": 0.001,
      "Currency": "USDC"
    },
    "CurrencyMode": "TEST",
    "Network": "BASE_SEPOLIA"
  },
  "Rules": [
    {
      "Name": "BotControlManagedRuleGroup",
      "Priority": 0,
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesBotControlRuleSet",
          "ManagedRuleGroupConfigs": [
            { "AWSManagedRulesBotControlRuleSet": { "InspectionLevel": "COMMON" } }
          ]
        }
      },
      "OverrideAction": { "Count": {} },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "BotControlManagedRuleGroup"
      }
    },
    {
      "Name": "RateLabelSearchPage",
      "Priority": 5,
      "Statement": {
        "RateBasedStatement": {
          "Limit": 100,
          "EvaluationWindowSec": 60,
          "AggregateKeyType": "IP",
          "ScopeDownStatement": {
            "AndStatement": {
              "Statements": [
                {
                  "IPSetReferenceStatement": {
                    "ARN": "<IP Set ARN>"
                  }
                },
                {
                  "ByteMatchStatement": {
                    "SearchString": "/search",
                    "FieldToMatch": { "UriPath": {} },
                    "TextTransformations": [{ "Priority": 0, "Type": "NONE" }],
                    "PositionalConstraint": "STARTS_WITH"
                  }
                }
              ]
            }
          }
        }
      },
      "Action": { "Count": {} },
      "RuleLabels": [{ "Name": "custom:rate-exceeded:search-page" }],
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLabelSearchPage"
      }
    },
    {
      "Name": "MonetizeRateExceeded",
      "Priority": 10,
      "Statement": {
        "AndStatement": {
          "Statements": [
            {
              "LabelMatchStatement": {
                "Scope": "LABEL",
                "Key": "custom:rate-exceeded:search-page"
              }
            },
            {
              "ByteMatchStatement": {
                "SearchString": "/search",
                "FieldToMatch": { "UriPath": {} },
                "TextTransformations": [{ "Priority": 0, "Type": "NONE" }],
                "PositionalConstraint": "STARTS_WITH"
              }
            }
          ]
        }
      },
      "Action": {
        "Monetize": {
          "PriceMultiplier": 10
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "MonetizeRateExceeded"
      }
    },
    {
      "Name": "MonetizeBotTraffic",
      "Priority": 20,
      "Statement": {
        "LabelMatchStatement": {
          "Scope": "NAMESPACE",
          "Key": "awswaf:managed:aws:bot-control:bot:"
        }
      },
      "Action": {
        "Monetize": {
          "PriceMultiplier": 1
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "MonetizeBotTraffic"
      }
    }
  ],
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "waf-402-evaluation"
  }
}

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事