Rosling Data resolver
One address that tells your wallet or application where to find account icons, banners and Discord verification on Keeta.
Resolver account
Mainnet is not live yet. The mainnet address will be published on this page; until then, build against testnet.
What it lists
A resolver is an account whose metadata names services by type and provider ID. This one lists two.
| Service type | Provider ID | What it does |
|---|---|---|
storage | storage-anchor | Stores one icon and one banner per account and serves them in display sizes. |
kyc | discord-kyc | Links a Discord account to a Keeta account with an on-chain certificate. Verified accounts get a banner and larger images. |
Look up an account
Shows what the storage anchor serves for an account right now. The example is a test account.
1Add the resolver
Give the SDK two roots: this resolver first, the network's own root second. Lookups then return the providers of both, so everything else on Keeta keeps working.
import { KeetaNet, lib } from '@keetanetwork/anchor';
const ROSLING_DATA = '';
const userClient = KeetaNet.UserClient.fromNetwork('test', account);
const networkRoot = KeetaNet.lib.Account.generateNetworkAddress(
KeetaNet.Client.Config.getDefaultConfig('test').network
);
const resolver = new lib.Resolver({
root: [KeetaNet.lib.Account.fromPublicKeyString(ROSLING_DATA), networkRoot],
client: userClient.client,
trustedCAs: []
});
// { 'storage-anchor': …, plus the network's own providers }
const storageProviders = await resolver.lookup('storage', {});Pass this resolver to any SDK client. The steps below use it.
2Show icons and banners
Images are public and need no key or signature. Ask for the width you are going to display.
/img/{account}/icon/{width}
/img/{account}/banner/{width}| Widths | Without a width | Shape | |
|---|---|---|---|
| Icon | 64, 128, 256, 512 | 256 | Square |
| Banner | 600, 1200, 2400 | 1200 | Landscape, 1.5:1 up to 6:1 |
- Every image is WebP. You get the smallest stored size that is at least as wide as you asked for, and never one larger than the upload.
- An account without an image answers
404. Show your own placeholder. - A new or replaced image is visible everywhere within five minutes.
<img
src="/img/${account}/icon/128"
srcset="/img/${account}/icon/256 2x"
width="64" height="64" alt=""
onerror="this.replaceWith(placeholder())">Prefer not to hard-code the host? The URL pattern is published with the provider, as the operation x-image (one of the x- fields):
import KeetaStorageAnchorClient from '@keetanetwork/anchor/services/storage/client.js';
const storage = new KeetaStorageAnchorClient(userClient, { resolver });
const provider = await storage.getProviderByID('storage-anchor');
const image = await provider.serviceInfo.operations['x-image'];
const url = image.url({ account: publicKey, kind: 'icon', width: '128' });3Upload an image
An account uploads its own icon and banner, signed with its own key. Nobody else can write to it, and each upload replaces the previous image.
const publicKey = account.publicKeyString.get();
const session = provider.beginSession({
account,
workingDirectory: `/user/${publicKey}/`,
defaultVisibility: 'public'
});
await session.put('icon', bytes, { mimeType: 'image/png' }); // or 'banner'
await session.delete('icon'); // always allowedLimits
There are two tiers. An account is verified when it holds a certificate from an accepted issuer (step 4); every other account is basic.
| Basic | Verified | |
|---|---|---|
| Icon | up to 256 px, 128 KB | up to 512 px, 512 KB |
| Banner | not available | up to 2400 × 1600 px, 2 MB |
| Replacing the same image | once per hour | once per minute |
Shown from this page. The current values are published by the provider.
- PNG, JPEG or WebP. Animated images are refused.
- Photos that carry a GPS position are refused, because everything stored here is public. Re-encoding the image in your app removes it.
- Fit the image before you upload: crop to shape, scale down to the limit, re-encode. Users should never have to open an image editor.
Ask before you upload
One request tells you the account's tier, its limits and when it may upload again. Use it to disable the button instead of letting an upload fail.
GET /api/tiers/{account}
{
"tier": "basic",
"limits": { "icon": { "maxWidth": 256, "maxBytes": 131072, … }, "banner": null },
"cooldownSeconds": 3600,
"nextUploadAllowedAt": { "icon": "2026-11-11T00:33:13.977Z", "banner": null }
}The same limits for both tiers are in the provider's metadata under x-assets, together with the accepted issuers. The x- fields describes every value.
4Verify an account
Verification links a Discord account to a Keeta account. The user signs in with Discord, receives a certificate, and puts it on their own account. From then on the storage anchor treats the account as verified.
import { KYC } from '@keetanetwork/anchor';
const kyc = new KYC.Client(userClient, { resolver });
const providers = await kyc.createVerification({ countryCodes: ['NL'], account });
const discord = providers.find((provider) => provider.id === 'discord-kyc');
const verification = await discord.startVerification();
window.open(verification.webURL); // the user signs in with Discord
// Poll until the certificate is ready
let response = await verification.getCertificates();
while (!response.ok) {
await new Promise((resolve) => setTimeout(resolve, response.retryAfter));
response = await verification.getCertificates();
}
// Put it on the user's account
const { certificate, intermediates } = response.results[0];
await userClient.modifyCertificate(
KeetaNet.lib.Block.AdjustMethod.ADD,
certificate,
intermediates ? new KeetaNet.lib.Utils.Certificate.CertificateBundle([...intermediates]) : null
);- Any country code works; the provider is not tied to a country.
- The certificate is valid for one year. The Discord ID is stored in it encrypted, readable only by the account's owner; nothing personal is visible on the ledger.
- After the certificate is on the account, allow up to 30 seconds before
/api/tiersreportsverified.
This proves control of a Discord account. It is not a legal identity check, and should not be presented to users as one.
In a wallet
Wallets that support third-party resolvers can use this one without any code. Add the resolver account under the wallet's advanced settings, allow KYC operations, then add a certificate and choose discord-kyc.
The x- fields
The SDK defines what a storage provider publishes about itself: operations such as put and get, and its quotas. Icons, banners and tiers are not part of that, so this provider adds three fields of its own. Their names start with x- to mark them as additions.
| Field | What it gives you |
|---|---|
x-imageoperation | The address of an account's icon or banner. Takes account, kind and width. |
x-account-tieroperation | The address that reports an account's tier, limits and next upload time. Takes account. |
x-assetsobject | The rules for every account: limits per tier, the widths that are served, and who can verify an account. |
- The SDK carries these fields along without reading them. Nothing in the SDK changes its behaviour because of them.
- Other storage providers do not have them. If your resolver also returns the network's own providers, check that a field exists before you use it.
- New fields can be added over time. Ignore the ones you do not know.
Read them through the SDK
The two operations sit next to the standard ones and resolve the same way: await the operation, then fill in its address.
const provider = await storage.getProviderByID('storage-anchor');
const operations = provider.serviceInfo.operations;
const image = await operations['x-image'];
const tier = await operations['x-account-tier'];
if (image === undefined || tier === undefined) {
// A provider without account images
}
image.url({ account: publicKey, kind: 'banner', width: '1200' });
const info = await (await fetch(tier.url({ account: publicKey }))).json();Give every value as a string, and always give all of them. A value you leave out stays in the address as {width}.
In TypeScript the SDK only lists the standard operations, so cast once:
type Address = { url(values: Record<string, string>): URL };
const operations = provider.serviceInfo.operations as
Record<string, Promise<Address> | undefined>;x-assets is not on serviceInfo. Read it from the provider's entry in the resolver:
const found = await resolver.lookup('storage', {}, {
providerIDs: ['storage-anchor']
});
const entry = await lib.Resolver.Metadata.fullyResolveValuizable(
found['storage-anchor']
);
const assets = entry['x-assets'];Not using the SDK? The same entry is served as JSON at /serviceMetadata.
What x-assets holds
"x-assets": {
"tiers": {
"basic": {
"icon": {
"maxBytes": 131072,
"maxWidth": 256, "maxHeight": 256,
"minWidth": 64, "minHeight": 64,
"minAspect": 0.95, "maxAspect": 1.05
},
"banner": null,
"cooldownSeconds": 3600
},
"verified": { "icon": { … }, "banner": { … }, "cooldownSeconds": 60 }
},
"displayWidths": { "icon": [64, 128, 256, 512], "banner": [600, 1200, 2400] },
"defaultDisplayWidth": { "icon": 256, "banner": 1200 },
"verifiedIssuerDNs": [
[{ "name": "commonName", "value": "Rosling Data Discord KYC" }]
]
}| Value | Meaning |
|---|---|
tiers.basic, tiers.verified | What an account in that tier may upload. icon and banner each hold the limits below; null means the tier cannot upload that image at all. |
maxBytes | Largest file, as uploaded. |
maxWidth, maxHeightminWidth, minHeight | Largest and smallest image, in pixels. |
minAspect, maxAspect | Allowed shape, as width divided by height. An icon between 0.95 and 1.05 is square, give or take a few pixels. |
cooldownSeconds | How long an account waits before it can replace the same image again. Icon and banner are counted apart. |
displayWidths | The widths you can ask x-image for. Any other width is refused with 400. |
defaultDisplayWidth | The width you get from an address without one. |
verifiedIssuerDNs | The certificate issuers that make an account verified, each as a list of name parts. Use it to tell users which verification counts here. |
Read these values instead of copying them into your app. Crop and scale to tiers, build srcset from displayWidths, and your app follows along when a limit changes.
What x-account-tier answers
x-assets describes the rules for everyone. This operation applies them to one account.
| Value | Meaning |
|---|---|
tier | basic or verified. |
tierCertain | false when the account's certificates could not be checked just now. The basic you see is then a fallback, not a verdict: do not tell the user they are unverified, ask again in a moment. |
limits | The icon and banner limits of this account's tier, in the same form as in x-assets. |
cooldownSeconds | The waiting time of this account's tier. |
nextUploadAllowedAt | Per image, the time from which the account can upload again. null means now. |
Errors
Errors are the SDK's typed errors. Each carries a message written for the user, so you can show error.message as it is.
- KeetaAnchorCertificateRequiredError
- The upload needs a verified account: a banner, or an image above the basic limits.
acceptedIssuerslists who can verify the account. Offer step 4. - KeetaStorageAnchorServiceUnavailableError, not retryable
- The account replaced this image too recently. The message says how long to wait;
/api/tiershas the exact time. - KeetaStorageAnchorServiceUnavailableError, retryable
- Too many requests, or a short outage. Wait a few seconds and try again; the SDK's retry helpers do this for you.
- KeetaStorageAnchorValidationFailedError
- The image breaks a rule no tier allows: too many pixels, the wrong shape, animated, carrying a GPS position, or not an image at all. The message names the rule and the limit.
- KeetaStorageAnchorQuotaExceededError
- The upload is far larger than anything that is stored. Scale the image down first.