Check images
Check member photos and X pictures: match your campaign picture, review with AI, read screenshots, and catch reused photos.
Many quests ask members for a picture. Domino can check it for you, so your team only looks at the unclear ones. For example:
- "Use our campaign picture as your X avatar." Domino compares the member's X profile picture with yours.
- "Upload a selfie at our booth." Domino spots the same photo submitted by several people, then AI checks it against your criteria.
- "Show that you joined our X Community." AI reads the screenshot and Domino compares what it shows with the member's linked X account.
The member gets an answer: accepted, not accepted with a reason, or sent to your team for review when the quest uses review. Images are never served publicly, and the AI reviewer receives copies without location data.
To start from a working quest, install a recipe:
| Recipe | Name in the URL | Checks |
|---|---|---|
| X profile picture | x-profile-picture | The linked X avatar is your campaign picture, or a customized version AI accepts |
| Unique photo | unique-photo | An uploaded photo was not already submitted by someone else, then AI review |
| X Community screenshot | community-screenshot | A screenshot shows your Community, membership and the linked X username |
pnpm dlx shadcn@4.21.0 add https://snippets.domino.run/r/x-profile-picture.jsonEach recipe installs quests/<name>.quest.ts and instructions under docs/snippets/. See start from a snippet.
| To | Use |
|---|---|
| Require your campaign picture as an X avatar | compare against a reference image |
| Accept a customized version, such as a framed selfie | review with the reference as a second image |
| Read a screenshot, such as a joined X Community | extract |
| Catch the same photo submitted by several people | seen |
| Get dimensions, format and hashes | inspect |
Name an image
Every method takes image references, not files or URLs:
| Reference | Image |
|---|---|
{ evidence: ctx.input.photo } | The photo the member uploaded with this attempt. Another attempt's upload is refused. |
{ x: "profile" } | The linked X account's current profile picture, at 400×400. The member must link X first. |
{ x: "post", postId, index } | The image attached to an X post, where index is its position: 0 (default) to 3. Videos and GIFs use their preview image. |
{ asset: "key" } | A reference image you deployed with the project. |
X images are downloaded only from X's image hosts. X post reads also list attached media in post.media, as { type, url } in post order.
Add reference images
Add reference images to relay.json
List each image with a key and a path relative to relay.json:
{
"images": [
{ "key": "campaign-avatar", "path": "assets/campaign-avatar.png" }
]
}Use JPEG, PNG or WebP files of at most 5 MiB and 25 megapixels, up to 10 per deployment. Commit them with your project.
Deploy
Run domino deploy. The CLI uploads each new file straight to private storage, then previews the deployment with only each file's hash and size; unchanged files are not uploaded again. The platform decodes every image before publication and rejects anything that is not a supported image. The deployment summary lists the published images. Images are never served publicly.
Hosted builds publish the same images from your repository: build them with domino build --hosting-artifacts, as the starters do, or keep domino build --json output unchanged in your build script.
Deploying a key again replaces its image for later checks. Removing an entry from relay.json does not delete the published image. Results that were already captured for an attempt stay unchanged.
Compare two images
compare returns perceptual hash distances between 0 (same picture) and 64:
import { x, defineQuest, settings, manual, once, decision } from "@domino-sdk/relay/authoring";
export default defineQuest({
integrations: [x],
key: "campaign-avatar",
title: "Use the campaign picture on X",
settings: settings.object({}),
trigger: manual(),
completion: once(),
rewards: [],
async evaluate(ctx) {
if (!(await ctx.x.connection())) {
return decision.reject({ reason: "Connect X, then start a new submission." });
}
const match = await ctx.images.compare({ x: "profile" }, { asset: "campaign-avatar" });
return match.phashDistance <= 8
? decision.accept()
: decision.reject({ reason: "Set the campaign picture as your X avatar." });
},
});| Result | Meaning |
|---|---|
identical | The two files have the same bytes. |
phashDistance | Differences in overall structure. Up to about 8 means the same picture resized or re-encoded. Unrelated photos usually differ by 25 or more. |
dhashDistance | Differences in local brightness changes. It reacts more to small edits. |
Your quest decides the threshold. Make it a setting so operators can tune it in Console without a code change:
import { x, defineQuest, settings, manual, once, decision } from "@domino-sdk/relay/authoring";
export default defineQuest({
integrations: [x],
key: "campaign-avatar-tunable",
title: "Use the campaign picture on X",
settings: settings.object({
maxDistance: settings.integer({
label: "Largest hash distance that counts as the same picture (0–64)",
default: 8,
min: 0,
max: 64,
}),
}),
trigger: manual(),
completion: once(),
rewards: [],
async evaluate(ctx) {
const match = await ctx.images.compare({ x: "profile" }, { asset: "campaign-avatar" });
return match.phashDistance <= ctx.settings.maxDistance
? decision.accept()
: decision.reject({ reason: "Set the campaign picture as your X avatar." });
},
});Hashes tolerate resizing, compression and small color changes. Crops, rotations, overlays and added frames change them substantially. Use AI review when a member may customize the picture.
Review images with AI
review judges up to four images against your criteria and returns pass, fail or unclear with a short reason. Images are numbered in the order you pass them, so the criteria can refer to them:
import { decision, type EvaluationContext } from "@domino-sdk/relay/authoring";
// Call from evaluate() after checking that X is connected.
export async function framedAvatar(ctx: EvaluationContext<unknown, unknown>) {
const result = await ctx.images.review({
images: [{ x: "profile" }, { asset: "campaign-avatar" }],
criteria: "Image 1 uses the frame and logo from image 2. The person's own photo may appear inside the frame.",
});
if (result.kind === "unclear") {
return decision.review({ reason: result.reason });
}
return result.kind === "pass" ? decision.accept() : decision.reject({ reason: result.reason });
}The AI reviewer receives re-encoded copies without EXIF or location data, never the image URL. It is told to ignore instructions inside the images, but AI can still misjudge adversarial pictures, so send unclear results to your team.
Read fields from a screenshot
extract reads up to 12 declared fields from one image. Each field is a string, number or boolean, and is null when the image does not show it clearly:
import { decision, type EvaluationContext } from "@domino-sdk/relay/authoring";
// For a quest with trigger: manual({ input: evidence.photo() }).
export async function joinedCommunity(ctx: EvaluationContext<unknown, { photo: string }>) {
const screenshot = await ctx.images.extract({
image: { evidence: ctx.input.photo },
fields: {
community: { type: "string", description: "The X Community name shown on the page" },
joined: { type: "boolean", description: "Whether the page shows the viewer as a member" },
},
});
const { community, joined } = screenshot.values;
if (community === null || joined === null) {
return decision.review({ reason: screenshot.reason });
}
return community === "Domino Builders" && joined
? decision.accept()
: decision.reject({ reason: "Upload a screenshot after joining the community." });
}A screenshot is a member's claim, not proof from X. Compare extracted values with data you can verify, such as the linked account's username, and send uncertain cases to staff.
Catch reused photos
A photo booth or a "share your setup" quest invites copying. seen records the image for this attempt and counts other members whose earlier images are within the distance:
import { decision, type EvaluationContext } from "@domino-sdk/relay/authoring";
// Call first in evaluate(); continue with your own checks when it returns null.
export async function reusedPhoto(ctx: EvaluationContext<unknown, { photo: string }>) {
const seen = await ctx.images.seen({ evidence: ctx.input.photo });
return seen.matches > 0
? decision.review({ reason: "This photo matches another member's submission." })
: null;
}The result contains matches, the closestDistance, and firstSeenAt in milliseconds. The default maxDistance is 8; pass { maxDistance } from 0 to 8 to change it, for example from a quest setting as the unique-photo recipe does. The same member's other attempts never count. Only images passed to seen are recorded, within the same project and environment. seen records the image when it runs, even if the attempt is later rejected, so two people submitting the same photo at once see each other.
Inspect an image
inspect returns width and height after EXIF rotation, format, bytes, sha256, and the 16-character phash and dhash. Use it to require a minimum size or to store hashes in your own records.
Limits and failures
| Limit | Value |
|---|---|
Images per review | 4 |
Fields per extract | 12 |
| Criteria | 2,000 characters |
| Downloaded or uploaded image | 5 MiB and 25 megapixels |
| Photo uploads by a member | 30 an hour; past it, uploads answer 429 rate-limited |
| Reference images | JPEG, PNG or WebP; 5 MiB each; 10 per deployment |
| Capability calls | Each method call uses one of the evaluation's eight calls |
Member avatar uploads are decoded by the same image service with the same 5 MiB and 25 megapixel limits, then stored as 512×512 WebP. They are profile pictures, not quest evidence; see member profiles.
A technical failure, such as an image service outage, never passes or rejects a member; when the quest code lets the error through, Domino retries it. To check a changed picture or account, the member starts a new submission.
The recipes above are complete quests built from these methods. Each one is also an SDK example in packages/sdk/examples.
Retire an image
Archive an image in Settings → Resources, or run domino images archive <key> --reason <text> in test. Fresh evaluator reads of that image name fail while it is archived. Stored bytes and image evidence already captured by an attempt remain available for replay. Publishing the image again preserves archive state. Unarchive it explicitly before new checks should capture it again.