보상 광고 설정

목차

왜 사전 동의 삽입 광고를 사용하나요?

Vungle은 우수한 UX를 유지하면서 수익을 극대화하기 위해 사전 동의 삽입 광고를 사용할 것을 권장합니다. 앱의 휴식 시간에 표시됩니다. 삽입 광고를 선택하면 수익을 높일 수 있으며, 특히 건너뛸 수 없게 만든 경우 수익이 극대화됩니다. 또한 이 광고위치에서는 사용자가 비디오 시청을 선택하면 가상 화폐, 프리미엄 콘텐츠 또는 인앱 상품 등 가치 있는 보상을 받기 때문에 뛰어난 UX도 제공합니다. 비디오 시청을 완료한 사용자에게 제공하는 보상 금액과 유형은 전적으로 귀하가 선택할 수 있습니다. 인앱 보상 또는 서버 간 콜백으로 이를 선택할 수 있습니다.

추천 옵션: 인앱 보상

개요

서버 간 콜백 대신 사용할 수 있는 옵션입니다. 사용자가 광고 시청을 완료하거나 다운로드 버튼을 클릭하면 앱에서 바로 사용자에게 보상을 제공할 수 있습니다. 이 접근 방식은 구현이 매우 쉽다는 것이 장점입니다. 이 방식은 재전송 공격을 걱정할 필요가 없는 경우에 사용하는 것이 좋습니다.

Vungle은 이제 동적 템플릿 광고로 다양한 광고 포멧을 제공합니다. 비디오 재생 후에 끝내기 카드가 표시되는 기존 광고 포멧과 달리 Vungle은 비디오 재생 중에 클릭 유도 문안 (CTA) 버튼을 사용할 수 있는 템플릿을 제공합니다. 비디오 광고를 끝까지 시청한 사용자 및 버튼을 클릭한 사용자에게 보상을 제공해야 합니다.

참고: 경우에 따라 보상 광고가 인센티브화 광고로 불리기도 합니다. 두 용어는 항상 같은 종류의 광고를 나타냅니다. SDK 코드와 보고 API에서는 '인센티브'라는 용어를 사용합니다.

iOS에서 구현

Vungle 시작하기 - iOS SDK v. 5.1 이상의 "델리게이트 콜백" 섹션에서 언급된 VungleSDK 델리게이트를 구현합니다.

- (void)vungleWillCloseAdWithViewInfo:(VungleViewInfo *)info placementID:(NSString *)placementID;

vungleSDKwillCloseAdWithViewInfo 콜백이 completedView 또는 didDownload에 대한 값이 'yes'인 viewInfo 딕셔너리를 전달하는 경우, 사용자가 보상을 받습니다.

Android에서 구현

Vungle 시작하기 - iOS SDK v. 5.1 이상에서 언급된 EventListener 인터페이스를 구현합니다.

public void onAdEnd (String placementReferenceId, boolean wasSuccessfulView, boolean wasCallToActionClicked)

onAdEnd 콜백이 wasSuccessfulView 또는wasCallToActionClicked 중 하나에 관해 'true'를 반환하면 사용자가 보상을 얻습니다.

서버 간 콜백

개요

서버 간 콜백을 사용하면 광고 시청을 완료한 사용자에게 인게임 통화 또는 기타 보상으로 보상을 제공할 수 있습니다. 사용자가 광고 시청을 완료하면 Vungle's servers to your own to notify you of the user' 완료된 액션에서 콜백을 구성할 수 있습니다.

이 접근 방식은 컨트롤이 장점입니다. 이 방법을 사용하면 서버 측에서 직접 변경 및 업데이트가 가능하므로 업데이트를 푸시할 필요가 없습니다. 또한, 재전송 공격(유효한 데이터 전송을 악의적 또는 불법적으로 반복하거나 지연하는 행위)을 방지하는 보안 기능도 제공합니다.

구현

Vungle SDK v.5.x 참고 사항: SDK v.5.1부터 보상 옵션이 대시보드로 이동하여 게시자가 코드를 변경하지 않아도 옵션을 쉽게 변경할 수 있습니다. 대시보드에서 애플리케이션 단계광고위치 단계로 이동한 후, 연필 아이콘 클릭image1.png광고위치 편집으로 이동하여 광고위치 수준에서 보상 옵션을 사용 또는 사용 중지하는 체크박스를 찾습니다.

image2.jpg

