· Tutorial: publishing a Compute-to-Data algorithm
Step-by-step tutorial

From Docker image to the portal: publishing a Compute-to-Data algorithm

You've already written and locally tested the algorithm. This guide covers the other 50%: building the image with buildx, pushing it to a registry, pinning its checksum, and publishing it on the Pontus-X portal with pontus-x_cli — including the link to the dataset that will be able to run it.

docker buildx · multi-arch Own registry or Docker Hub pontus-x_cli + Nautilus Real example: clip-raster

How to navigate
Scroll horizontally, use the arrow keys or the buttons below. There are 14 slides.

02 / 14
Overview

From source code to an executable asset

Publishing a C2D algorithm is a chain of steps where each one feeds the next. The critical link is the checksum: it ties the physical image to its on-chain description.

buildx
Build
Multi-architecture image from the Dockerfile.
--push
Push
To the registry (own or Docker Hub).
sha256:…
Pin digest
The immutable checksum of the image.
index.ts
Describe
Publishing script with metadata.
publish
Publish
On-chain asset via pontus-x_cli.
trust
Link
Authorize the algo on a dataset.

Key idea
The portal does not store your code: it stores a reference to the image + its checksum. The C2D node downloads that exact image and runs it in isolation over the dataset.

03 / 14
Before you start

What you need at hand

🐳 Docker + buildx

Recent Docker (buildx is included). Check with docker buildx version.

📦 A registry account

The own registry (registry.agrospai.udl.cat) or Docker Hub. With push permissions.

🟢 Node.js + npm

To install and run pontus-x_cli and build the scripts with Nautilus.

🔑 A wallet

A .json key file (exportable with export-private-key) to sign the on-chain publication.

🧩 The algorithm repo

With its Dockerfile already tested locally (see the algorithm build guide).

💡 Your configs repo

Your own repo where you keep your publishing scripts. AgrospAI uses a private one (agrospai-configs); create yours with the same structure.

Install the CLI
Once: npm install -g pontus-x_cli  —  or use it from the configs repo, which already lists it as a dependency.

04 / 14
Architectures

Why the image architecture matters

The algorithm doesn't run on your laptop, but on the C2D provider's compute node. If the image architecture doesn't match the node's, the job fails to start (exec format error).

  • Most C2D nodes are linux/amd64 — the bare minimum.
  • If you build from an Apple Silicon Mac, your native build is arm64: it won't work by default.
  • A multi-architecture image packs several and the node picks its own.

Rule of thumb
If you don't know where it will run, build multi-arch (linux/amd64,linux/arm64). If you do, just target that platform.

🎯 A single platform

Faster, lighter build. Ideal when you know the target node (almost always amd64).

🌐 Multi-architecture

A single tag serves amd64 and arm64 nodes. The registry stores a manifest index pointing to each variant.

⚙️ Emulation (QEMU)

buildx can build amd64 from an arm Mac via emulation — slower, but without needing another machine.

05 / 14
Step 1 · Build

Build and push with docker buildx

Unlike docker build, buildx builds and pushes in a single step with --push, and supports several platforms at once with --platform.

  • Create a builder once: docker buildx create --use.
  • --platform defines the architecture(s).
  • --push pushes the result straight to the registry.
  • The tag rules: registry/namespace/name:tag.

Multi-arch needs --push
A multi-platform image can't be loaded into the local Docker (--load); it must go to the registry, which stores the manifest index.

terminal
# One-time: create and activate a buildx builder
docker buildx create --name agrospai --use
docker buildx inspect --bootstrap

# --- Option A · single architecture (the node's) ---
docker buildx build \
  --platform linux/amd64 \
  -t registry.agrospai.udl.cat/library/clip-raster:latest \
  --push .

# --- Option B · multi-architecture (portable) ---
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.agrospai.udl.cat/library/clip-raster:latest \
  --push .

The trailing . is the build context (the repo root, where the Dockerfile is).

06 / 14
Step 2 · The registry

Where the image lives: own registry or Docker Hub

The C2D node must be able to download your image. That means the registry has to be accessible (public, or with credentials the node knows).

  • Own registryregistry.agrospai.udl.cat/library/<name>. Full control.
  • Docker Hubdocker.io/<user>/<name>. The namespace is your username.
  • Authenticate with docker login before doing --push.

Important
The image in the publishing script must be written exactly like the tag you pushed with: same host, same namespace, same name.

terminal
# Own registry
docker login registry.agrospai.udl.cat
#   Username: ...
#   Password: ...

# Docker Hub
docker login            # docker.io by default

# After login, the same build with --push uploads it.
# Naming by destination:
#   registry.agrospai.udl.cat/library/clip-raster:latest
#   docker.io/miempresa/clip-raster:latest

Use a versioned tag (:0.1, :2025-06) in addition to :latest: it lets you re-publish without overwriting previous versions.

07 / 14
Step 3 · The critical link

The checksum: pin the exact image

