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 when numerator >= denominator. See the “Create a Quest query” for details on the syntax (Treasure team can assist you with formulating this query if needed).
time_server (a string representation of an integer)
smart_account (or user_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.
Copy
{ // 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 telemetry. 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'. }}
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 languages. The following example is in TypeScript (assumes you have already installed kafkajs@2.2.4):
Copy
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, }, ],});
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 languages. The following example is in TypeScript (assumes you have already installed kafkajs@2.2.4):
Copy
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, }, ],});
If you choose this step, let us know and in a private channel we’ll provide
you the {DATA_PLATFORM_URL} and the X-API-Key header value to use. Make a
POST call to https:// {DATA_PLATFORM_URL}/ingress/events where the body of the quest is your event
payload JSON.
Make an HTTP POST call using your preferred method. This example uses the Axios HTTP client for Node.js.
If you choose this method, let us know and we’ll provide you the {TREASURE_AWS_ACCOUNT} in a private channel.
Create an SNS topic. Let’s say you called it events-for-treasure. Then its topicArn is going to be arn:aws:sns:{your AWS region}:{your AWS account}:events-for-treasure.
Allow Treasure’s AWS account to subscribe to your SNS topic by adding this to your SNS topic’s access policy:
The numerator query over Treasure’s data platform.
Copy
SELECT -- Count matching event rows. COUNT(*) AS numeratorFROM -- This is a table prepared just for events matching cartridge_tag/name. events.mygame_completed_matchWHERE -- Filter for events within the quest period. time_server BETWEEN {{START_DATE_MILLIS}} AND {{END_DATE_MILLIS}} -- Filter for current user. AND smart_account = '{{USER_ADDRESS}}' -- Filter for victories. AND result = 'victory'
cartridge_tag = mygame
name = player-status
denominator = 5
The numerator query over Treasure’s data platform (picks player_level from the latest game entry).
Copy
SELECT -- Cast player_level as an integer and assign as the numerator. player_level AS numeratorFROM -- This is a table prepared just for events matching cartridge_tag/name. events.mygame_player_statusWHERE -- Filter for events within the quest period. time_server BETWEEN {{START_DATE_MILLIS}} AND {{END_DATE_MILLIS}} -- Filter for current user. AND smart_account = '{{USER_ADDRESS}}'-- Pick the latest event in the quest period.ORDER BY time_server DESCLIMIT 1
cartridge_tag = mygame
name = completed-match
denominator = 100
The numerator query over Treasure’s data platform.
Copy
SELECT -- Sum the ants_defeated field over the matching event rows. SUM(ants_defeated) AS numeratorFROM -- This is a table prepared just for events matching cartridge_tag/name. events.mygame_completed_matchWHERE -- Filter for events within the quest period. time_server BETWEEN {{START_DATE_MILLIS}} AND {{END_DATE_MILLIS}} -- Filter for current user. AND smart_account = '{{USER_ADDRESS}}'
cartridge_tag = mygame
name = completed-match
denominator = 7
The numerator query over Treasure’s data platform.
Copy
SELECT COUNT(*) AS numeratorFROM events.mygame_completed_matchWHERE time_server BETWEEN {{START_DATE_MILLIS}} AND {{END_DATE_MILLIS}} AND smart_account = '{{USER_ADDRESS}}' AND ants_defeated >= 10
Using Goldsky’sMirror 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, here are the steps.
1
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.
2
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).
Construct a 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”:
Copy
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 numeratorFROM victory_count FULL JOIN stamp_count ON TRUE