Quests v2 Integration Guide
1 - Fill out Quest metadata form
In order for us to set up a Quest for a game within the Treasure Platform, the following details will need to be collected (see here for a Google Sheets template to provide quests in bulk).
- Quest Name
- Name of the associated game
- Quest Description: A non-technical definition of what a player needs to do to achieve this quest (73 character limit is preferred).
- Start and End Dates: Provide a UTC date + timestamp (e.g.,
2024-06-07T16:00:00Z
). - CTA Link: The URL that the player will need to navigate to to work on said quest
- Badge (optional): to accompany this specific quest. See the Badge Design Guide for more details.
- Quest Mechanics : To help us normalize rewards please provide approximate scores in the following categories:
- Friction Score: On a scale of 1-10 with 10 being most friction, how much friction does a player need to go through in order to complete this quest?
- Difficulty Score: On a scale of 1-10 with 10 being most difficult, how much skill does a player need to complete this quest?
- Time Required: On average, how much time (in hrs) will a player need to commit to complete this quest?
- Quest Event Schema: Tell us the schema for your event payload. See Step (2).
- Quest Denominator (integer): This is the
denominator
field described in the technical criteria step. - Quest Numerator (query): Tell us the SQL query to use to compute the
numerator
for this quest. The quest is complete whennumerator >= denominator
. See the “Create a Quest query” for details on the syntax (Treasure team can assist you with formulating this query if needed).
2 - Prepare your payload
The required fields are
cartridge_tag
name
time_server
(a string representation of an integer)smart_account
(oruser_id
)id
(for the event)properties
(a JSON object containing game-specific event data)
Recommended and optional fields are indicated in the schema below.
The other fields you add within properties
can be JSON scalar, array, or object types.
Your Event Payload schema should have the following format.
A TypeScript version of your schema.
{
// Game partner / time / player info.
cartridge_tag: string; // Required. Value is assigned to you by Treasure.
name: string; // Required. Name of this type of event. You decide this value.
time_server: string; // Required. UNIX milliseconds.
time_local?: string; // Optional. UNIX milliseconds of event at originating device.
smart_account: string; // Required. Ethereum wallet address for player. Lowercase.
user_id?: string; // Required if smart_account is undefined. Can be player ID or email, etc.
// Database upsert info.
id: string; // Required. Unique identifier for this event.
// Used as a database upsert key to filter out duplicate updates.
// Can be omitted if a unique identifier can be found elsewhere in
// the payload.
op?: string; // Optional. A field indicating whether to upsert/delete the
// database entry specified in id. Valid values:
// "upsert" / "u"
// "delete" / "d"
// In the absence of an 'op', default behavior is to upsert.
// Event-specific properties. Up to 500 KB.
properties: {
// ...
// ...
},
// Other metadata. Device and app telemtry.
session_id?: string; // Optional. Unique Session ID. Helps w/ analytics.
chain_id?: number; // Optional. Chain ID.
device: {
device_unique_id?: string; // Recommended. Unique identifier for device.
device_name?: string; // Optional. Device name.
device_model?: string; // Optional. Device model.
device_os?: string; // Optional. Device OS.
},
app: {
app_identifier: string; // Recommended. Name of backend sending this ("my_game_backend").
app_environment: number; // Recommended. 0 for dev, 1 for prod.
app_version: string; // Recommended. Build version for the backend server sending this event.
// For example, '0.1' or '1.2.1-alpha'.
}
}
Examples payloads
3 - Push game events to Treasure
From your backend, push your event payloads to our infra. You can choose from the following methods: Kafka, HTTP POST, or AWS SNS.
There is no need for you to set up your own Kafka cluster with this approach. Just send messages to a dedicated Redpanda Kafka topic that Treasure manages for you.
-
Receive Kafka client credentials (username, password, bootstrap URL) from Treasure.
-
Send your JSON payloads using a Kafka producer client. Kafka client libraries are available in a number of langauges. The following example is in TypeScript (assumes you have already installed
kafkajs@2.2.4
):
import { Kafka as KafkaJs, logLevel } from 'kafkajs';
// Create a Kafka producer client.
const kafka = new KafkaJs({
clientId: 'mygame-client-id', // You choose this.
brokers: ['KAFKA_BOOTSTRAP_URL'], // Assigned by Treasure.
logLevel: logLevel.WARN,
ssl: true,
sasl: {
mechanism: 'scram-sha-256',
username: 'KAFKA_USERNAME', // Assigned by Treasure.
password: 'KAFKA_PASSWORD', // Assigned by Treasure.
},
});
const producer = kafka.producer();
await producer.connect();
// Prepare event key, payload, optional headers.
const eventKey = 'match-id-81ac4712'; // You determine this.
const eventPayload = {
cartridge_tag: 'mygame',
name: 'completed-match',
time_server: '1717526677354',
smart_account: '0xabcde123456789012345678901234567890abcde',
id: 'match-id-81ac4712',
properties: {
completed: true,
score: '513',
result: 'victory',
ants_defeated: '8',
mosquitos_defeated: '5',
health_max: '100',
health_remaining: '90',
team_members: ['mantis', 'dragonfly', 'antlion'],
player_level: '3'
},
chain_id: 978657,
device: {
device_unique_id: '0AF433c8927-9178-C2'
},
app: {
app_identifier: 'mygame-super-server',
app_environment: 1,
app_version: '0.1.2'
}
};
// Headers are optional, but can inform us what to do with entries
// that share the same (upsert) key.
const eventHeaders: String<string, string | string[] | undefined. = {
op: 'u', // Valid values are "u" / "upsert" / "d" / "delete".
};
// Send message to Treasure Kafka.
await producer.send({
topic: 'KAFKA_TOPIC', // Assigned by Treasure.
messages: [
{
key: eventKey,
value: JSON.stringify(eventPayload),
headers: eventHeaders,
},
],
});
4 - Create a Quest query
Aside from the usual Quest metadata (name, description, optional image, start/end dates), you’ll need to tell us
- What
cartridge_tag
+name
to filter for. - The value for the quest’s
denominator
. - How to count/sum/aggregate the quest’s
numerator
from the set of filtered events.
A quest is complete when numerator >= denominator
. You can express the numerator computation as an aggregate query over the DB.
We currently support PostgreSQL syntax. The following special variables will be subbed into your SQL query:
{{START_DATE_MILLIS}}
(the start date of the quest in UNIX milliseconds){{END_DATE_MILLIS}}
(the end date of the quest in UNIX milliseconds){{START_DATE_SECONDS}}
(the start date of the quest in UNIX seconds){{END_DATE_SECONDS}}
(the end date of the quest in UNIX seconds){{USER_ADDRESS}}
(the player’s wallet address)
Appendix
Quest query examples
We provide a few simple example queries here.
Combining on-chain + off-chain quest criteria
Using Goldsky’s Mirror pipelines, Darkmatter is able to ingest on-chain events and transactions, allow us to make queries that combine web3 and web2 data.
If you’d like us to ingest your on-chain data to use for Quests v2, here are the steps.
Provide event signature
Give Treasure the event signature that you want us to index. Example: Stamp(uint256 indexed stampId, address indexed caller, uint256 nonce)
If your event is defined in a widely adopted standard (like ERC20, ERC721, etc), then please tell us which
chain
and contract_address
to listen for as well.
Table setup
Treasure will set up a table containing those events. In our example, that table would be contract_events.stamp
. Note that event_params
JSONB captures the event parameters for an emitted event Stamp(13, 0xabcde123456789012345678901234567890abcde, 69)
.
{
"chain": "ruby",
"address": "0x5167e9a422aced95c2d0b62bf05a7847a9a942b2",
"block_timestamp": 1717526700,
"block_number": 640297,
"transaction_index": 1,
"log_index": 0,
"transaction_hash": "0x53cad22fdb2283e9c35415e73a638efb3731712dd3b27ca4ed73631b8bd099ea",
"event_name": "Stamp",
"event_signature_indexed": "Stamp(uint256 indexed,address indexed,uint256)",
"event_signature": "Stamp(uint256,address,uint256)",
"event_params": {
"stampId": "13",
"caller": "0xabcde123456789012345678901234567890abcde",
"nonce": "69"
}
}
Construct SQL query
Contruct an SQL query to compute the numerator
for you quest. Example for a quest whose goal is to “win at least 1 match and produce an on-chain Stamp
with nonce 69 or 420”:
WITH victory_count AS (
SELECT
COUNT(*) AS count
FROM
events.mygame_completed_match
WHERE
time_server BETWEEN {{START_DATE_MILLIS}} AND {{END_DATE_MILLIS}}
AND smart_account = '{{USER_ADDRESS}}'
AND result = 'victory'
),
stamp_count AS (
SELECT
COUNT(*) AS count
FROM
contract_events.stamp
WHERE
block_timestamp BETWEEN {{START_DATE_SECONDS}} AND {{END_DATE_SECONDS}}
AND event_params->>'caller' = '{{USER_ADDRESS}}'
AND (event_params->>'nonce')::numeric IN (69, 420)
)
SELECT
(COALESCE(victory_count.count, 0) > 0)::int
+ (COALESCE(stamp_count.count, 0) > 0)::int AS numerator
FROM
victory_count FULL JOIN stamp_count ON TRUE