C2D doesn't trust the tag (:latest can change). Pin the image by its sha256 digest — an immutable hash of the content. That digest is the checksum in the publishing script.

  • The --push already prints the digest when it finishes.
  • Or retrieve it with buildx imagetools inspect.
  • For multi-arch, use the manifest index digest (the top-level one).

If the image changes, the digest changes
Every time you rebuild and push, recompute the checksum and update the asset (re-publish or edit-algo). Otherwise, the node runs the old image.

terminal
# Get the digest of the already-pushed image
docker buildx imagetools inspect \
  registry.agrospai.udl.cat/library/clip-raster:latest

# Output (abridged):
# Name:      registry.agrospai.udl.cat/library/clip-raster:latest
# MediaType: application/vnd.oci.image.index.v1+json
# Digest:    sha256:b6ba37cc7a77c936bc03a58b0bbaf9debb55b2
#            5f4b04a097181ee442ea5e50f2
#   Manifests:
#     linux/amd64  sha256:...
#     linux/arm64  sha256:...

Copy the top-level Digest (the index one). That goes, as-is with its sha256: prefix, into the checksum field.

08 / 14
Step 4 · The publishing script

Anatomy of a publishing script

Each asset is a folder inside scripts/ with two files. pontus-x_cli publish takes the path to index.ts, imports it and runs its publish function.

  • index.ts — builds the asset with Nautilus.
  • description.md — the text users will see on the portal.
  • The signature is fixed: publish(folder, connection, provider, dryRun).

Create your own repo
AgrospAI keeps its scripts in a private repo (agrospai-configs). You create yours replicating this same structure — the repo name doesn't matter; what matters is scripts/<asset>/index.ts.

tu-repo-configs/
tu-repo-configs/         # private, yours (same structure)
└── scripts/
    └── clip-raster/
        ├── index.ts          # describes and publishes the asset
        └── description.md     # public description (Markdown)
scripts/clip-raster/index.ts
import { readFileSync } from 'fs';
import {
  AssetBuilder, ConsumerParameterBuilder,
  FileTypes, ServiceTypes, ServiceBuilder
} from '@deltadao/nautilus'
import { ServiceFileType } from '@deltadao/nautilus'

const publish = async (
  folder: string, connection: any,
  provider: string, dryRun: boolean
) => {
  // 1) metadata  2) algoMetadata  3) service  4) publish
}
09 / 14
Script · Metadata

Describe the asset with AssetBuilder

The AssetBuilder declares what the asset is: type, name, authorship, description, tags and the NFT that represents it on-chain.

  • setType('algorithm') — distinguishes algorithms from datasets.
  • The description is read from description.md.
  • The tags improve discovery on the portal.
  • setOwner uses the connected wallet's address.

The asset's NFT
setNftData defines the non-fungible token that gives the asset identity and ownership. transferable:false anchors it to your organization.

index.ts · (1) metadatos
const assetBuilder = new AssetBuilder();
assetBuilder.setType('algorithm')
  .setName('Clip Raster')
  .setAuthor('Your Organization')
  .setOwner(connection.wallet.address)
  .setDescription(
    readFileSync(`${folder}/description.md`, 'utf8'))
  .addTags(['agrospai', 'remote-sensing',
            'sentinel', 'raster'])
  .setLicense('MIT')
  .setNftData({
    name: 'Clip Raster',
    symbol: 'AgrospAI-ClipRaster',
    templateIndex: 1,
    tokenURI: 'https://.../logo.png',
    transferable: false
  });
10 / 14
Script · The container

Here goes the checksum: setAlgorithm

The algoMetadata block is the bridge between the portal and your image: it declares which image to run, with which entrypoint, and pinned to which checksum.

  • image + tag — exactly what you pushed to the registry.
  • checksum — the sha256 digest from step 3.
  • entrypointocean-execute --base-dir /data.
  • consumerParameters — the parameters the user fills in when launching the job.

The three must match
image, tag and checksum must correspond to the same published image. A mismatch here is the #1 cause of jobs that don't start.

index.ts · (2) algoMetadata + params
const cpb = new ConsumerParameterBuilder();
const refcat = cpb
  .setType('text').setName('refcat')
  .setLabel('Cadastral Reference')
  .setDescription('Cadastral reference code')
  .setRequired(true)
  .setDefault('25168A010000060000IZ')
  .build();

const algoMetadata = {
  language: 'python',
  version: '0.1',
  container: {
    entrypoint: 'ocean-execute --base-dir /data',
    image: 'registry.agrospai.udl.cat/library/clip-raster',
    tag: 'latest',
    checksum: 'sha256:b6ba37cc7a77c936bc03a58b0bb...'
  },
  consumerParameters: [ refcat ]
};
assetBuilder.setAlgorithm(algoMetadata);
11 / 14
Script · The service

The compute service, the price and the datatoken

