AWS WAF AI Traffic MonetizationでBot UAを再現して402 Payment Requiredを返してみた
はじめに
2026年6月、AWS WAFにAI Traffic Monetization機能が追加されました。
条件に合致し、必要な支払い情報がないリクエストに対してHTTP 402 Payment Requiredとx402 price manifestを返す機能です。Botトラフィックに「ブロック」ではなく「支払いを要求する」形で対応する選択肢が増えました。
x402プロトコルの背景は以下の記事を参照してください。
従来の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利用料は通常どおり発生します。
事前準備: ウォレットアドレス
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で生成し、MonetizationConfig の Token に指定しました。
ルール構成
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エージェントが自律的に支払いを行うユースケースへの一歩であり、今後の動向に注目していきたいと思います。
参考リンク
参考: 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"
}
}