Vungle SDK v.1.0 - v.4.1 참고 사항:

  • iOS의 경우, playAd 객체에서 vunglePlayAdOptionKeyIncentivized 옵션을 '예'로 설정하고 시작합니다. (이 옵션에 대한 자세한 내용 및 playAd 관련 옵션은 여기에서 확인하십시오.)

  • Android의 경우, AdConfig 객체에서 setIncentivized 옵션을 'true'로 설정하고 시작합니다. (이 옵션에 대한 자세한 내용 및 playAd 관련 옵션은 여기에서 확인하십시오.)

전체 Vungle SDK 버전 구현

사용자가 보상 광고를 80% 이상 시청하면 시청을 완료한 것으로 간주됩니다. Vungle은 다음과 같은 콜백 URL을 사용하여 서버를 ping합니다.

http://acme.com/bugzBunny/reward?uid=%user%

다음과 같은 URL을 사용할 수도 있습니다.

http://acme.com/bugzBunny/reward? amount=1&uid=%user%&txid=%txid%&digest=%digest%

다음을 사용하시기 바랍니다. etxidedigest예시는 다음과 같습니다.

http://acme.com/bugzBunny/reward? amount=1&uid=%user%&etxid=%etxid%&edigest=%edigest%.

앱의 대시보드 내 설정에서 콜백 URL을 구성합니다(아래 참조).

CallbackURL.gif

대부분의 퍼블리셔는 보안상의 이유로 %user%, plus%txid%, %digest%만 사용하지만, 다음과 같은 것을 모두 사용할 수 있습니다.

변수

설명

%user%

다음을 통해 Vungle SDK에 제공한 사용자 이름:

●      iOS: playAd()로 전달된 옵션 딕셔너리VunglePlayAdOptionKeyUser

●      Android: playAd()로 전달된 전역 광고 구성 객체setIncentivizedUserId 설정자

%udid%

기종 고유 식별자

%ifa%

●      iOS: Apple의 기종 고유 식별자

●      Android: Google Advertiser ID가 반환됩니다.

%etxid%

시청 완료 시 서버 타임 스탬프가 포함된 고유 트랜잭션 ID, transactionID:timestamp(권장).

%edigest%

Vungle에서 보낸 콜백을 확인하는 보안 토큰. 자세한 내용은 보안 섹션을 참조하십시오(권장).

%txid%

시청 완료된 건에 대한 고유 트랜잭션 ID

%digest%

콜백이 Vungle에서 왔는지 확인하는 보안 토큰. 자세한 내용은 보안 섹션을 참조하십시오.

변수 중에서 %user%만 전달하면 되고, 나머지는 콜백 URL에 포함하면 Vungle 서버에서 반환됩니다.

보상 구성

이제 광고를 시청한 사용자에게 보상을 제공할 수 있습니다. 보상으로 무엇을 주시겠습니까? 간단한 방법을 원하시면 매번 보석을 줄 수 있습니다. 그보다 발전된 방법을 원하십니까? Vungle에는 내장된 보상 구성 옵션이 없지만, 다음을 추천해드립니다.

예: 코인 vs. 생명

앱이 여러 장소(게임 오버 3회마다 한 번, 상점)에 보상 광고를 갖고 있으며 상점에서는 코인으로, 게임 오버에는 생명으로 플레이어에게 보상을 하려고 합니다. 매번 playAd()가 발생하면 사용자를 다음과 같이 구성합니다.

userName123:coins or userName123:lives 

그런 다음 서버가 Vungle의 콜백을 받으면 %user% 구문을 분석해서 올바른 보상을 합니다.

보안

콜백 인증

Vungle에서 발신한 콜백을 확인하려면 애플리케이션의 보안 콜백 보안 키 확인란을 선택하십시오. 다음과 같은 비밀 키가 생성됩니다.

4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=

키를 사용하여 다음과 같이 콜백의 출처를 확인할 수 있습니다.

  1. 다음과 같이 콜론으로 구분된 비밀 키를 트랜잭션 ID와 연결하여 원시 트랜잭션 확인 문자열을 생성합니다.
    transactionString = secretKey + ":" + %txid% or %etxid%
  2. SHA-256 알고리즘을 사용하여 transactionString의 바이트를 두 번 해시합니다.

  3. 순차적인 SHA-256 해싱의 출력 바이트를 16진수로 인코딩하여 트랜잭션 확인 토큰을 생성합니다. 다음과 같이 표시됩니다.
    transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. 생성된 transactionToken%digest% 콜백 쿼리 문자열에서 전송된 것과 동일한지 확인하거나 %edigest%

