Rosling Data resolver

One address that tells your wallet or application where to find account icons, banners and Discord verification on Keeta.

Resolver account

Use it as a resolver root in the SDK, or as a third-party resolver in a wallet.

What it lists

A resolver is an account whose metadata names services by type and provider ID. This one lists two.

Service typeProvider IDWhat it does
storagestorage-anchorStores one icon and one banner per account and serves them in display sizes.
kycdiscord-kycLinks 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}
WidthsWithout a widthShape
Icon64, 128, 256, 512256Square
Banner600, 1200, 24001200Landscape, 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 allowed

Limits

There are two tiers. An account is verified when it holds a certificate from an accepted issuer (step 4); every other account is basic.

BasicVerified
Iconup to 256 px, 128 KBup to 512 px, 512 KB
Bannernot availableup to 2400 × 1600 px, 2 MB
Replacing the same imageonce per houronce 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/tiers reports verified.

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.

FieldWhat it gives you
x-image
operation
The address of an account's icon or banner. Takes account, kind and width.
x-account-tier
operation
The address that reports an account's tier, limits and next upload time. Takes account.
x-assets
object
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" }]
  ]
}
ValueMeaning
tiers.basic, tiers.verifiedWhat 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.
maxBytesLargest file, as uploaded.
maxWidth, maxHeight
minWidth, minHeight
Largest and smallest image, in pixels.
minAspect, maxAspectAllowed shape, as width divided by height. An icon between 0.95 and 1.05 is square, give or take a few pixels.
cooldownSecondsHow long an account waits before it can replace the same image again. Icon and banner are counted apart.
displayWidthsThe widths you can ask x-image for. Any other width is refused with 400.
defaultDisplayWidthThe width you get from an address without one.
verifiedIssuerDNsThe 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.

ValueMeaning
tierbasic or verified.
tierCertainfalse 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.
limitsThe icon and banner limits of this account's tier, in the same form as in x-assets.
cooldownSecondsThe waiting time of this account's tier.
nextUploadAllowedAtPer 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. acceptedIssuers lists 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/tiers has 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.