ゲームの開発者のみなさん、こんにちは。今回はポイント改ざんやアイテム複製など、プレイヤーによる不正行為を防ぐためのサーバーサイド設計についてお話しします。オンラインゲームでは、通信の仕組みを悪用してズルをしようとする強者(?)が必ず現れます。そこで本記事では、初心者サーバーエンジニアの方にも分かりやすいように、「クライアントは信用できない」という大原則から始まり、具体的な実装例やチェックリストまで、軽妙な語り口でまとめてみました。読めばあなたのゲームのセキュリティ理解がグッと深まる…かもしれません。
それでは早速、本題に入りましょう。チーターに負けない堅牢なゲームサーバー作りの始まりです!
クライアントは信用できない – サーバーをゲームの司令塔にしよう
オンラインゲーム開発の世界には昔からの鉄則があります。それはズバリ:「クライアント(ゲームアプリ)は信用しない」ということ。プレイヤーの手元にあるクライアント側では、データや挙動はいくらでも改ざんできてしまうからです。例えば、ゲーム内通貨を100ポイント持っているはずのプレイヤーが、クライアントのメモリを書き換えて10000ポイントあるように装ったとしても、それを鵜呑みにしてはいけません。
ゲームの本当の進行と判定は、常にサーバー側で行う必要があります。