재전송 공격 방지

서버에 대해 단일 콜백이 여러 번 재전송되는 것을 막으려면 인증된 트랜잭션 ID를 저장하고, 중복 트랜잭션 ID로 향후 콜백을 거부합니다. 트랜잭션 ID에는 타임 스탬프가 포함되어 있으므로 저장해야 하는 트랜잭션 ID의 수를 제한할 수 있고, 다음과 같이 적시 콜백에 대한 차단을 적용하여 중복을 검사할 수 있습니다.

  1. 트랜잭션 ID에서 다음과 같이 타임 스탬프(밀리 초 기준)를 추출합니다.
    transactionMillis = transactionId.substringAfter(":")
  2. transactionMillis가 귀하의 컷오프보다 늦었는지 확인하고, 컷오프 이후 transactionId가 발생하지 않았는지도 확인하십시오.

샘플 코드

이 샘플 코드는 서버 간 콜백 보안 구현에 유용하게 사용할 수 있습니다. Node.js, Java, Python 및 Ruby에 대한 예제가 있습니다. 참고 사항:

  • 해당 transaction_id%txid% 또는 %etxid% 일 수 있습니다(권장).
  • 또한 %etxid%확인 해시가 %edigest%.

Node.js

var crypto = require('crypto');

function isTransactionValid(secret, transaction_id, provided_hash) {
  return isTransactionRecent(transaction_id) &&
         isTransactionNew(transaction_id)    &&
         createSecurityDigest(secret, transaction_id) === provided_hash;
}

function getTransactionTimestamp(transaction_id) {
  return parseInt(transaction_id.split(":")[1], 10) || null;
}

function isTransactionRecent(transaction_id) {
  // Does the transaction have a reasonable format?
  var tx_timestamp = getTransactionTimestamp(transaction_id);
  if (tx_timestamp === null) { return false; }

  // Is the transaction within a reasonable time range?
  var now = new Date().getTime();
  var time_diff = now - tx_timestamp;
  var hour_in_future = -1000 * 60 * 60, three_days_ago = 1000 * 60 * 60 * 24 * 3;
  return (time_diff < three_days_ago && time_diff > hour_in_future);
}

// Have we seen this transaction before?
// NOTE: To scale beyond just this one node process, you'll need to put this in some
// sort of centralized datastore. Any one that supports atomic insertions into a set
// should do. The Redis database, with its SADD command, would be a good place to start.
var known_transactions = {};
function isTransactionNew(transaction_id) {

// For %etxid%, extract unique event id from etxid, which is the string before “:”, and use it to identify unique ad events
if (known_transactions[transaction_id]) { return false; } known_transactions[transaction_id] = true; return true; } function createSecurityDigest(secret, transaction_id) { var firsthash = crypto.createHash("sha256").update(secret + ":" + transaction_id).digest("binary"); return crypto.createHash("sha256").update(firsthash,"binary").digest("hex"); }

Java

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;

public class ServerCallbackSecurityExample {
  private static final long MAX_CALLBACK_AGE_MILLIS = 7 * 24 * 60 * 60 * 1000;  // 7 days

  /**
   * Checks that a transaction is recent enough, signed with the secretKey, and not a duplicate.
   * 
   * @param transactionId the transaction ID.
   * @param secretKey a shared secret.
   * @param verificationDigest the verification digest sent in the callback querystring.
   */
  public boolean isValidTransaction(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return isRecentTransaction(transactionId)
      && isDigestValid(transactionId, secretKey, verificationDigest)
      && isNewTransaction(transactionId);
  }

  protected boolean isRecentTransaction(String transactionId) {
    boolean isRecent = false;
    try {
      final long minCallbackAgeMillis = System.currentTimeMillis() - MAX_CALLBACK_AGE_MILLIS;
      final long transactionMillis = getTransactionMillis(transactionId);
      isRecent = (transactionMillis > minCallbackAgeMillis);
    }
    catch (ParseException exception) {
      // invalid transaction ID format
    }
    return isRecent;
  }

  protected boolean isDigestValid(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return createSecurityDigest(transactionId, secretKey)
      .equals(verificationDigest);
  }