The ServiceBuilder defines how the asset is consumed. For a C2D algorithm it's a COMPUTE-type service, with a backing file and a pricing policy.

  • serviceEndpoint — the provider that controls the asset.
  • addFile — the URL of the algorithm resource (e.g. the runner's entrypoint.py)…
  • …or a URL to a MinIO / object storage with protected variables (username, password, tokens).
  • setTimeout(0) — non-expiring access after purchase.
  • setPricing — free or fixed rate (EUROe).

Auth secrets outside the code
The file can point to a private bucket (minio.agrospai.udl.cat/secrets/…) whose content the node mounts at /data/transformations. That way the algo reads credentials —e.g. the OData login in clip-raster— without embedding them in the image.

index.ts · (3) servicio + (4) publish
const sb = new ServiceBuilder({
  serviceType: ServiceTypes.COMPUTE,
  fileType: FileTypes.URL });

const urlFile: ServiceFileType<FileTypes> = {
  type: 'url',
  // the algorithm resource… or a private bucket with
  // the auth secrets (mounted at /data/transformations)
  url: 'https://minio.agrospai.udl.cat/secrets/<id>/algorithm',
  method: 'GET' };

const service = sb
  .setServiceEndpoint(provider)
  .setTimeout(0)
  .addFile(urlFile)
  .setPricing(connection.pricingConfig.fixedRateEUROe(1))
  .setDatatokenNameAndSymbol(
      'AgrospAI Clip Raster', 'AgrospAI-ClipRaster')
  .build();
assetBuilder.addService(service);

const asset = assetBuilder.build();
if (!dryRun) await connection.nautilus.publish(asset);
12 / 14
Step 5 · Publish

Login and publish with pontus-x_cli

Configure the network in .env, authenticate with your key file, and publish pointing to the --provider and the script folder.

  • .env — sets the NETWORK (e.g. PONTUSXDEV).
  • login — loads your private key from your .json (your own account/wallet).
  • --dry-run first; then without it to publish.
  • Note the DID it returns: you'll need it for the link.

Your own account
The example .json is UdL's; you use yours (your wallet, exportable with export-private-key). The login determines who signs and who appears as owner — everything else is identical.

.env
NETWORK=PONTUSXDEV
# PRIVATE_KEY is filled in by `login`
terminal
# 1) Authenticate with YOUR key file (your account)
pontus-x_cli login TU-CLAVE.json

# 2) Dry run: prints the asset without publishing
pontus-x_cli publish \
  --provider https://provider.agrospai.udl.cat \
  scripts/clip-raster/index.ts --dry-run

# 3) Publish for real → returns the DID
pontus-x_cli publish \
  --provider https://provider.agrospai.udl.cat \
  scripts/clip-raster/index.ts

Where does the .json come from? From pontus-x_cli export-private-key, which dumps to a file a wallet you already have (it doesn't create one). Keep it safe: whoever has it can sign as you.

13 / 14
Step 6 · The link to the dataset

Every C2D job needs a dataset — even if the algo doesn't use it

By design, a C2D computation always runs against at least one dataset, and that dataset must explicitly authorize the algorithm with edit-trusted-algos.

  • If your algo consumes data: link it to the real dataset.
  • If it doesn't consume data (it gets them from an external API): link it to the Empty Placeholder Dataset.
  • edit-trusted-algos overwrites the list: pass all allowed algos at once.

Who authorizes
edit-trusted-algos is run by the dataset owner. login with that identity first (not the algorithm's, if they differ).

terminal · caso con dataset propio
# Authorize your algo on the dataset (as the dataset owner)
pontus-x_cli edit-trusted-algos \
  did:op:<DATASET> --algos did:op:<ALGORITHM>
terminal · caso sin datos (placeholder)
# 1) Publish the empty dataset once
pontus-x_cli publish \
  --provider https://provider.agrospai.udl.cat \
  scripts/placeholder-dataset/index.ts

# 2) Authorize your algo on the placeholder
pontus-x_cli edit-trusted-algos \
  did:op:<PLACEHOLDER> --algos did:op:<ALGORITHM>

The algorithm ignores the placeholder's content and produces its output from its own source. You launch the job with placeholder + algorithm.

Summary

Checklist to publish your algorithm

  • Create the builder: docker buildx create --use.
  • Build with --platform (multi-arch if unsure) and --push.
  • Push to a registry accessible by the C2D node.
  • Get the sha256 with imagetools inspect.
  • Create the script folder: index.ts + description.md.
  • Set consistent image · tag · checksum in setAlgorithm.
  • login with the owner identity.
  • publish --dry-run, review, and publish for real.
  • Note the algorithm's DID.
  • edit-trusted-algos on the dataset (or the placeholder).

Resources
Reference code clip-raster: github.com/AgrospAI/ocean-algo
Publishing scripts: create your own repo with the structure of agrospai-configs (private) · CLI: github.com/AgrospAI/pontus-x_cli
Questions about the build, the registry or publishing? The AgrospAI team supports you through the integration.

AgrospAI · Universitat de Lleida · Compute-to-Data data space

to navigate