クライアント-サーバー型ゲームの基本構成。クライアントからの要求①に対し、サーバー(アプリケーションサーバ)はデータベースに問い合わせ②③を行い、処理結果をクライアントに返す④。重要なゲームロジックやデータ更新はサーバー側で完結させる.
たとえばガチャ(ランダムなアイテム入手)
の実装を考えてみましょう。クライアントで乱数を生成してレアアイテムを引いた!と自己申告させるのは非常に危険です。必ずサーバー側で抽選を行い、その結果(当たったアイテム)だけをクライアントに送るようにしましょう 。クライアントに渡す情報は「画面表示に必要な最低限」に留め、ゲームの状態を変更する処理(ポイント加減算やアイテム付与など)はすべてサーバー上で実施します 。こうすることで、クライアントを改ざんされてもゲーム内の整合性が崩れないようにできます。
要するに、
サーバー=厳格な司令塔であり、
クライアント=演出装置に徹してもらうイメージです。サーバーが「これが君の結果だよ」とお墨付きを与えない限り、ゲームの重要情報は一切確定しません。こうした「サーバー権限型」の設計にすることで、チート行為の大半は未然に防ぐことができます。クライアントは疑い深く扱う――これがまず第一歩です。
ポイント消費・アイテム購入はサーバーで二重チェック!そのワークフロー
ゲーム内ショップでアイテムを購入する、ガチャを引く、スタミナを消費してクエストに入る…いずれも
ポイントやアイテムの消費処理が伴います。ここでの合言葉は「DB(データベース)を信じよ、しかし確認せよ」です。具体的には、
現在の所持ポイントやアイテム数を必ずDBから再確認し、条件を満たす場合にのみ消費処理を行うというワークフローを徹底します 。
典型的なポイント消費処理の流れを見てみましょう。
-
クライアント:「○○ポイント支払ってアイテムAを買いたいです!」とサーバーにリクエストを送る(※支払いに必要なポイント数やアイテムIDは本来サーバーが把握している)。
-
サーバー:リクエストを受け取ったら、まず
データベースからユーザーの現在ポイント残高を取得します。この際に該当行にロックをかけて読み込むと安全です(後述するトランザクション管理の話につながります)。
-
サーバー:残高を確認し、必要ポイントに足りているかチェックします。足りなければ「ポイント不足」エラーを返して処理を終了します。
-
サーバー:十分な残高がある場合、
ポイント残高を減算し、購入アイテムをユーザーのインベントリに追加する処理を行います。これら一連の変更は後述のようにトランザクション内で実行し、確実にDBに保存します。
-
サーバー:処理結果(購入成功や失敗)をクライアントにレスポンスします。クライアント側では成功の場合アイテムを増やして見せたり、失敗ならエラーメッセージを表示します。
// 簡略化した擬似コード例(ポイントでアイテム購入)
function purchaseItem(userId, itemId):
BEGIN TRANSACTION
# ユーザーの現在ポイントを取得(行ロック)
current_points = SELECT points FROM Users WHERE id = userId FOR UPDATE;
price = SELECT price FROM Items WHERE id = itemId;
IF current_points < price:
ROLLBACK;
return "エラー: ポイント不足";
END IF
# ポイントを減算し、アイテムを付与
UPDATE Users SET points = points - price WHERE id = userId;
INSERT INTO UserItems(user_id, item_id, obtained_at=NOW());
COMMIT;
return "購入成功";
上記のように、
サーバー側で二重の確認と更新を行うことで、「本当はポイントなんて持っていないのにアイテムを不正入手する」といったチートは防げます。実際、クライアント側だけで残高チェックをしていたようなケースでは、プレイヤーがリクエスト内容を改ざんして
本来有償通貨で引くガチャを無償通貨で引けてしまったという脆弱性も報告されています 。サーバー側できちんと「ポイント種別と支払い先の整合性」をチェックし、そもそも不正な組み合わせのリクエストを受け付けない設計にすることが重要です。
また、App StoreやGoogle Play決済を利用するアイテム購入では、ストアから発行される
購入レシート(トークン)をサーバーで検証し、アイテム付与する方式が一般的です。このように外部決済を使う場合も、最終的なアイテム付与処理はサーバーがレシートの正当性を確認してから行いましょう。サーバー初心者のUnityエンジニアの方は、クライアントで「購入成功!」と表示させる前に
必ずサーバー側でお墨付きが出ていることを意識してくださいね。
リクエスト改ざんの対策 – 署名付きリクエスト&リプレイ攻撃防止
「通信の改ざん」と「繰り返し送信」はオンラインゲームでよくある攻撃パターンです。悪いプレイヤーは通信を盗聴・操作するプロキシツールを使って、サーバーとのやり取りを横取り・書き換えしようとします。例えば「このアイテムを購入する」というリクエストの中身を書き換えて、
本来は買えないアイテムIDを指定して送信したり、
一度きりのはずの操作を何度も再送信したりするのです。サーバーがそれを丸呑みしてしまえば、不正行為が成立してしまいます 。
では、どう防ぐか?ポイントは2つ、「リクエストの正当性確認」と「使い捨てチェック」です。
署名(ハッシュ)でリクエスト改ざん検知
まず、クライアントからサーバーへの
リクエストに署名(シグネチャ)を付与しましょう。これはリクエスト内容が途中で変更されていないことを確認するお守りのようなものです。一般的な手法として、
リクエストボディや重要パラメータからHMAC-SHA256といったハッシュ値を計算し、それをリクエストヘッダに載せて送信します (
HMAC認証)。サーバー側では同じ秘密鍵を使ってハッシュを再計算し、送信されてきた署名と突き合わせます。これが一致していなければ、途中でデータが書き換えられた可能性が高いのでリクエストを拒否します。言い換えれば、
改ざんされたデータにはサインが無効になるので無視するという仕組みです。
実装のイメージとしては、例えばHTTPヘッダにX-Request-Signatureのような項目を設け、署名 = HMAC(secret, リクエストボディ+タイムスタンプ)という値を入れて送ります。サーバー側でsecret(サーバーとクライアントだけが共有する鍵)を使ってハッシュを検証し、正しければ処理を続行、間違っていれば即座に弾く、という流れです。「このダメージ量のデータを書き換えられていないかな?よし署名OK、通過!」とサーバーが門番になるイメージですね 。
📝
ワンポイント: HTTPS通信自体も暗号化されていますが、ユーザー自身が自分の端末の通信を操作する場合(プロキシを立てる等)にはHTTPSでも改ざんは可能です。そのため
アプリとサーバー間で事前共有した秘密情報を使って署名チェックを行うことで、より改ざん検知のレイヤーを一つ追加できます。ただし、クライアントアプリを解析されて秘密鍵が漏れるリスクもあるため、
可能な範囲で難読化や鍵の安全管理も必要です。この点はモバイルアプリのセキュリティの深い話になるのでここでは割愛します。
ノンスやタイムスタンプでリプレイ攻撃防止
次に、
リプレイアタック(同じリクエストの再送)への対策です。どんなに内容が正しいリクエストでも、それを何度も送りつけられると困る場合があります。例えば「1日1回無料でもらえる報酬」を本来1回だけ受け取れるAPIがあったとしましょう。悪意あるユーザーは最初の1回の通信を保存しておき、
同じ内容をあとで何度も再送信するかもしれません。サーバーがそれを何度も処理してしまえば、ユーザーは本来1回のところを好きなだけ得することになります。
この防止策として、リクエストに
一意なノンス(nonce)やタイムスタンプを含め、サーバー側で「このリクエストは過去に処理したか?」「有効期限内か?」
をチェックします。具体例としては、各リクエストにnonce(一度きりのランダム文字列)とtimestamp(現在時刻)を付与し、それも含めて先ほどの署名に組み込みます。サーバーは過去に受け取ったnonceを保存しておき、再度同じものが来たら「おっと、これは二重送信だな?」と判断して拒否します。またタイムスタンプもチェックし、例えば時刻が現在より5分以上昔だった場合は無効とみなすことで、録音再生したような古いリクエストをはじけます。
実際のゲーム開発では、
サーバー側で「○○は既に実行済み」フラグや最終実行時間を記録しておき、重複リクエストを検知・無視することが行われています (
DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering)】。例えば「1日1回無料ガチャ」を引くAPIなら、サーバーに
最後に引いた日時を記録し、それを見て2回目以降の試行は拒否します。また、後述する
トランザクション処理と組み合わせて、一度しか使えない購入処理用のIDやフラグを用いる手もあります。
このように、
署名(ハッシュ)で改ざん防止しつつ、
ノンスやタイムスタンプで使い回し防止を施すことで、リクエスト改ざん系のチート攻撃に対抗できます。少し技術的な実装が必要ですが、一度組み込んでしまえば強力な守りとなるので是非チャレンジしてみてください。
トークン方式の設計 – IDや数量を直接渡さない工夫
クライアントからサーバーに送るリクエストパラメータを
そのまま信用しないという発想は、別のデザインアプローチにも応用できます。「IDや数量を直接渡さずトークンなどで渡す」とは、クライアントに重要な値を決めさせない・見せない工夫のことです。
例えば、ゲーム内ショップで
アイテム購入を行う際に「アイテムID: 123、価格: 500ポイント」という情報をクライアントが送ってきたらどうでしょう? 悪いユーザーなら、このリクエストを改ざんして「価格: 1ポイント」に書き換えるかもしれませんよね。これを防ぐために、
サーバー側で取引情報を一時的に記録したトークンを発行し、クライアントはそれを使って最終確定リクエストを送る、という二段構えにする方法があります。
トークン方式の購入フロー例:
-
クライアント:「アイテムAを購入したいです!」とサーバーにリクエスト(この時点ではアイテムID程度だけ送る。価格や在庫数など信用できない情報は送らない)。
-
サーバー:アイテムAの価格や在庫を自サーバーで確認し、購入に必要なデータを内部に保持します。例えば「ユーザーXがアイテムA購入、価格500ポイント、有効期限60秒」のような情報を記録し、それに対応する購入トークン(例: purchase_token = "XYZ123")を発行します。
-
サーバー:クライアントに「purchase_token = XYZ123」を返信します。クライアントには価格等は伝えず、「このトークンで購入手続きを進めてね」という合図だけ送るイメージです。
-
クライアント:受け取ったpurchase_tokenを使って「このトークンで購入確定お願いします!」とサーバーにリクエストします。
-
サーバー:トークンXYZ123に紐づいた内部情報を引き当て、「ユーザーXがアイテムAを500ポイントで購入」と分かるので、改めてポイント残高チェック→減算→アイテム付与を実行します(
当然ここもトランザクションで)。トークンは使い捨てなので無効化し、二度と使えないようにします。
-
サーバー:購入成功をクライアントに返す。めでたしめでたし。
この方式なら、
クライアントからサーバーへ「価格:500ポイント」という情報を一度も送っていません。価格はサーバー内部に隠され、トークン自体には秘密裏にサーバーだけが知る情報と紐づいています。仮にユーザーがトークンを盗み見ても、内容を勝手に変えることはできませんし、別の安いアイテム用のトークンを流用して高額アイテムを買う…なんてこともできません。トークンXYZ123が指す内容はサーバー上で固定されているからです。
似た発想で、前述した
ガチャの例では「どの通貨で引くガチャか」をクライアントに決めさせないという対策がありました。有償限定のガチャなら通貨種別は有償石に固定し、リクエストで通貨IDを受け取らなくても済むように設計する、といった手法です(
DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering)】。要するに、「
サーバーが分かっていることをわざわざクライアントに送らせない」というのも立派なセキュリティ設計なんですね。渡す情報が減れば、改ざんされるリスクもその分減るというわけです。
擬似コードでトークン利用のイメージ:
// 購入要求を受け付け、一時トークンを発行
function requestPurchase(user, itemId):
price = Items[itemId].price
if (!canPurchase(user, itemId, price)) return "購入不可";
token = generateUniqueToken()
storeTempData(token, {user, itemId, price}, expires=60s)
return token // トークンだけ返す
// 購入確定リクエスト(トークンを使って実行)
function confirmPurchase(token):
data = lookupTempData(token)
if (data == null) return "無効なトークン";
user = data.user, itemId = data.itemId, price = data.price
BEGIN TRANSACTION
userPoints = SELECT points FROM Users WHERE id=user FOR UPDATE
if (userPoints < price) { ROLLBACK; return "ポイント不足"; }
UPDATE Users SET points = points - price WHERE id=user
INSERT UserItems(user, itemId)
COMMIT
invalidateToken(token) // トークンを無効化
return "購入成功";
上記のように二段階にすることで、クライアントが勝手に価格を書き換える隙を無くせます。
一手間かかりますが、高額アイテムの不正取得などを確実に防ぐための有効な策です。
データの一貫性確保 – トランザクション管理でアイテム複製を防ぐ
次はデータベースの
トランザクション管理と
排他制御の話です。「なんだか急に難しそう」と思うかもしれませんが、不正防止には避けて通れない要素です。これを怠ると、「ボタン連打でアイテム増殖」なんていう典型的なチートを許してしまうことになります。
ボタン連打・同時リクエストへの対処
ユーザーが運悪く(?)
同じリクエストをほぼ同時に二度送ってしまった場合を考えましょう。例えば通信が遅くて購入ボタンを連打してしまったとか、悪意を持って2台の端末から同時に同じアカウントの購入APIを叩いたとか…。サーバー側の実装が甘いと、この「ほぼ同時」に来た複数リクエストを
両方とも処理してしまい、結果的に二重にアイテムを与えてしまう恐れがあります。
実際にあった例では、ガチャを引くAPIに対して
同時に大量のリクエストを送りつけると、以下のような不備が露呈しました (
DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering)】:
- 複数スレッド(複数リクエスト)が並行して
DBからポイント残高を読み込み、「まだ十分ポイントがある」とそれぞれ判断する。
- 各スレッドが同時にポイントを1回分消費する処理を書き込もうとするが、適切なロックがないため競合状態になる。
- 結果として
ポイントは1回分しか減らないのに、ガチャ抽選が複数回実行されてしまった(=1回分のコストで複数回ガチャを引けてしまった)。
この問題への対策はシンプルで、
処理にロックをかけて順番に捌くことです。データベースのトランザクション機能を使って、
ポイント残高の読み取り→更新→ガチャ結果付与までを一つの不可分な処理にすれば、たとえ100本同時にリクエストが来ようとも一人ずつ順番待ちさせることができます。最初の1件が終われば次の1件…という風に処理されるため、ポイントが重複して消費されない限り二度目以降のリクエストは「残高不足」と判定され、余計なガチャ実行は起きません。
データベースによっては、
行ロック(SELECT ... FOR UPDATE)や楽観ロック(バージョン番号やタイムスタンプでの整合性チェック)など様々な手法が取れます。またアプリケーションサーバー側でミューテックス(排他制御)を行い、同じユーザーからの並行処理をコード上で順番待ちさせる方法もあります。いずれにせよ大事なのは、「一連の更新処理は原子的(atomic)に実行される」ことを保証することです。これによってデータの整合性が守られ、「1つの消費に対して1つの結果」という当たり前のルールが破られないようになります。
現場のチェックリスト的には、以下の点を確認しましょう:
- 重要な更新処理(ポイント減算+アイテム付与など)は
トランザクション内で行っているか?
- DBの該当レコードに
ロックをかけて読み書きしているか?(あるいはUPSERTやINCREMENTを使って一発で更新するクエリでもOK)
- アプリ側で
ボタンの連打を無効にするUX上の工夫もしているか?(サーバーだけではなくクライアント側でも連打防止策があると尚良いです)
これらを怠ると、悪意がなくてもバグでアイテムが増殖してしまったり、悪意あるユーザーには意図的にそこを突かれてしまいます。
「銀行の口座引き落とし処理」と同じくらい厳重にゲーム内通貨の処理も守ってあげましょう。
冪等性(Idempotence)で二重処理を許さない設計
少し関連しますが、
RESTful通信の落とし穴として「同じリクエストが複数回実行されても結果が変わらない性質(冪等性)」があります。たとえばHTTPのGETやPUTは理論上は冪等であるべきとされていますが、ゲームのPOSTアクション(購入やガチャ)は基本的に冪等ではありません。とはいえ、
クライアントの通信再試行やネットワークの不調による重複リクエストは現実に起こり得ます。その際にサーバーが二重処理しないよう、
設計的に工夫することが重要です。
冪等性を担保する一般的な方法の一つに、
「リクエストごとにユニークなID(リクエストIDやトランザクションID)を付けてもらい、同じIDのリクエストは二度処理しない」というものがあります。これは先ほどのノンスと原理は似ていますが、より「APIの利用者にユニークIDを要求する」という設計寄りの話です。大手の決済API(StripeやPayPalなど)でもIdempotency-Keyという仕組みで重複課金を防いでいます。クライアント(ゲーム側)がUUIDのような一意のキーを生成してリクエストに含め、サーバーはそれを見て「このキーではまだ処理していないな、では実行しよう」「おっと同じキーはもう処理済みだ、では結果だけ返そう」と判断するのです。
ゲームサーバーにおいても、特に
課金アイテムの付与処理など重要度の高い操作ではこの冪等性キーを導入すると安心です。例えば購入処理APIにorder_idのようなパラメータを設け、サーバー側でそれを記録しておきます。二度目以降の同じorder_idからのリクエストは「Duplicate」として弾くか、もしくは「以前成功した処理の結果」をそのまま返すようにします。後者の「結果を再利用する」方式であれば、クライアントにはまるで二重に実行しても同じ結果が返ってくる(=冪等)ように見えるので安心ですね。
冪等性の考慮は、ユーザー体験の向上にも繋がります。例えば決済で一度エラーが出てもユーザーが「もう一回購入ボタン押して大丈夫かな?二重に課金されたら嫌だな…」
と不安を持たないようにする効果があります。サーバー側が「絶対に二重で処理しません。安心してリトライどうぞ!」という態度を示せれば、開発者もユーザーもハッピーです。
まとめると、
RESTful APIの冪等性は難しく聞こえますが、基本は
重複を検出して無視 or 同一結果を返すことです。これもセキュリティとユーザビリティ双方にメリットがある設計上のベストプラクティスと言えるでしょう。
## セキュア設計チェックリスト
最後に、本記事で紹介した内容を
チェックリスト形式でおさらいしておきましょう。ゲームサーバー実装時に下記ポイントを一つずつチェックすれば、不正行為に対する耐性はかなり高まるはずです。
-
クライアントを信用しない: すべての重要なゲーム処理(抽選結果の決定、ポイント計算、アイテム付与など)はサーバー側で行う。クライアントには結果や表示用情報のみ渡す 。
-
入力検証を徹底: サーバーに届いたリクエストパラメータは必ず妥当性チェックする。存在しないIDや不正な組み合わせ(例: 無償通貨で有償限定ガチャを引こうとしていないか等)を弾く 。
-
ポイント消費はサーバーで確認: 残高や在庫はDBの値を参照し、条件を満たす場合のみ減算・購入処理を行う 。クライアント任せにしないこと。
-
トランザクション&ロック活用: 複数のDB操作にまたがる処理はトランザクションで括り、必要に応じて行ロック/楽観ロックを使って同時実行制御する 。これでアイテム複製バグを防止。
-
署名付きリクエスト: クライアントとサーバーだけが知っている秘密鍵でリクエストデータに署名 or ハッシュを付与し、改ざんされていないことを検証する。HTTPだけに頼らずアプリ固有の検証を追加。
-
ノンス・タイムスタンプ検証: リクエストごとに一意のノンスや送信時刻を含め、過去の再送やリプレイを検知して拒否する 。必要ならサーバー側で使用済みノンスを保存しておく。
-
トークンなどによる設計の工夫: クライアントに生データを持たせず、サーバー発行のトークンやIDで処理を進めるデザインを検討する。特に価格や通貨種類など不正に書き換えられそうなものは隠蔽す。
-
冪等性(Idempotency)の考慮: 同じリクエストが複数回届いても副作用が一度きりになるよう工夫する。ユニークID(リクエストID)を要求し、二重処理を防ぐ 。リトライ時に重複課金・重複実行が起きないようにする。
-
ログと監視: 最後に、セキュリティ実装ではありませんが重要な習慣として、怪しいリクエストやエラーはログに記録し、定期的に監視・分析しましょう。不正の兆候を早期に発見できます。
チェックリストは以上です。
「サーバー側でもう一手間かけるだけ」で得られる安心感は大きいので、ぜひ抜け漏れがないか確認してみてください。
## おわりに
ゲーム開発におけるセキュアなサーバーサイド設計のポイントを、駆け足で紹介してきました。最初は少しやることが多いように感じるかもしれません。しかし、一度仕組みを整えてしまえば、あとは開発中も運用中も「サーバーが堅牢な守護神」として働いてくれます。
不正行為を完全になくすことは難しいですが、紹介した原則やベストプラクティスを守れば
大多数のスクリプトキディ(いたずらハッカー)達を跳ね返すことができます。公平なゲーム体験を提供することは、ユーザーの信頼とゲームの寿命を延ばすことにも繋がります。
あなたも、サーバー側のことが少しイメージできてきたのではないでしょうか?クライアントの実装だけでなく、ぜひサーバー目線でもゲームの挙動を考えてみてください。最後にもう一度、「Never trust the client!(クライアントは絶対信用するな!)」この言葉を胸に刻み、今日からあなたのゲームをチートから守っていきましょ。
それでは、健全で楽しいゲーム開発を!不正プレイヤーに負けない強固なサーバーを構築できることを祈っています💪✨
参考文献・情報源
- Stack Overflow:
“How to prevent cheating in our (multiplayer) games? (anti cheat - How to prevent cheating in our (multiplayer) games? - Stack Overflow)7】
- リブロワークス社 内ブログ: 「スマホゲームアプリの知識 — チートを防げる (
〖社員ブログ〗知っておくとゲームが楽しくなるかもしれない? スマホゲームアプリの知識 – リブロワークス – LibroWorks) (
〖社員ブログ〗知っておくとゲームが楽しくなるかもしれない? スマホゲームアプリの知識 – リブロワークス – LibroWorks)0】
- DeNA Engineering Blog:
「ゲーム開発で作りがちなチートの穴 (DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering) (DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering) (DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering) (DeNAでのセキュリティチェックから分かるゲーム開発で作りがちなチートの穴 | BLOG - DeNA Engineering)0】
- Qiita: 「ゲームでよくされるチート手法とその対策 (
ゲームでよくされるチート手法とその対策 〜アプリケーションハッキング編〜 #iOS - Qiita) (
ゲームでよくされるチート手法とその対策 〜アプリケーションハッキング編〜 #iOS - Qiita)3】
- Note:
「ネットワークゲームとチートについて (ネットワークゲームとチートについて|黒河優介) (ネットワークゲームとチートについて|黒河優介) (ネットワークゲームとチートについて|黒河優介)9】
- KARTE Developer Portal: 「WebhookにおけるHMAC署名 (
HMAC認証)4】
- Medium: *“Idempotency Keys: How PayPal and Stripe Prevent Duplicate Payment (
Preventing Duplicate Payments with Idempotency Keys by Stripe, PayPal and Adyen | Medium) (
Preventing Duplicate Payments with Idempotency Keys by Stripe, PayPal and Adyen | Medium)3】