{
"id": "town.walls.photo",
"defs": {
"main": {
"key": "tid",
"type": "record",
"record": {
"type": "object",
"required": [
"createdAt"
],
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
},
"maxLength": 40,
"description": "Faceted tags. Conventions (parsed/enforced in app code, not the lexicon): kind:<piece|throwup|tag|mural|sticker|roller|wheatpaste|other> (exactly one expected), crew:<name>, surface:<wall|truck|pole|shutter|...>. Plus freeform tags (e.g. \"trackside\", \"two-color\")."
},
"image": {
"type": "blob",
"accept": [
"image/*"
],
"maxSize": 4000000,
"description": "The displayed photo. Optional in schema; exactly one of image or grainPost is required (enforced in app code)."
},
"notes": {
"type": "string",
"maxLength": 10000,
"maxGraphemes": 1000
},
"camera": {
"ref": "#camera",
"type": "ref"
},
"artists": {
"type": "array",
"items": {
"type": "string"
},
"maxLength": 20,
"description": "Artist names. Free strings; a piece may have several."
},
"caption": {
"type": "string",
"maxLength": 3000,
"maxGraphemes": 300
},
"bskyPost": {
"ref": "com.atproto.repo.strongRef",
"type": "ref",
"description": "strongRef to an app.bsky.feed.post cross-posting this sighting."
},
"location": {
"ref": "#location",
"type": "ref"
},
"longform": {
"ref": "com.atproto.repo.strongRef",
"type": "ref",
"description": "strongRef to a site.standard.document for pieces with an essay. Reserved; not wired yet."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Record creation time."
},
"grainPost": {
"ref": "com.atproto.repo.strongRef",
"type": "ref",
"description": "strongRef to a grain post serving as the photo. Alternative to image (exactly one of image/grainPost required, app-enforced)."
},
"capturedAt": {
"type": "string",
"format": "datetime",
"description": "When the photo was taken (EXIF DateTimeOriginal), if known."
},
"siteRecord": {
"ref": "com.atproto.repo.strongRef",
"type": "ref",
"description": "strongRef to the site.standard.* record the website publishes per photo for discoverability."
},
"aspectRatio": {
"ref": "#aspectRatio",
"type": "ref",
"description": "Pixel dimensions of the image, used for justified layout / to avoid reflow. Best-effort."
},
"grainCrosspost": {
"ref": "com.atproto.repo.strongRef",
"type": "ref",
"description": "strongRef to a social.grain.gallery this admin created to syndicate the sighting onto grain (gallery == grain post). Distinct from grainPost, which is an external grain post used AS the photo source. Set by the syndication step; absence means the grain leg hasn't run."
}
}
},
"description": "A documented photo of a graffiti piece."
},
"camera": {
"type": "object",
"properties": {
"iso": {
"type": "integer"
},
"lens": {
"type": "string"
},
"make": {
"type": "string"
},
"model": {
"type": "string"
},
"shutter": {
"type": "string"
},
"aperture": {
"type": "string"
},
"focalLength": {
"type": "string"
}
},
"description": "Best-effort capture metadata from EXIF or a grain record."
},
"location": {
"type": "object",
"properties": {
"lat": {
"type": "string",
"description": "Decimal degrees, full precision. e.g. \"37.77531\"."
},
"lng": {
"type": "string",
"description": "Decimal degrees, full precision. e.g. \"-122.22417\"."
},
"name": {
"type": "string",
"description": "Optional human label, e.g. \"E 12th St underpass\"."
}
},
"description": "Capture location. lat/lng are decimal-degree STRINGS: ATProto lexicons have no float type (only integer). NOTE: coordinates are stored at full precision here. The PUBLIC website coarsens them at render time via geo.coarsen, but the record itself is public via listRecords — exact coords are readable by anyone reading the repo directly."
},
"aspectRatio": {
"type": "object",
"required": [
"width",
"height"
],
"properties": {
"width": {
"type": "integer",
"minimum": 1
},
"height": {
"type": "integer",
"minimum": 1
}
},
"description": "Image pixel dimensions. width/height are integers (e.g. for a 6000x4000 photo)."
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}