  protected boolean isNewTransaction(String transactionId) {
    // TODO if transactionId is new, store the transactionId with its associated transactionMillis
// For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events
return true; } protected long getTransactionMillis(String transactionId) throws ParseException { final int transactionMillisIndex = transactionId.lastIndexOf(":") + 1; try { if (transactionMillisIndex > 0 && transactionMillisIndex < transactionId.length()) { return Long.parseLong( transactionId.substring(transactionMillisIndex)); } else { throw new ParseException("No timestamp in transaction ID", transactionMillisIndex); } } catch (NumberFormatException exception) { throw new ParseException("Invalid transaction ID timestamp", transactionMillisIndex); // invalid timestamp } } protected String createSecurityDigest(String transactionId, String secretKey) throws NoSuchAlgorithmException { final String verificationString = secretKey + ":" + transactionId; final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return toHexString( messageDigest.digest( messageDigest.digest( verificationString.getBytes(Charset.forName("US-ASCII"))))); } protected String toHexString(byte[] bytes) { final StringBuffer hexStringBuffer = new StringBuffer(); for (final byte byt : bytes) { hexStringBuffer.append( Integer.toString((byt & 0xff) + 0x100, 16) .substring(1)); } return hexStringBuffer.toString(); } }

Python

import hashlib, time

def isTransactionValid(secret, transaction_id, input_hash):
  """Returns whether this transaction id / hash should be considered valid"""
  return isTransactionIDRecent(transaction_id) and \
         isTransactionIDNew(transaction_id)    and \
         createSecurityDigest(secret, transaction_id) == input_hash

def getTransactionTimestamp(transaction_id):
  """Will return the unix time (in milliseconds) of this transaction, or None if invalid"""
  parsed = transaction_id.split(":")
  try:
    return int(parsed[1]) if len(parsed) == 2 else None
  except ValueError as e:
    return None

def isTransactionIDRecent(transaction_id):
  """Is this transaction within a reasonable time range?"""
  tx_time = getTransactionTimestamp(transaction_id)

  # Handle bad transaction:
  if tx_time is None:
    return False

  # Handle bad transaction times:
  now = int(time.time() * 1000)
  three_days_ago = now - (1000 * 60 * 60 * 24 * 3)
  one_hour_from_now = now + (1000 * 60 * 60)
  return ( three_days_ago < tx_time < one_hour_from_now )

def isTransactionIDNew(transaction_id, known_transaction_ids=set()):
  """Is this a duplicate transaction?
     NOTE: We only use the Python set for simplicity. For better / more centralized solutions,
     you can use any datastore that supports atomic insertion into a set. For starters, try
     Redis with its "SADD" command."""

"""For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events"""
if transaction_id in known_transaction_ids: return False # Else, valid: known_transaction_ids.add(transaction_id) return True def createSecurityDigest(secret, transaction_id): """Will return the string that the security hash should have been""" firsthash = hashlib.sha256() firsthash.update(secret + ":" + transaction_id) secondhash = hashlib.sha256() secondhash.update(firsthash.digest()) return secondhash.hexdigest()

Ruby

require "openssl"
require "digest/sha2"
require "base64"
require "time"

# just some helper methods, ignore if using Rails
class Fixnum
  SECONDS_IN_DAY = 24 * 60 * 60
  HOURS_IN_DAY = 24 * 60

  def days
    self * SECONDS_IN_DAY
  end

  def hour
    self * 60 * 60
  end

  def ago
    (Time.now - self)
  end

  def from_now
    (Time.now + self)
  end
end

def transaction_valid?(secret, txid, input_hash)
  transaction_id_recent?(txid) && transaction_id_new?(txid) && create_security_digest(secret, txid) == input_hash
end

def transaction_timestamp(txid)
  arr = txid.split(":")
  return arr[1].to_i if arr.size == 2
end

def transaction_id_recent?(txid)
  tx_time = transaction_timestamp(txid)
  return false if tx_time.nil?

  now = Time.now.to_i
  three_days_ago = 3.days.ago
  one_hour_from_now = 1.hour.from_now
  three_days_ago.to_i < tx_time && tx_time < one_hour_from_now.to_i
end

# For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events def transaction_id_new?(txid, transactions = []) return false if transactions.include?(txid) transactions << txid return true end def create_security_digest(secret, txid) verification_string = "#{secret}:#{txid}" first_digest = Digest::SHA2.new(256).update(verification_string) Digest::SHA2.new(256).hexdigest(first_digest.digest) end
또 다른 질문이 있으십니까? 문의 등록

댓글