agents/openai.yaml
interface:
display_name: "Akash"
short_description: "Deploy workloads and write Akash SDL"
brand_color: "#FF414C"
default_prompt: "Use $akash to write and deploy an SDL on Akash."
policy:
allow_implicit_invocation: true
rules/authz/granting-permissions.md
# Granting Permissions
How to create and manage AuthZ grants on Akash.
## Creating Grants
### Generic Authorization
Grant permission for specific message types:
```bash
provider-services tx authz grant <GRANTEE_ADDRESS> generic \
--msg-type <MSG_TYPE_URL> \
--from <GRANTER_KEY> \
--expiration "2025-12-31T23:59:59Z"
```
### Common Deployment Grants
```bash
GRANTEE="akash1grantee..."
# Full deployment lifecycle
provider-services tx authz grant $GRANTEE generic \
--msg-type /akash.deployment.v1beta4.MsgCreateDeployment \
--from granter
provider-services tx authz grant $GRANTEE generic \
--msg-type /akash.deployment.v1beta4.MsgUpdateDeployment \
--from granter
provider-services tx authz grant $GRANTEE generic \
--msg-type /akash.deployment.v1beta4.MsgCloseDeployment \
--from granter
provider-services tx authz grant $GRANTEE generic \
--msg-type /akash.deployment.v1beta4.MsgDepositDeployment \
--from granter
# Lease management
provider-services tx authz grant $GRANTEE generic \
--msg-type /akash.market.v1beta5.MsgCreateLease \
--from granter
```
### Fee Grant
Allow grantee to use granter's funds for fees:
```bash
# Unlimited fee grant
provider-services tx feegrant grant $(provider-services keys show granter -a) $GRANTEE \
--from granter
# With limit
provider-services tx feegrant grant $(provider-services keys show granter -a) $GRANTEE \
--spend-limit 10000000uakt \
--from granter
# With time expiration
provider-services tx feegrant grant $(provider-services keys show granter -a) $GRANTEE \
--expiration "2025-06-30T00:00:00Z" \
--from granter
# With both
provider-services tx feegrant grant $(provider-services keys show granter -a) $GRANTEE \
--spend-limit 10000000uakt \
--expiration "2025-06-30T00:00:00Z" \
--from granter
```
## Grant Expiration
### Set Expiration on Grant
```bash
provider-services tx authz grant $GRANTEE generic \
--msg-type /akash.deployment.v1beta4.MsgCreateDeployment \
--expiration "2025-12-31T23:59:59Z" \
--from granter
```
### No Expiration
Omit `--expiration` for indefinite grants (not recommended for production).
## Query Grants
### List All Grants from Address
```bash
provider-services query authz grants-by-granter $(provider-services keys show granter -a)
```
### List All Grants to Address
```bash
provider-services query authz grants-by-grantee $(provider-services keys show grantee -a)
```
### Query Specific Grant
```bash
provider-services query authz grants \
$(provider-services keys show granter -a) \
$(provider-services keys show grantee -a) \
/akash.deployment.v1beta4.MsgCreateDeployment
```
### Query Fee Grants
```bash
provider-services query feegrant grants-by-granter $(provider-services keys show granter -a)
provider-services query feegrant grants-by-grantee $(provider-services keys show grantee -a)
```
## Revoke Grants
### Revoke AuthZ Grant
```bash
provider-services tx authz revoke $GRANTEE \
/akash.deployment.v1beta4.MsgCreateDeployment \
--from granter
```
### Revoke Fee Grant
```bash
provider-services tx feegrant revoke $(provider-services keys show granter -a) $GRANTEE \
--from granter
```
### Revoke All
Revoke each grant individually - there's no batch revoke.
## Security Best Practices
1. **Always set expiration** - Prevents indefinite access
2. **Grant minimum permissions** - Only what's needed
3. **Use separate grantee accounts** - Don't reuse keys
4. **Monitor grants** - Regularly audit active grants
5. **Revoke when done** - Clean up unused grants
6. **Use fee limits** - Set spending limits on fee grants
## Message Type Reference
| Message Type | Purpose |
|-------------|---------|
| `/akash.deployment.v1beta4.MsgCreateDeployment` | Create deployment |
| `/akash.deployment.v1beta4.MsgUpdateDeployment` | Update deployment |
| `/akash.deployment.v1beta4.MsgCloseDeployment` | Close deployment |
| `/akash.deployment.v1beta4.MsgDepositDeployment` | Deposit to deployment |
| `/akash.market.v1beta5.MsgCreateLease` | Create lease |
| `/akash.cert.v1.MsgCreateCertificate` | Create certificate |
| `/akash.cert.v1.MsgRevokeCertificate` | Revoke certificate |
rules/authz/overview.md
# AuthZ Overview
Akash supports Cosmos SDK AuthZ (authorization) for delegated permissions, allowing one account to perform actions on behalf of another.
## What is AuthZ?
AuthZ allows a **granter** to authorize a **grantee** to execute specific message types. This enables:
- Deployment automation without exposing main wallet
- Fee grants for sponsored deployments
- Delegated deployment management
- Multi-tenant deployment platforms
## Use Cases
| Use Case | Granter | Grantee | Permission |
|----------|---------|---------|------------|
| Automation bot | User wallet | Bot wallet | CreateDeployment, CloseDeployment |
| Fee sponsorship | Sponsor | User | Pay fees on behalf |
| Platform | User | Platform wallet | Manage deployments |
| Team access | Org wallet | Team member | Deployment management |
## Grant Types
### Deployment Grants
Allow creating and managing deployments:
```bash
# Grant deployment creation
provider-services tx authz grant <GRANTEE_ADDRESS> generic \
--msg-type /akash.deployment.v1beta4.MsgCreateDeployment \
--from granter
# Grant deployment closure
provider-services tx authz grant <GRANTEE_ADDRESS> generic \
--msg-type /akash.deployment.v1beta4.MsgCloseDeployment \
--from granter
# Grant deposit
provider-services tx authz grant <GRANTEE_ADDRESS> generic \
--msg-type /akash.deployment.v1beta4.MsgDepositDeployment \
--from granter
```
### Market Grants
Allow lease operations:
```bash
# Grant lease creation
provider-services tx authz grant <GRANTEE_ADDRESS> generic \
--msg-type /akash.market.v1beta5.MsgCreateLease \
--from granter
```
### Fee Grants
Allow one account to pay fees for another:
```bash
# Basic fee grant
provider-services tx feegrant grant <GRANTER_ADDRESS> <GRANTEE_ADDRESS> \
--from granter
# With spending limit
provider-services tx feegrant grant <GRANTER_ADDRESS> <GRANTEE_ADDRESS> \
--spend-limit 10000000uakt \
--from granter
# With expiration
provider-services tx feegrant grant <GRANTER_ADDRESS> <GRANTEE_ADDRESS> \
--expiration "2025-12-31T23:59:59Z" \
--from granter
```
## How It Works
```
┌──────────┐ ┌──────────┐
│ Granter │ ─── Grant Auth ──► │ Chain │
│ (Owner) │ │ │
└──────────┘ └──────────┘
│
┌──────────┐ │
│ Grantee │ ─── Execute Msg ───────►│
│ (Bot) │ (on behalf of │
└──────────┘ granter) │
```
1. Granter creates authorization on-chain
2. Grantee constructs messages with granter's address as owner
3. Grantee wraps messages in `MsgExec`
4. Chain verifies authorization exists
5. Message executes as if sent by granter
## Related Documentation
- **@granting-permissions.md** - Detailed granting guide
- **@using-grants.md** - Using grants programmatically
rules/authz/using-grants.md
# Using AuthZ Grants
Execute transactions on behalf of a granter using AuthZ.
## CLI Usage
### Execute as Grantee
Wrap any authorized message in `MsgExec`:
```bash
# Create deployment on behalf of granter
provider-services tx authz exec <(provider-services tx deployment create deploy.yaml \
--from <GRANTER_ADDRESS> \
--generate-only) \
--from grantee
```
### Step-by-Step Example
```bash
GRANTER="akash1granter..."
GRANTEE_KEY="grantee"
# 1. Generate unsigned deployment tx (from granter's perspective)
provider-services tx deployment create deploy.yaml \
--from $GRANTER \
--generate-only > unsigned_tx.json
# 2. Execute as grantee
provider-services tx authz exec unsigned_tx.json \
--from $GRANTEE_KEY
# Clean up
rm unsigned_tx.json
```
### With Fee Grant
```bash
# Execute and use granter's fees
provider-services tx authz exec unsigned_tx.json \
--from $GRANTEE_KEY \
--fee-granter $GRANTER
```
## TypeScript Usage
### Execute Grant with SDK
```typescript
import { MsgExec } from "cosmjs-types/cosmos/authz/v1beta1/tx";
import { MsgCreateDeployment, MsgCloseDeployment } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4";
async function executeAsGrantee(
client: SigningStargateClient,
granteeAddress: string,
granterAddress: string,
sdl: SDL
) {
// Build the inner message (from granter's perspective)
const innerMsg = MsgCreateDeployment.fromPartial({
id: {
owner: granterAddress, // Granter is the deployment owner
dseq: BigInt(Date.now())
},
groups: sdl.groups(),
version: await sdl.manifestVersion(),
deposit: { denom: "uact", amount: "5000000" },
depositor: granterAddress
});
// Wrap in MsgExec
const execMsg = {
typeUrl: "/cosmos.authz.v1beta1.MsgExec",
value: MsgExec.fromPartial({
grantee: granteeAddress,
msgs: [
{
typeUrl: "/akash.deployment.v1beta4.MsgCreateDeployment",
value: MsgCreateDeployment.encode(innerMsg).finish()
}
]
})
};
// Sign and broadcast as grantee
const result = await client.signAndBroadcast(
granteeAddress,
[execMsg],
"auto"
);
return result;
}
```
### Grant Creation with SDK
```typescript
import { MsgGrant } from "cosmjs-types/cosmos/authz/v1beta1/tx";
import { GenericAuthorization } from "cosmjs-types/cosmos/authz/v1beta1/authz";
async function createGrant(
client: SigningStargateClient,
granterAddress: string,
granteeAddress: string,
msgTypeUrl: string,
expirationDate: Date
) {
const authorization = GenericAuthorization.fromPartial({
msg: msgTypeUrl
});
const msg = {
typeUrl: "/cosmos.authz.v1beta1.MsgGrant",
value: MsgGrant.fromPartial({
granter: granterAddress,
grantee: granteeAddress,
grant: {
authorization: {
typeUrl: "/cosmos.authz.v1beta1.GenericAuthorization",
value: GenericAuthorization.encode(authorization).finish()
},
expiration: {
seconds: BigInt(Math.floor(expirationDate.getTime() / 1000)),
nanos: 0
}
}
})
};
return client.signAndBroadcast(granterAddress, [msg], "auto");
}
```
### Complete Automation Example
```typescript
class AkashAutomation {
private granteeClient: SigningStargateClient;
private granteeAddress: string;
private granterAddress: string;
constructor(
granteeClient: SigningStargateClient,
granteeAddress: string,
granterAddress: string
) {
this.granteeClient = granteeClient;
this.granteeAddress = granteeAddress;
this.granterAddress = granterAddress;
}
async createDeployment(sdlContent: string) {
const sdl = SDL.fromString(sdlContent);
const dseq = Date.now().toString();
const innerMsg = MsgCreateDeployment.fromPartial({
id: { owner: this.granterAddress, dseq: BigInt(dseq) },
groups: sdl.groups(),
version: await sdl.manifestVersion(),
deposit: { denom: "uact", amount: "5000000" },
depositor: this.granterAddress
});
const execMsg = {
typeUrl: "/cosmos.authz.v1beta1.MsgExec",
value: MsgExec.fromPartial({
grantee: this.granteeAddress,
msgs: [{
typeUrl: "/akash.deployment.v1beta4.MsgCreateDeployment",
value: MsgCreateDeployment.encode(innerMsg).finish()
}]
})
};
const result = await this.granteeClient.signAndBroadcast(
this.granteeAddress,
[execMsg],
"auto"
);
return { dseq, txHash: result.transactionHash };
}
async closeDeployment(dseq: string) {
const innerMsg = MsgCloseDeployment.fromPartial({
id: { owner: this.granterAddress, dseq: BigInt(dseq) }
});
const execMsg = {
typeUrl: "/cosmos.authz.v1beta1.MsgExec",
value: MsgExec.fromPartial({
grantee: this.granteeAddress,
msgs: [{
typeUrl: "/akash.deployment.v1beta4.MsgCloseDeployment",
value: MsgCloseDeployment.encode(innerMsg).finish()
}]
})
};
return this.granteeClient.signAndBroadcast(
this.granteeAddress,
[execMsg],
"auto"
);
}
}
```
## Error Handling
### Authorization Not Found
```
authorization not found: unauthorized
```
**Cause:** Grant doesn't exist or has expired.
**Solution:** Create or renew the grant.
### Unauthorized Message Type
```
unauthorized: authorization not found
```
**Cause:** The grant doesn't cover the message type being executed.
**Solution:** Grant the specific message type needed.
### Expired Grant
```
authorization expired
```
**Cause:** Grant expiration date has passed.
**Solution:** Create a new grant with updated expiration.
## Monitoring Grants
### Check Grant Status
```bash
# Verify grant exists
provider-services query authz grants \
$(provider-services keys show granter -a) \
$(provider-services keys show grantee -a)
```
### Automated Monitoring
```typescript
async function checkGrantValid(
client: StargateClient,
granterAddress: string,
granteeAddress: string,
msgType: string
): Promise<boolean> {
try {
const grants = await client.queryClient.authz.grants(
granterAddress,
granteeAddress,
msgType
);
return grants.grants.length > 0;
} catch {
return false;
}
}
```
rules/bid-matching/adaptation-rules.md
# Bid-Matching Adaptation Rules
When adapting an SDL to widen the bid pool, follow this priority order. The order encodes a real lesson: GPU model swaps are tempting because they multiply bidder count fast, but they silently break workloads when the new GPU can't fit the model — bids land, the lease starts ticking, and the container OOMs at startup. Count reductions preserve workload behavior; model swaps don't.
## What to leave alone — pricing and denom
**Never modify `pricing.<profile>.amount` or `pricing.<profile>.denom`.**
- The Akash chain enforces a max bid price. Raising `amount` can trip a *"Unit price exceeds the maximum allowed by the network"* error and the deployment is rejected outright before any bid window opens.
- The providers endpoint does **not** expose which denoms a provider accepts, so denom changes are guesswork. Leave `denom` alone.
- Pricing is a deployer choice, not a bid-matching lever. The matcher's job is to align compute requirements (CPU/memory/storage/GPU/feature flags) with provider supply.
If capability matches are tiny and workload-relaxation options are exhausted, recommend out-of-band actions (direct provider DMs on Akash Discord, smaller-model functional test, pre-arranged capacity) — don't reach for the price knob.
## What to leave alone — CPU architecture
**Never change or remove `cpu.attributes.arch` to win a bid.**
- `arch` is a hard constraint, not a preference. An arm64 image does not start on an amd64 node and vice versa; a bid on the wrong architecture fails at container start and the lease bills until it is closed.
- Unlike a GPU model there is no "similar or larger" architecture to fall back to. There are two values and they are not interchangeable.
- If no online, audited provider serves the requested architecture, say exactly that: "No provider currently serves `arm64`." Do not drop the attribute and do not switch it to `amd64`. The SDL has no implicit default. Providers read a group with no `arch` as amd64, but that is the provider's interpretation, not something to write into the SDL.
- The one legitimate edit is when the user asked for an architecture they do not need (a multi-arch image with no ARM requirement) and confirms it. Then remove the attribute rather than replacing it with `amd64`.
## Adaptation priority — try each tier before the next
### 1. Reduce GPU count, keep the requested model
Safest first move. The model the user picked is presumably the one their workload actually runs on; lower count keeps GPU architecture, memory-per-card, and bandwidth identical, so behavior at startup is predictable.
Example: `8× H200 → 4× H200` before considering H100.
If the workload (TP/DP config, model memory footprint) genuinely needs N GPUs, this lever is unavailable — say so explicitly and skip to step 2.
### 2. Add fallback GPU models *of similar or larger memory*
Only after step 1. Order from closest match downward (H200 → H100 → A100). For each model added, sanity-check the workload's memory budget — adding a smaller-memory GPU just trades "no bids" for "bids that OOM at startup," which costs the user real money on the lease before they can close it.
**Never add a model the workload cannot fit.** When unsure of fit, say so and let the user confirm. Quick reference for big LLMs:
| GPUs | Total memory | Fits Kimi-K2 / DeepSeek-V3-class (~1 TB fp8)? |
|------|---:|---|
| 8× H200 (141 GB) | 1128 GB | yes, comfortable |
| 8× H100 (80 GB) | 640 GB | only with fp8 + careful KV cache |
| 8× A100-80GB | 640 GB | tight, may OOM with long context |
| 4× any 80 GB | 320 GB | no |
### 3. Drop the model constraint entirely
Last resort. Use `vendor.nvidia:` with no model list. Only if step 2 still yields nothing and the user has explicitly accepted the OOM/quality risk.
### 4. Storage class
`beta3` is supported by fewer providers than `beta2`. RAM-class volumes require opt-in. Class support is read from `attributes[]` keys `capabilities/storage/<N>/class` (paired with `.../persistent`).
Counter-intuitive case: relaxing `beta3 → beta2` can *reduce* the match count if your workload's GPU model only exists at providers that declare `beta3`. Always re-run the matcher after a class change.
### 5. Persistent storage
If the workload can tolerate ephemeral, skip persistent entirely. Verify the provider has `featPersistentStorage=true` (or attribute `feat-persistent-storage=true`) before assuming a persistent volume will land.
### 6. IP endpoints
`featEndpointIp` is relatively rare; avoid if not essential.
### 7. CPU/memory size
Very large single-node asks (>64 CPU, >256 GiB) narrow the pool. Sizing should follow real workload need; oversized asks filter providers without buying anything.
## When changes are exhausted
If capability matches plateau at 1–2 providers and the workload can't be relaxed further, **say so directly and stop iterating the SDL.** The remaining gates — bid-engine price floors, denom acceptance config, deployment ACLs, stale capacity — aren't visible in the providers endpoint and aren't fixable from the SDL.
A `cpu_arch` plateau is a supply fact, not an SDL problem. Report that no provider serves the architecture and stop; do not relax it.
Recommend out-of-band actions:
- Direct provider DM on Akash Discord (with a 2-provider pool, a 30-second message beats another deploy cycle).
- Smaller-model functional test on the same SDL shape, to validate plumbing while waiting on capacity.
- Pre-arranged capacity for niche workloads that aren't a walk-up bid today.
## What providers tell us about feature support
Use these fields when deciding whether a provider can satisfy a feature:
| Feature | Where to look |
|---|---|
| Persistent storage at all | top-level `featPersistentStorage`, OR attribute `feat-persistent-storage=true` |
| Specific persistent class (`beta2`/`beta3`) | attributes `capabilities/storage/<N>/class=<cls>` paired with `capabilities/storage/<N>/persistent=true`; fallback `feat-persistent-storage-type=<cls>` |
| RAM-class volumes | attribute `capabilities/storage/<N>/class=ram` |
| IP endpoints | top-level `featEndpointIp` |
| Custom domains | top-level `featEndpointCustomDomain` |
| CPU architecture | `hardwareCpuArch` (Console-derived from on-chain attribute `capabilities/cpu/arch`; declared vocabulary `x86-64` / `arm-64`, but `x86_64`, `amd64`, `arm64` occur in the wild, so normalise). Self-declared and empty on most providers. The bid engine matches on live node inventory, which this endpoint does not expose; the managed Console's bid screening does read it. |
| GPU vendor/model | `gpuModels[]` with `{vendor, model, ram, interface}` |
| GPU free count (any model) | `stats.gpu.available` |
| Region / location | `locationRegion`, `country`, `ipCountry`, attributes `region`, `location-region` |
`stats.gpu.available` is **not** broken down by model — a provider with mixed GPU types reports a single total. Treat the model match as best-effort and surface that caveat when relevant.
The providers endpoint does **not** expose: accepted denoms, bid-engine price floors, deployment ACLs, or per-model GPU availability. When a deployment capability-matches but gets no bids, those invisible factors are usually the gate.
rules/bid-matching/matching-rules.md
# SDL ↔ Provider Field Mapping
Reference for how the matcher maps SDL compute requirements to provider capability fields returned by `https://console-api.akash.network/v1/providers`.
The matcher is a *forecast* of what the actual provider bid engine in [akash-network/provider](https://github.com/akash-network/provider) will accept. Where the underlying logic differs from a naive pool-wide check, this doc cites the canonical file:line in the provider repo so the matcher's behavior can be verified against ground truth. **Caveat up front:** the providers endpoint reports pool-wide totals (CPU/memory/GPU/storage `available`) per provider; the actual bid engine works *per node* (each replica must fit on a single node, all GPUs of a service on one node). The matcher is therefore optimistic for multi-replica or large single-node GPU asks.
## Filter: which providers to consider
| Provider field | Required value | Why |
|----------------|----------------|-----|
| `isOnline` | `true` | Offline providers don't respond to bid requests at all. |
| `isAudited` | `true` | Console and most tooling surfaces audited providers by default; unaudited providers exist but are not the pool most deployments land on. |
Both filters are applied up front before any capability checks. The funnel reports the dropoff at each stage.
## CPU
- **SDL:** `resources.cpu.units` — accepts `0.5`, `2`, or `"500m"` (millicores)
- **Provider:** `stats.cpu.available` — reported in millicores
- **Match:** `stats.cpu.available >= requested_millis * deployment.count`
The script multiplies per-service requirements by `deployment[svc][placement].count` because a single provider typically has to fit all replicas.
**Canonical:** `tryAdjust` does CPU via `SubMilliNLZ` per-node (`provider/cluster/kube/operators/clients/inventory/inventory.go:46`), then `Adjust` walks nodes one at a time decrementing `resources[i].Count` per fit (`inventory.go:243-294`). Overcommit factors (`CPUCommitLevel`/`MemoryCommitLevel`/`GPUCommitLevel`) are applied via `sdlutil.ComputeCommittedResources` *before* `Adjust`, scaling the requested amount down by the commit ratio (`provider/cluster/inventory.go:286-294`). The matcher does not model overcommit — its check is conservative for that reason but optimistic on per-node fit.
## CPU architecture
- **SDL:** `resources.cpu.attributes.arch` — `amd64` or `arm64`, or absent (no CPU attributes written)
- **Provider:** `hardwareCpuArch` — Console-derived from the self-declared on-chain attribute `capabilities/cpu/arch`. The declared vocabulary is `x86-64` / `arm-64`; `x86_64`, `amd64`, `arm64` and junk appear in the wild, and a repeated attribute is comma-joined. Empty on most providers. The raw `attributes[]` entry is honored as a fallback.
- **Match:** applies only when the SDL sets `arch`. Normalise the provider value (`x86-64`, `x86_64`, `amd64` → `amd64`; `arm-64`, `aarch64`, `arm64` → `arm64`; 32-bit `x86` / `arm` never match) and require equality. A provider that declares no architecture **fails** an explicit `arch` request. Its architecture can't be verified from this endpoint, and passing it would report nearly the whole amd64 pool as arm64-capable. This is the same rule as the empty `gpuModels` edge case below.
**Canonical:** the accepted values are enforced at SDL parse time, not by the provider. `cpuArchitectures` (`chain-sdk/go/sdl/cpu.go:20-23`) and `v2CPUAttributes.UnmarshalYAML` (`cpu.go:54-83`) reject anything but `amd64` / `arm64` and any key other than `arch`; the TypeScript SDK validates against the same enum in `chain-sdk/go/sdl/sdl-input.schema.yaml`. Omitting `arch` produces zero attributes; there is deliberately no default. Provider side: nodes report `CPUInfo.arch` in inventory (`chain-sdk/proto/provider/akash/inventory/v1/cpu.proto:36`, empty on inventory operators that predate the field), and bid-engine filtering of `arch` against node architecture ships with akash-network/provider#433 (AKT-490). The bid engine treats a group with no `arch` as amd64.
### CPU architecture caveats
- `capabilities/cpu/arch` is self-declared and rarely audited; the bid engine matches on live node inventory, which the providers endpoint does not expose. The matcher can miss a provider that runs arm64 nodes but declares nothing, and can pass one that declares `arm-64` with no free arm64 node.
- The managed Console's bid screening reads live node inventory in addition to this attribute, so its arm64 verdict is more reliable than the matcher's.
- This check is never relaxed. If `passes_cpu_arch` is the biggest filter, report that no online, audited provider currently serves that architecture and stop. Do not drop the attribute or switch it to `amd64`.
## Memory
- **SDL:** `resources.memory.size` — accepts `Ki`/`Mi`/`Gi`/`Ti` (binary) and `k`/`M`/`G`/`T` (decimal)
- **Provider:** `stats.memory.available` — bytes
- **Match:** `stats.memory.available >= requested_bytes * count`
## Storage
SDL storage can be a single object (`size: 5Gi`) or an array of named volumes with attributes.
### Ephemeral
- **SDL:** storage entries without `attributes.persistent: true`
- **Provider:** `stats.storage.ephemeral.available`
- **Match:** sum of ephemeral requests ≤ available
### Persistent
- **SDL:** storage entries with `attributes.persistent: true` and a `class` (e.g., `beta2`, `beta3`)
- **Provider capacity:** `stats.storage.persistent.available` (bytes)
- **Provider class support:** derived from the `attributes[]` array, which declares pairs of:
- `capabilities/storage/<N>/class = <beta1|beta2|beta3|ram|...>`
- `capabilities/storage/<N>/persistent = true|false`
A class counts as persistent-capable when the matching slot's `persistent` is `true`. The older `feat-persistent-storage-type=<class>` attribute is also honored as a fallback.
- **Match:** capacity sufficient AND the requested class is in the provider's set of persistent-capable classes.
Do **not** use `featPersistentStorageType` to match SDL classes — that field uses hardware taxonomy (`hdd`, `ssd`, `nvme`), not Akash's SDL class taxonomy (`beta1`, `beta2`, `beta3`). They are unrelated.
The providers endpoint does not break down persistent capacity by class, so total persistent availability is the only capacity check available — class support is verified via the attribute pairs above.
### RAM-class volumes
RAM-class storage (`class: ram`) is ephemeral by definition. The current matcher treats it as ephemeral capacity. Not all providers expose RAM-class volumes, but this endpoint doesn't explicitly report that — be aware the match can be optimistic for RAM volumes.
## GPU
- **SDL:** `resources.gpu.units` + `resources.gpu.attributes.vendor.<vendor>[{ model, ram, interface }]`. Each entry under `vendor.<v>[*]` is an *alternative* — any one of them being satisfied is a pass.
- **Provider:** `stats.gpu.available` (count, total across all models on the cluster), `gpuModels[]` (array of `{ vendor, model, ram, interface }`).
- **Match:**
- `stats.gpu.available >= units * count`, AND
- For each SDL alternative, the provider's `gpuModels` contains at least one entry matching every field that was specified — vendor (always required), `model` (case-insensitive), `ram` (string-equal, e.g. `"80Gi"`), `interface` (case-insensitive). The matcher tracks all four fields, not just model.
**Canonical:** `tryAdjustGPU` (`provider/cluster/kube/operators/clients/inventory/inventory.go:125-203`) iterates `rp.Info[]` (each entry is one discrete GPU) and decrements `reqCnt` only when vendor + model + optional RAM + interface all match. `cinventory.ParseGPUAttributes` resolves SDL keys like `vendor/nvidia/model/h100/ram/80Gi/interface/sxm5`. Wildcards: `*` is supported on the model field via `ExistsOrWildcard`.
### GPU matching caveats
- `stats.gpu.available` is not broken down by model. A provider with 4× A100 free and 4× H100 leased reports `available: 4` total — but the matcher treats "provider has the model in `gpuModels` AND total available ≥ requested" as a pass. **A provider with mixed GPU models can be overcounted** if some of those models are leased while the unrequested ones are free.
- The provider bid engine fits all GPUs of a service on a *single node*. The Console API exposes per-provider totals only — `stats.gpu.available: 8` could be 4 free on each of two nodes, which would not satisfy an `8× h200` ask. The matcher cannot detect this from the API.
- Rare models (H200, B200, MI300X) appear in very few providers' `gpuModels`. The biggest-filter analysis surfaces this automatically.
- For multi-model fallback, the user can declare multiple entries under `vendor.nvidia:` — the matcher considers any match a pass (mirrors `provider/.../inventory.go:tryAdjustGPU` outer loop).
## Placement attributes
- **SDL:** `profiles.placement.<name>.attributes` (a key=value map; e.g. `region: us-west`, `host: akash`, `tier: enterprise`)
- **Provider:** `attributes[]` array on the provider, with each entry `{ key, value, auditedBy[] }`
- **Match:** every required `key=value` from the SDL must be present on the provider, with **glob-aware** value comparison in either direction (provider-side or SDL-side wildcards work).
**Canonical:** `bidengine/order.go:shouldBid` (lines 523-594) calls `MatchAttributes` (`chain-sdk/.../v1beta4/groupspec.go:108-146`) which uses `Attributes.SubsetOf` with `filepath.Match`-based key matching (`chain-sdk/.../attributes/v1/attribute.go:104-219`). The provider's *own* declared attributes (e.g. `host=akash`, `region=us-west`, `capabilities/gpu=...`) must be a superset of the SDL's required attributes.
The matcher implements glob matching via Python `fnmatch.fnmatchcase` in both directions to mirror the `filepath.Match` glob semantics (limited — `*` matches any characters, `?` matches one).
## IP endpoints
- **SDL v2.1 trigger:** a service's `expose[*].to[*]` contains `{ ip: <name> }`, or the top-level `endpoints:` block declares one
- **Provider:** `featEndpointIp === true` (Console-API derived field)
- **Match:** required iff the SDL uses an IP endpoint
**Canonical:** the provider has no specific feat-key check for IP support; instead it tracks IP allocation in cluster inventory (`provider/cluster/inventory.go:432-447`) and rejects with `errInsufficientIPs` when no addresses are free. The matcher uses Console's `featEndpointIp` boolean as a heuristic since the per-IP-quota count isn't exposed.
## Pricing / denom
`uact` is the denom for SDL pricing and lease payment on Akash, and is the practical choice for new deployments. The providers endpoint does **not** expose accepted denominations, but in practice `uact` is what deployments and providers settle on for lease payment (`uakt` is only accepted via a rare burn-mint fallback).
The matcher treats only `uact` as recognized. Anything else — including legacy `uakt` and any `ibc/...` denom — gets a note that it's unrecognized, signalling the user to migrate. Do **not** advise switching denoms to "fix bids" beyond migrating to `uact` — beyond that, the bottleneck is almost always GPU model/count/storage class, not denom.
**Canonical chain check:** `x/market/handler/server.go:66` — when a provider posts a bid, the chain rejects with `ErrBidInvalidPrice` ("invalid bid price") if `order.Price().IsLT(msg.Price)`, i.e. the provider's bid exceeds the SDL's stated max amount. The Console UI surfaces this as *"Unit price exceeds the maximum allowed by the network"* (mapped in `console/apps/api/src/billing/services/chain-error/chain-error.service.ts`). Raising `pricing.<profile>.amount` can also trip this from the deployer side depending on how the chain validates `MsgCreateDeployment` — leave it alone.
**Provider-side pricing:** `BidPricingStrategy.CalculatePrice` (`provider/bidengine/pricing.go`) computes the provider's bid; `bidengine/order.go:419-427` rejects on denom mismatch or when the computed price exceeds the SDL ceiling. The matcher cannot see provider-side floor prices — they're a silent gate.
## Unit conversion quick reference
| Input | Parsed as |
|-------|-----------|
| `0.5` (cpu.units) | 500 millicores |
| `2` (cpu.units) | 2000 millicores |
| `"500m"` | 500 millicores |
| `512Mi` | 536,870,912 bytes |
| `2Gi` | 2,147,483,648 bytes |
| `100M` | 100,000,000 bytes |
## Edge cases
- **Missing `count`** — defaults to 1 per placement.
- **Multiple placements for one service** — the matcher uses the pricing/placement pair from each deployment entry; each profile is checked against the union of its usages.
- **Storage name without `class`** — treated as ephemeral with no class constraint.
- **GPU with `units: 0` and attributes** — invalid per SDL rules, ignored by the matcher (no GPU check applied).
- **`gpuModels` empty but `stats.gpu.available > 0`** — provider has GPUs but model is unknown; the model check fails for any model-constrained request.
- **`cpu.attributes.arch` absent** — no arch check applied. The bid engine treats the group as amd64; the SDL itself says nothing.
- **Provider `hardwareCpuArch` is `x86` or `arm`** — 32-bit values, never match either accepted SDL value.
## Things the matcher cannot see (canonical bid filters not in the providers API)
- **Provider-local attribute allowlist** (`cfg.Attributes` checked in `provider/bidengine/order.go:531`) — provider may require *the deployer's order* to declare specific attributes. Not exposed.
- **MaxGroupVolumes** (`provider/bidengine/config.go`, checked at `order.go:548`) — provider can cap total volume count per group.
- **Per-node fit and overcommit** — see CPU and GPU sections above.
- **`IsStorageClassSupported` cluster check** — even if attributes declare `class=beta3`, the provider's cluster operator does a runtime check (`provider/cluster/kube/.../inventory.go:85-107`) the matcher can't reach.
- **Custom shellscript filter** (`provider/bidengine/shellscript.go`) — providers can run arbitrary scripts to reject orders. Invisible.
- **Existing bid dedupe and stale expiry** (`order.go:255`) — if a bid is already pending, the provider doesn't re-bid.
- **Per-node CPU architecture** (`CPUInfo.arch` in provider inventory) — the providers endpoint only exposes the self-declared `capabilities/cpu/arch` attribute.
## Keep in mind
This matcher predicts **capability**, not **intent**. A provider can satisfy the SDL on paper and still decline to bid because of its own price floor, regional policy, or deployment filter. Treat a positive match as "the bid can land here if the provider's price engine agrees," not as a guarantee.
rules/bid-matching/overview.md
# Bid Matching
Diagnose why an Akash SDL isn't getting bids — or predict whether it will — by matching its compute requirements against the live audited + online provider set returned by `https://console-api.akash.network/v1/providers`.
This is the live-market companion to the SDL syntax/validation rules under `rules/sdl/`. Schema validity says the SDL *parses*; bid matching says the SDL *can land on a real provider right now*.
## When to use this
Trigger this workflow whenever a user:
- Asks "why am I not getting bids" / "will this SDL get bids" / "which providers can run this"
- Pastes an SDL and wants a feasibility check before broadcasting
- Plans a GPU or otherwise large deployment and wants to verify supply
- Reports an empty or stuck bid list on Console
- Asks to "adapt", "fix", or "optimize" an SDL for better bid coverage
Pair with the SDL rules: those tell you if the YAML is valid; this tells you if the network actually has a provider that can run it.
## How it works
1. Fetch `https://console-api.akash.network/v1/providers`.
2. Keep only providers where `isOnline === true` AND `isAudited === true`. Both filters matter — offline providers won't bid at all, and audited providers are what Console surfaces by default.
3. Extract compute requirements from every profile in the SDL (CPU millis, CPU architecture if set, memory, ephemeral + persistent storage, storage class, GPU count + vendor + model, IP endpoint need).
4. Check each provider's `stats.*.available` and capability flags against those requirements.
5. Produce a funnel that shows which single constraint collapses the provider pool. The biggest filter is usually more useful than the final count, because it tells the user what to relax.
## Running the matcher
The repo ships `scripts/match_providers.py`. Given an SDL file path, it fetches live providers and prints a JSON report:
```bash
python3 scripts/match_providers.py /path/to/deploy.yaml
```
Optional flags:
- `--top N` — top N matching providers per profile (default 5)
- `--api URL` — override the providers endpoint
- `--api file:///abs/path/providers.json` — run against a saved providers response instead of the live endpoint
- `--json` — emit only JSON (default behavior anyway)
If the user pasted an SDL inline, save it to a temp file first:
```bash
TMP=$(mktemp -t sdl).yaml
cat > "$TMP" <<'EOF'
<paste SDL here>
EOF
python3 scripts/match_providers.py "$TMP"
```
Requires `pyyaml`. Install with `pip3 install --user pyyaml` (or use a venv).
## Interpreting the report
The script returns JSON like:
```json
{
"total_providers": 60,
"online_providers": 34,
"online_audited_providers": 22,
"profiles": [
{
"profile": "sglang",
"requirements": { ... },
"funnel": [
{"stage": "total_providers", "count": 60},
{"stage": "online", "count": 34},
{"stage": "online_audited", "count": 22},
{"stage": "passes_cpu", "count": 8, "applicable": true},
{"stage": "passes_memory", "count": 6, "applicable": true},
{"stage": "passes_gpu_model", "count": 0, "applicable": true}
],
"biggest_filter": "passes_gpu_model",
"match_count": 0,
"feasible": false
}
]
}
```
**The most important field is `biggest_filter`.** It names the single constraint that the fewest providers pass. That's the lever to pull first to widen the pool.
Each `passes_<check>` stage has an `applicable` flag — checks that don't apply to this profile (e.g., `passes_gpu_model` when the profile has no GPU) should be ignored.
## How to present results
Every response must include both the diagnosis **and** a complete adapted SDL — even if the verdict is "feasible" (in which case the adapted SDL is identical to the input, noted as such). The user wants a copy-pasteable file, not snippets to hand-merge.
Structure:
1. **One-line feasibility verdict** — "X of Y online+audited providers can satisfy profile `<name>`." If zero, say so plainly.
2. **Funnel** — show the dropoff stage by stage. A compact table works well. Highlight the biggest filter.
3. **Requirement summary** — what the profile asks for (human-readable: "96 cores, 512 GiB RAM, 8× nvidia/h200, 800 GiB persistent beta3").
4. **Top matching providers** (if any) — name, region, GPUs. Don't dump all fields.
5. **Changes made** — tight bullet list naming every field being modified and why, ordered by impact. One line per change.
6. **Full adapted SDL** — a single fenced ```yaml``` block containing the entire adapted document, not a diff or partial snippet. Preserve the user's original keys, comments, ordering, and structure except where deliberately changing things.
7. **Caveats** — anything the report can't verify. See "Known limitations" below.
The full SDL is required on every invocation; never skip it with "here are the changes, apply them to your file." If the SDL already matches enough providers and no change is warranted, output the original SDL verbatim under step 6 and say "no changes needed" in step 5.
When picking changes:
- Work from the biggest filter outward — don't relax things that aren't filtering.
- Never silently change semantics (drop a persistent volume the app needs, remove an IP endpoint the service depends on). If a relaxation would break the app, say so in step 5 and skip it rather than applying it.
- If the user's app intent is ambiguous (e.g., can the workload fit on fewer GPUs?), make the conservative choice (keep the count) and mention the alternative for them to opt into.
- Preserve the input's version, image tags, env vars, command/args, and anything else untouched. Do not reformat or restyle YAML.
## Known limitations
- **Denom support is not in the providers endpoint.** The script accepts only `uact` as recognized; any other denom — including `ibc/...` — gets a warning. It can't verify which denoms a *specific* provider accepts, but in practice only `uact` is taken.
- **Per-GPU-model availability is not exposed.** The endpoint reports total GPU count via `stats.gpu.available` and a list of models via `gpuModels[]`, but not a per-model available count. The matcher treats "provider has this model in `gpuModels` AND total GPUs available ≥ requested" as a match — it can overcount when one provider has mixed models.
- **Provider-side bid config (floor prices, deployment ACLs, denom acceptance) is not exposed.** A provider can satisfy the SDL on paper and still decline the bid based on its own pricing rules or policy. The report predicts *capability*, not *intent*.
- **Stats are snapshots.** Available capacity changes minute-to-minute as leases come and go.
- **CPU architecture is self-declared.** The endpoint exposes `hardwareCpuArch` (attribute `capabilities/cpu/arch`), not per-node inventory. Providers that declare nothing fail an explicit `arch` request. Never relax `arch`; a zero-match result means no provider serves that architecture right now.
Surface these caveats when they matter — especially when the verdict is "feasible" but the user is still not getting bids. At that point SDL changes are the wrong lever; recommend out-of-band actions (direct provider DM on Akash Discord, smaller-model functional test, pre-arranged capacity).
## Adaptation rules
See `rules/bid-matching/adaptation-rules.md` for the priority order of changes (what to change, what to leave alone, why).
For the underlying SDL ↔ provider field mapping, unit conversions, and edge cases, see `rules/bid-matching/matching-rules.md`.
rules/deploy/akashml/account-and-billing.md
# AkashML Account and Billing
AkashML billing is a **USD credit balance** on your AkashML account. It is **not** on-chain — there is no `uact` / `uakt` involvement on this path. AkashML pays the underlying providers on your behalf out of your credit balance.
This is structurally different from every other path in this skill. Anchor on the table below before reading the rest.
| Path | What you fund | Denom of record | Where the money goes |
|---|---|---|---|
| **AkashML** | AkashML credits (USD) | USD (off-chain) | AkashML pays inference providers |
| **Console API** | Console managed wallet (USD via Stripe) | USD → `uact` on-chain | Lease escrow for *your* deployment |
| **CLI / SDK** | Self-custody wallet (AKT, then burn → ACT) | `uakt` (gas) + `uact` (escrow) | Lease escrow for *your* deployment |
If you're already thinking about "deposit", "escrow", "uact", "burn rate", or "lease balance", you're on the wrong path — those concepts belong to deploying your own workload, not calling AkashML. See [@overview.md](overview.md) "When to use AkashML vs alternatives" if you're unsure which path you want.
## Funding the account
1. Sign up at [playground.akashml.com](https://playground.akashml.com) and verify your email.
2. Under **Settings → Billing**, add a payment method (Stripe). This unlocks **trial credits** on the account.
3. Top up manually as needed, or enable **auto-top-up** at a configured threshold.
### Auto-top-up
The dashboard lets you set:
- A **threshold** (e.g., $5) below which the account auto-charges your saved payment method.
- A **top-up amount** (e.g., $25) that gets added when the threshold trips.
This is dashboard-only configuration — there is no observed programmatic endpoint for managing billing settings. For automation that needs guaranteed availability, keep the threshold conservatively high and monitor usage.
### Credit expiration
- **Trial credits expire** — granted on email verify + payment method, with an expiration date attached.
- **Purchased credits do not expire.**
Plan around this: if you let an account go idle long enough, only the purchased balance remains. Always read the live balance from the dashboard rather than caching the "I bought $X last month" mental model.
## Reading usage and balance
Usage and remaining balance are surfaced in the **AkashML dashboard**. There is no documented programmatic balance endpoint at the time of writing — if you need to gate behavior on remaining credit from code, treat the dashboard as the source of truth and rely on `429` responses to back-pressure your client.
If a balance endpoint exists in a future release, it will be added here.
## Per-key limits
Each API key has independently configurable limits (set in the dashboard, not via API):
| Limit | Meaning | What happens when exceeded |
|---|---|---|
| **RPM** | Requests per minute | `429 Too Many Requests` |
| **TPM** | Tokens per minute (input + output combined) | `429 Too Many Requests` |
| **Concurrent** | Simultaneous in-flight requests | `429 Too Many Requests` |
| **Expiration** | Optional ISO 8601 date | Key stops working after this date |
The limits are per-key, so you can split workloads across keys to isolate noisy producers from latency-sensitive consumers.
## Two failure modes: out of credits vs rate limited
Treat these distinctly — the fixes differ.
| Status | Meaning | Headers to inspect | Fix |
|---|---|---|---|
| `402 Payment Required` | **Out of credits** on the account (not a per-key limit) | — | Top up the account, or enable auto-top-up. Retrying without topping up will keep failing. |
| `429 Too Many Requests` | Per-key rate limit (RPM/TPM/concurrent) | `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` | Honor `Retry-After`; back off. Raise the limit in the dashboard if it's a persistent ceiling. |
`402` is **not** retryable on its own — your client must either short-circuit, fall back to a different account, or surface the error to a human. Retrying a `402` in a tight loop just wastes the next request.
## Handling `429` with the right headers
```typescript
async function callWithBackoff(fn: () => Promise<Response>, maxRetries = 5) {
let delay = 1000;
for (let i = 0; i < maxRetries; i++) {
const response = await fn();
if (response.status === 402) {
throw new Error("Out of AkashML credits — top up the account");
}
if (response.status !== 429) return response;
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0", 10);
const reset = parseInt(response.headers.get("X-RateLimit-Reset") ?? "0", 10);
const computedDelay = retryAfter
? retryAfter * 1000
: reset
? Math.max(reset * 1000 - Date.now(), 0)
: delay;
await new Promise((r) => setTimeout(r, computedDelay));
delay = Math.min(delay * 2, 30_000);
}
throw new Error("Rate limited after retries");
}
```
Order of preference: `Retry-After` → `X-RateLimit-Reset` → exponential backoff with jitter.
## Stripe-specific notes
AkashML's billing relationship is with **AkashML**, not with the Akash chain. Stripe charges show up under the AkashML account. There is no token movement on-chain triggered by your AkashML usage — the chain-side economics of the underlying providers are handled by AkashML internally.
This means:
- **No AKT / ACT acquisition needed.** AkashML users never touch the Akash chain.
- **No `uact` deposit visible in your account.** Your balance is denominated in USD credits.
- **No lease lifecycle to manage.** AkashML owns the underlying deployments; you only interact with the inference API.
If you want chain-side visibility (lease balances, escrow burn rate, on-chain auditability), you want one of the four deployment paths, not AkashML.
## Cost shape
Pricing is **per-token**, varying by model. The dashboard displays current rates for each model. Pricing details are intentionally not enumerated here because they shift faster than this file does — always check the dashboard or the model's listing in the API response for live pricing.
For cost-sensitive workloads, the same trade-offs as any inference API apply:
- Cache frequent prompts client-side.
- Use shorter `max_tokens` and stricter `system` prompts.
- Pick smaller models for simple tasks; reserve large models for harder requests.
- Stream responses to reduce perceived latency without changing token cost.
## Related files
- **[@overview.md](overview.md)** — Path comparison and when AkashML is the wrong answer
- **[@authentication.md](authentication.md)** — Per-key limit semantics, env vars, rotation
- **[@api-reference.md](api-reference.md)** — `429` handling in context of the API surface
rules/deploy/akashml/api-reference.md
# AkashML API Reference
> **Hit the live endpoints — don't trust this file's snapshots.** Model IDs, prices, context lengths, and supported features change. Before recommending a model or quoting numbers, call `GET /v1/models` (or `GET /anthropic/v1/models`) and read the live response. The shapes documented here are stable; the *contents* of model lists, pricing, and feature flags are not.
AkashML exposes two HTTP surfaces that follow established schemas. Both accept the same `Authorization: Bearer $AKASHML_API_KEY` header.
| Surface | Base URL | Schema | Use when |
|---|---|---|---|
| **OpenAI-compatible** | `https://api.akashml.com/v1` | OpenAI REST API | You already use the `openai` SDK or generic OpenAI tooling |
| **Anthropic-compatible** | `https://api.akashml.com/anthropic` | Anthropic Messages API | You already use the `anthropic` SDK, Claude Code, or Anthropic-shaped clients |
Pick the surface that matches your existing client. Both are served by the same backend; the request/response shapes are the only difference.
## OpenAI-compatible endpoints
### `GET /v1/models` — list models
```bash
curl https://api.akashml.com/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY"
```
Returns the standard OpenAI list shape, enriched with AkashML-specific metadata (pricing, context length, capabilities):
```json
{
"object": "list",
"data": [
{
"id": "MiniMaxAI/MiniMax-M2.5",
"object": "model",
"created": 1700000000,
"owned_by": "akashml",
"name": "MiniMax M2.5",
"context_length": 200000,
"input_modalities": ["text"],
"output_modalities": ["text"],
"supported_features": ["chat", "tools", "streaming"],
"supported_sampling_parameters": ["temperature", "top_p", "top_k", "frequency_penalty"],
"max_output_length": 8192,
"quantization": "fp8",
"pricing": {
"input": "0.20",
"output": "1.00",
"request": "0"
}
}
]
}
```
Per-model `pricing` is per **million tokens** (USD). Read it from this endpoint rather than hardcoding — pricing changes.
Requires auth — calling without a key returns `401 Unauthorized`.
### `POST /v1/chat/completions` — chat completion
Standard OpenAI Chat Completions contract. Required fields: `model`, `messages`. Common optional fields: `max_completion_tokens` (preferred over the legacy `max_tokens`), `temperature`, `top_p`, `stop`, `tools`, `tool_choice`, `response_format`, `stream`, `seed`.
The response includes an **`Inference-Id`** header — capture it; you'll need it when contacting support about a specific call.
**curl:**
```bash
curl -X POST https://api.akashml.com/v1/chat/completions \
-H "Authorization: Bearer $AKASHML_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMaxAI/MiniMax-M2.5",
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Summarize the Akash Network in one sentence."}
],
"max_completion_tokens": 200
}'
```
**Python (`openai` SDK):**
```python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["AKASHML_API_KEY"],
base_url="https://api.akashml.com/v1",
)
response = client.chat.completions.create(
model="MiniMaxAI/MiniMax-M2.5",
messages=[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Summarize the Akash Network in one sentence."},
],
max_completion_tokens=200,
)
print(response.choices[0].message.content)
```
**Node (`openai` SDK):**
```typescript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.AKASHML_API_KEY,
baseURL: "https://api.akashml.com/v1",
});
const response = await client.chat.completions.create({
model: "MiniMaxAI/MiniMax-M2.5",
messages: [
{ role: "system", content: "You are a concise assistant." },
{ role: "user", content: "Summarize the Akash Network in one sentence." },
],
max_completion_tokens: 200,
});
console.log(response.choices[0].message.content);
```
#### Akash-specific extension: `reasoning`
The OpenAI surface accepts an Akash-specific `reasoning` object that controls thinking-style models:
```json
{
"model": "...",
"messages": [...],
"reasoning": {
"effort": "high",
"max_tokens": 4096,
"exclude": false,
"enabled": true
}
}
```
| Field | Meaning |
|---|---|
| `effort` | `xhigh` / `high` / `medium` / `low` / `minimal` / `none` (accepted values vary by model) |
| `max_tokens` | Max tokens spent on reasoning |
| `exclude` | If true, reasoning tokens are stripped from the response |
| `enabled` | Whether to enable reasoning at all |
This is non-portable to vanilla OpenAI — guard the field behind a feature flag if your code targets both AkashML and `api.openai.com`.
### Streaming chat completion
Add `stream: true` and consume the SSE stream the SDK exposes. The stream emits `data:` lines and terminates with `data: [DONE]`. Pass `stream_options: { include_usage: true }` if you need a final usage chunk.
```typescript
const stream = await client.chat.completions.create({
model: "MiniMaxAI/MiniMax-M2.5",
messages: [{ role: "user", content: "Stream a haiku about decentralized compute." }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```
curl with raw SSE:
```bash
curl -N -X POST https://api.akashml.com/v1/chat/completions \
-H "Authorization: Bearer $AKASHML_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"MiniMaxAI/MiniMax-M2.5","messages":[{"role":"user","content":"hi"}],"stream":true}'
```
### `POST /v1/completions` — legacy completion
Same auth and shape as the upstream OpenAI legacy completion endpoint. Prefer `/v1/chat/completions` for new code; this exists for compatibility with older clients.
## Anthropic-compatible endpoints
### `GET /anthropic/v1/models` — list models
```bash
curl https://api.akashml.com/anthropic/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY"
```
Response shape mirrors Anthropic's list-models response:
```json
{
"data": [
{
"type": "model",
"id": "MiniMaxAI--MiniMax-M2.5",
"display_name": "MiniMax M2.5",
"created_at": "2026-01-15T00:00:00Z"
}
],
"has_more": false,
"first_id": "MiniMaxAI--MiniMax-M2.5",
"last_id": "MiniMaxAI--MiniMax-M2.5"
}
```
This endpoint is reachable **without** auth (returns `200` even with no key), so it doubles as an unauthenticated reachability probe. (`GET /v1/models` on the OpenAI surface, by contrast, requires a Bearer key.)
**Model ID aliasing on the Anthropic surface.** Slashes in upstream IDs are aliased with `--` (e.g., `anthropic/claude-3-5-sonnet` → `anthropic--claude-3-5-sonnet`) — this exists because Claude Code rejects model IDs containing `/`. The list-models endpoint returns the aliased form. The Messages endpoint accepts either form (the canonical slashed form *and* the aliased form), so use whichever your client tolerates. If in doubt, pass the aliased form to play safe with Claude-Code-shaped clients.
### `POST /anthropic/v1/messages` — create message
Required fields: `model`, `messages`, `max_tokens` (Anthropic mandates `max_tokens` — not optional like on OpenAI). Common optional fields: `system`, `temperature`, `top_p`, `top_k`, `stop_sequences`, `stream`, `tools`, `tool_choice`, `thinking`, `metadata`.
**curl:**
```bash
curl -X POST https://api.akashml.com/anthropic/v1/messages \
-H "Authorization: Bearer $AKASHML_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMaxAI/MiniMax-M2.5",
"max_tokens": 256,
"messages": [
{"role": "user", "content": "Hello!"}
]
}'
```
**Python (`anthropic` SDK):**
```python
import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["AKASHML_API_KEY"],
base_url="https://api.akashml.com/anthropic",
)
response = client.messages.create(
model="MiniMaxAI/MiniMax-M2.5",
max_tokens=256,
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.content[0].text)
```
**Node (`@anthropic-ai/sdk`):**
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.AKASHML_API_KEY,
baseURL: "https://api.akashml.com/anthropic",
});
const response = await client.messages.create({
model: "MiniMaxAI/MiniMax-M2.5",
max_tokens: 256,
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.content[0].type === "text" ? response.content[0].text : "");
```
#### Extended thinking
Pass a `thinking` object to enable Anthropic-shaped reasoning traces:
```json
{
"model": "...",
"messages": [...],
"max_tokens": 4096,
"thinking": { "type": "enabled", "budget_tokens": 2048 }
}
```
The response's `content` array will include `thinking` blocks alongside `text` and `tool_use` blocks. Support depends on the chosen model — check the `supported_features` field from the OpenAI-surface `/v1/models` listing.
### Streaming messages
```python
with client.messages.stream(
model="MiniMaxAI/MiniMax-M2.5",
max_tokens=256,
messages=[{"role": "user", "content": "Stream a haiku."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
```
When `stream: true` is set, the response content type switches to `text/event-stream` and Anthropic-shaped events are emitted:
| Event | Meaning |
|---|---|
| `message_start` | Message envelope (id, model, role, usage) |
| `content_block_start` | A new content block begins (text, tool_use, thinking) |
| `content_block_delta` | Incremental content (text delta, input_json_delta, thinking_delta) |
| `content_block_stop` | End of a content block |
| `message_delta` | Running `stop_reason` / `usage` updates |
| `message_stop` | Terminal event |
| `ping` | Keep-alive |
| `error` | Terminal error event (see error type mapping below) |
The `anthropic` SDK abstracts these events; raw SSE consumers need to parse the event names.
## Model IDs across surfaces
Canonical AkashML model IDs use slashes (e.g., `MiniMaxAI/MiniMax-M2.5`). Both surfaces accept the slashed form. The Anthropic-surface list-models endpoint also returns each ID in a `--` aliased form because Claude Code (and clients of the same shape) reject slashed identifiers:
| Canonical (works on both surfaces) | Anthropic-surface aliased form (returned by `GET /anthropic/v1/models`) |
|---|---|
| `MiniMaxAI/MiniMax-M2.5` | `MiniMaxAI--MiniMax-M2.5` |
| `deepseek-ai/DeepSeek-V4-Flash` | `deepseek-ai--DeepSeek-V4-Flash` |
| `moonshotai/Kimi-K2.6` | `moonshotai--Kimi-K2.6` |
| `Qwen/Qwen3.5-35B-A3B` | `Qwen--Qwen3.5-35B-A3B` |
Rule of thumb: use the slashed form unless your client rejects slashes (Claude Code-shaped clients do). If you list and capture IDs at runtime, use whatever form the listing returned — don't translate by hand.
## Tool use, JSON mode, vision
These features follow the upstream SDK contract for each surface:
- **OpenAI surface** — `tools`, `tool_choice`, `response_format: { type: "json_object" }`, vision content blocks all behave as the OpenAI docs describe, *to the extent the underlying model supports them*. Not every open-source model supports tool calls; check the model card.
- **Anthropic surface** — `tools`, `tool_choice`, vision content blocks via `image` source — same caveat: support depends on the model.
If a feature isn't supported by the model, the call either errors or silently degrades. Test against your chosen model rather than assuming parity with first-party OpenAI/Anthropic models.
## Error responses
The two surfaces use different error envelopes — OpenAI's `{ "error": { ... } }` and Anthropic's `{ "type": "error", "error": { type, message } }` — and slightly different status-code surfaces.
### OpenAI surface (`/v1/*`)
| Status | Meaning | Action |
|---|---|---|
| `400` | Malformed request | Fix the request body |
| `401` | Bad / missing key | Check `Authorization: Bearer ...` and that the key is `akml-...` |
| `402` | **Insufficient credits** | Top up the AkashML account (or enable auto-top-up) |
| `429` | Rate limited (RPM / TPM / concurrent) | Honor `Retry-After` + `X-RateLimit-Reset`; back off |
| `500` | Internal server error | Retry with backoff |
| `504` | No backend available | Retry with backoff; consider a fallback model |
| `529` | No healthy backends for this model | Retry with backoff; consider a fallback model |
### Anthropic surface (`/anthropic/*`)
Anthropic responses carry a typed error category in `error.type`. Status → `error.type`:
| Status | `error.type` |
|---|---|
| `400` | `invalid_request_error` |
| `401` | `authentication_error` |
| `403` | `permission_error` |
| `404` | `not_found_error` |
| `413` | `request_too_large` |
| `429` | `rate_limit_error` (response includes `Retry-After`) |
| `500` | `api_error` |
| `503` / `529` | `overloaded_error` |
### Rate-limit response headers
Both surfaces return these when limiting kicks in:
| Header | Meaning |
|---|---|
| `Retry-After` | Seconds to wait before retrying |
| `X-RateLimit-Limit` | Total requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset` | Unix timestamp when the limit resets |
Prefer `Retry-After` for backoff timing; fall back to `X-RateLimit-Reset` if `Retry-After` is missing.
## Health probes
Both surfaces expose informal health endpoints (best-effort, not contractual):
- `GET https://api.akashml.com/anthropic/v1/models` — unauthenticated reachability check (returns `200` with no key)
- `GET https://api.akashml.com/anthropic` — base reachability check
For a real smoke test that also validates auth, do `GET /v1/models` with your key — that endpoint requires a Bearer key (`401` without), so it checks both reachability and authentication.
## Related files
- **[@overview.md](overview.md)** — Surface selection, when to use AkashML
- **[@authentication.md](authentication.md)** — Key creation, env-var conventions, CI/CD
- **[@quickstart.md](quickstart.md)** — Step-by-step first call
- **[@claude-code-integration.md](claude-code-integration.md)** — Anthropic-surface env-var setup for Claude Code
rules/deploy/akashml/authentication.md
# AkashML Authentication
AkashML uses **API keys** for all programmatic access. Both the OpenAI- and Anthropic-compatible surfaces accept the same key in an `Authorization: Bearer` header.
| Surface | Header | Example |
|---|---|---|
| OpenAI (`/v1/*`) | `Authorization: Bearer <key>` | `Authorization: Bearer akml-...` |
| Anthropic (`/anthropic/*`) | `Authorization: Bearer <key>` | `Authorization: Bearer akml-...` |
There is no `x-api-key` header on AkashML. (This differs from the Akash **Console API**, which uses `x-api-key` — see [@../console-api/authentication.md](../console-api/authentication.md). Different platform, different key, different header.)
## Getting an API key
1. Sign up at [playground.akashml.com](https://playground.akashml.com).
2. Verify your email.
3. Add a payment method (Settings → Billing). Trial credits unlock after verification.
4. Generate an API key under **Settings → API Keys**. Copy it immediately — the plaintext value is shown exactly once.
Keys begin with the prefix `akml-`. The prefix is a quick sanity check that you have the right kind of key in hand (it is *not* a Console API key).
## Using the key
Always reference the key via an environment variable. The canonical name in this skill is `AKASHML_API_KEY` — distinct from `AKASH_API_KEY` (which belongs to the Console API).
**curl (OpenAI surface):**
```bash
curl https://api.akashml.com/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY"
```
**curl (Anthropic surface):**
```bash
curl https://api.akashml.com/anthropic/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY"
```
**Python (`openai` SDK):**
```python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["AKASHML_API_KEY"],
base_url="https://api.akashml.com/v1",
)
```
**Python (`anthropic` SDK):**
```python
import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["AKASHML_API_KEY"],
base_url="https://api.akashml.com/anthropic",
)
```
**Node (`openai` SDK):**
```typescript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.AKASHML_API_KEY,
baseURL: "https://api.akashml.com/v1",
});
```
**Node (`@anthropic-ai/sdk`):**
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.AKASHML_API_KEY,
baseURL: "https://api.akashml.com/anthropic",
});
```
The only thing that changes from a stock OpenAI/Anthropic setup is `baseURL`. Everything else — request shape, response shape, streaming, tool use — follows the upstream SDK contract.
## Key handling rules (apply silently)
These are the same rules as Console API in [@../../../SKILL.md](../../../SKILL.md) "API key handling" — applied identically to AkashML keys:
1. **Always reference the key via env var** in code: `$AKASHML_API_KEY` / `process.env.AKASHML_API_KEY` / `os.environ["AKASHML_API_KEY"]` / `os.Getenv("AKASHML_API_KEY")`.
2. **Never echo a literal key** in code or chat, even if the user pasted one. Replace with `$AKASHML_API_KEY` and add one sentence: *"I've used `$AKASHML_API_KEY` in the code below — export your key as that env var before running."*
### When to add extra guidance (one of these, pick the smallest)
- User pastes a literal key → one-sentence redirect to env var. Don't lecture.
- User asks where to put the key for a specific runtime (GitHub Actions, Docker, etc.) → answer for that runtime in one line.
- User writing a Dockerfile → pass at runtime (`-e`), not baked in via `ARG`/`ENV`. One sentence.
- User mentions `.env` → one-line `.gitignore` reminder.
### Stay silent when
- User already said the key is in `$AKASHML_API_KEY` / `${{ secrets.AKASHML_API_KEY }}` — they've got it.
- The conversation has moved past auth (model selection, prompt engineering, response handling).
## Per-key limits
Each key carries configurable limits, set in the AkashML dashboard, **not** via API:
| Limit | Meaning |
|---|---|
| **RPM** | Requests per minute |
| **TPM** | Tokens per minute (input + output) |
| **Concurrent** | Simultaneous in-flight requests |
| **Expiration** | Optional ISO 8601 date when the key stops working |
Exceeding any rate limit returns `429 Too Many Requests`. Inspect the response headers:
| Header | Meaning |
|---|---|
| `Retry-After` | Seconds to wait before retrying |
| `X-RateLimit-Limit` | Total requests allowed in the window |
| `X-RateLimit-Remaining` | Requests remaining |
| `X-RateLimit-Reset` | Unix timestamp when the limit resets |
```typescript
async function callWithRetry(fn: () => Promise<Response>, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fn();
if (response.status !== 429) return response;
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "5", 10);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
throw new Error("Max retries exceeded");
}
```
## Storage and rotation
- **Do:** env vars, secrets manager, encrypted at rest.
- **Don't:** hardcode in source, commit to git, log.
- **Rotate** by creating a new key in the dashboard, deploying it, then deleting the old one.
- Use **separate keys per environment** (dev / staging / prod) so you can revoke without taking down everything.
## CI/CD examples
**GitHub Actions:**
```yaml
env:
AKASHML_API_KEY: ${{ secrets.AKASHML_API_KEY }}
steps:
- name: Run inference smoke test
run: |
curl -fsS https://api.akashml.com/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY" > /dev/null
```
**GitLab CI:**
```yaml
smoke-test:
variables:
AKASHML_API_KEY: $AKASHML_API_KEY # configured in CI/CD settings (mask + protect)
script:
- |
curl -fsS https://api.akashml.com/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY" > /dev/null
```
**Docker (runtime, not build):**
```bash
docker run -e AKASHML_API_KEY=$AKASHML_API_KEY my-inference-image
```
Never bake the key in via `ARG` / `ENV` — it survives in image layer history.
## Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
| `401 Unauthorized` | Missing or malformed key | Confirm `Authorization: Bearer <key>` (not `x-api-key`) |
| `401 Unauthorized` | Used a Console API key (`AKASH_API_KEY`) instead of an AkashML key (`AKASHML_API_KEY`) | They are different services; use the `akml-...` key |
| `402 Payment Required` | **Insufficient credits** on the AkashML account | Top up in the dashboard, or enable auto-top-up |
| `403 Forbidden` | Key revoked or model not permitted for this key | Check the AkashML dashboard |
| `429 Too Many Requests` | Hit RPM/TPM/concurrent limit | Honor `Retry-After`; consider higher limit in dashboard |
| Streaming hangs | Client timeout too low for long generations | Raise client timeout (Claude Code uses `API_TIMEOUT_MS=3000000` — 50 minutes) |
> **Capture `Inference-Id`.** Every successful inference response includes an `Inference-Id` header. Log it on the client side; AkashML support will ask for it when debugging a specific call.
## Related files
- **[@overview.md](overview.md)** — Base URLs, when to use, model catalog notes
- **[@api-reference.md](api-reference.md)** — Full endpoint reference
- **[@quickstart.md](quickstart.md)** — End-to-end first call
- **[@account-and-billing.md](account-and-billing.md)** — Credits, top-up, rate-limit semantics
- **[@claude-code-integration.md](claude-code-integration.md)** — Special env-var setup for Claude Code
rules/deploy/akashml/claude-code-integration.md
# Using AkashML as the Backend for Claude Code
Because the AkashML Anthropic-compatible surface speaks the Anthropic Messages API, you can point Claude Code itself at AkashML by setting two environment variables. Claude Code will then route its requests to open-source models running on Akash compute instead of (or alongside) Anthropic's first-party API.
This file documents that integration specifically. For the generic Anthropic API surface (any client), see [@api-reference.md](api-reference.md).
> Caveat: open-source models behind the Anthropic-compatible surface are not Anthropic's Claude models. Capability, latency, and tool-use compliance differ. Treat this as a separate backend with its own characteristics, not a drop-in for first-party Claude.
## Required env vars
| Variable | Value | Purpose |
|---|---|---|
| `ANTHROPIC_BASE_URL` | `https://api.akashml.com/anthropic` | Redirect Claude Code's Anthropic client to AkashML |
| `ANTHROPIC_AUTH_TOKEN` | `akml-...` (your AkashML key) | Auth credential (Bearer-style) |
## Optional model mappings
Claude Code asks for `sonnet`/`opus`/`haiku` tiers internally. Map each tier to a concrete AkashML model so requests resolve to the right open-source model:
| Variable | Suggested model (verify against `/anthropic/v1/models`) | Why |
|---|---|---|
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | `deepseek-ai/DeepSeek-V4-Flash` | Fast general-purpose coding model |
| `ANTHROPIC_DEFAULT_OPUS_MODEL` | `moonshotai/Kimi-K2.6` | Larger reasoning-focused model for harder tasks |
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `Qwen/Qwen3.5-35B-A3B` | Lightweight for quick lookups |
These values match the AkashML guide. Use the slashed (canonical) form here even though `GET /anthropic/v1/models` returns IDs in the `--` form — the env vars are interpreted by Claude Code before the API call, and Claude Code accepts the slashed AkashML model IDs as documented.
> **Always verify with the live API before pinning.** Run `curl -H "Authorization: Bearer $AKASHML_API_KEY" https://api.akashml.com/v1/models | jq '.data[] | {id, context_length, pricing, supported_features}'` and pick models whose `context_length`, `supported_features` (tools, streaming, reasoning), and `pricing` match your needs. The four IDs above are starting suggestions, not commitments — the AkashML catalog and their recommended mappings change.
## Configuring on macOS / Linux
Add an `env` block to `~/.claude/settings.json`:
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.akashml.com/anthropic",
"ANTHROPIC_AUTH_TOKEN": "akml-...",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-ai/DeepSeek-V4-Flash",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "moonshotai/Kimi-K2.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "Qwen/Qwen3.5-35B-A3B",
"API_TIMEOUT_MS": "3000000"
}
}
```
`API_TIMEOUT_MS` raises Claude Code's per-call timeout. Agentic loops with open-source models can run long; 3,000,000 ms (50 minutes) is what the AkashML docs recommend. Lower it if your workflows are short.
> Reminder: do not commit `~/.claude/settings.json` with the literal key. Either keep the key out of the file and export it as a shell env var (Claude Code reads ambient env vars too), or commit a template with a placeholder and document the export step.
## Configuring on Windows
PowerShell (persistent, user scope):
```powershell
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "https://api.akashml.com/anthropic", "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN", "akml-...", "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_DEFAULT_SONNET_MODEL", "deepseek-ai/DeepSeek-V4-Flash", "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_DEFAULT_OPUS_MODEL", "moonshotai/Kimi-K2.6", "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_DEFAULT_HAIKU_MODEL", "Qwen/Qwen3.5-35B-A3B", "User")
[System.Environment]::SetEnvironmentVariable("API_TIMEOUT_MS", "3000000", "User")
```
Command Prompt (`setx`):
```cmd
setx ANTHROPIC_BASE_URL "https://api.akashml.com/anthropic"
setx ANTHROPIC_AUTH_TOKEN "akml-..."
setx ANTHROPIC_DEFAULT_SONNET_MODEL "deepseek-ai/DeepSeek-V4-Flash"
setx ANTHROPIC_DEFAULT_OPUS_MODEL "moonshotai/Kimi-K2.6"
setx ANTHROPIC_DEFAULT_HAIKU_MODEL "Qwen/Qwen3.5-35B-A3B"
setx API_TIMEOUT_MS "3000000"
```
After `setx`, open a new terminal — `setx` writes to the registry but doesn't update the current process.
## Verifying the redirect
After setting the env vars, start a fresh Claude Code session and make a tiny request (e.g. *"say hi"*). Quick checks:
- Inference latency feels different from first-party Claude (varies by model and provider).
- The AkashML dashboard shows usage tick up.
- Token charges land on the AkashML account, not Anthropic billing.
If usage doesn't appear in the AkashML dashboard, the redirect isn't active — Claude Code probably fell back to first-party Anthropic. Re-check the env vars are present in the process Claude Code actually launched from (a parent shell, `~/.claude/settings.json`, etc.).
## What this changes — and what it doesn't
**Changes:**
- Where requests go (AkashML, not `api.anthropic.com`).
- Who pays (your AkashML credits, not your Anthropic account).
- What model actually runs (open-source on Akash compute).
**Doesn't change:**
- Claude Code's own CLI surface, tool calls, file edits, etc.
- Local files on disk (this is purely a backend redirect).
- Anthropic-specific features Claude Code uses (tool use, system prompts, streaming) — *to the extent the chosen open-source model supports them*. Some models will degrade silently on advanced features.
## Caveats
- **Tool-use parity is not guaranteed.** Open-source models vary widely in how well they follow Anthropic's tool-use schema. Test on real tasks before relying on it for production workflows.
- **Context windows vary.** Each underlying model has its own context limit; Claude Code doesn't know about per-model limits beyond what the API returns. Expect `400` on overlong inputs.
- **No prompt caching guarantees.** AkashML may not implement Anthropic's prompt-cache headers identically. Don't assume cache savings transfer.
## Why this is the most useful integration to document
For most consumers of the Anthropic-compatible surface, generic SDK docs ([@api-reference.md](api-reference.md)) are enough. Claude Code is the exception because the env-var contract is non-obvious, the model-mapping variables are Claude-Code-specific, and the timeout knob matters for agentic workflows in ways stateless inference doesn't surface.
## Related files
- **[@api-reference.md](api-reference.md)** — Generic Anthropic surface (any client)
- **[@authentication.md](authentication.md)** — Key creation, the `akml-` prefix, env-var rules
- **[@account-and-billing.md](account-and-billing.md)** — Credits and per-key limits that apply to Claude Code sessions too
rules/deploy/akashml/overview.md
# AkashML Overview
**AkashML** ([playground.akashml.com](https://playground.akashml.com)) is Akash Network's managed inference platform. It serves open-source AI models behind OpenAI- and Anthropic-compatible REST APIs, with the actual inference running on Akash decentralized GPU compute.
This is a **consumption** path, not a deployment path. You call an LLM endpoint with an API key; you do not write SDL, deposit ACT, or manage a deployment lifecycle. If you want to *host* a model yourself, see [@rules/sdl/examples/gpu-workload.md](../../sdl/examples/gpu-workload.md) and the four deployment methods documented elsewhere in this skill.
> **Always query the live API for catalog/pricing/capabilities.** Model IDs, prices, context lengths, and supported features change. Before recommending a model, quoting a price, or claiming a feature, call `GET /v1/models` (or `GET /anthropic/v1/models`) — the response includes pricing, `context_length`, `supported_features`, `supported_sampling_parameters`, `max_output_length`, and `quantization` per model. The examples in this file are illustrative; the API is authoritative.
## Base URLs
| Surface | Base URL | SDK compatibility |
|---|---|---|
| **OpenAI-compatible** | `https://api.akashml.com/v1` | `openai` (Python, Node), any OpenAI-compatible client — set `baseURL` and go |
| **Anthropic-compatible** | `https://api.akashml.com/anthropic` | `anthropic` (Python, Node), Claude Code via `ANTHROPIC_BASE_URL` |
Both surfaces serve the same underlying model catalog; pick the one that matches the SDK you're already using.
## When to use AkashML vs alternatives
| You want… | Use | Why |
|---|---|---|
| To **call** an LLM (chat, completion, RAG) without owning the box | **AkashML** | Managed, OpenAI/Anthropic-compatible, credit-billed, no SDL |
| To **host** your own model (custom weights, fine-tunes, persistent state) | **SDL + GPU profile** ([gpu-workload.md](../../sdl/examples/gpu-workload.md)) | You control the runtime, weights, persistence; you pay in ACT |
| A web chat UI for an open model (one-off, no integration) | [playground.akashml.com](https://playground.akashml.com) | Browser-only; no code needed |
| Inference *plus* fine-tuning or model upload | Self-deploy via SDL | AkashML does not expose fine-tuning APIs |
If a user says *"I want to run Llama on Akash"* — that's ambiguous. Ask **one** short question: *"Do you want to call a hosted model, or run your own instance?"* — and then commit. The two answers go to completely different files.
## Auth at a glance
- **OpenAI surface:** `Authorization: Bearer $AKASHML_API_KEY`
- **Anthropic surface:** `Authorization: Bearer $AKASHML_API_KEY`
Both surfaces use the same Bearer scheme. The API key format starts with `akml-`. See [@authentication.md](authentication.md) for the full key-management flow.
## Models
The catalog is dynamic — always query `GET /v1/models` (or `GET /anthropic/v1/models`) for the current list rather than hardcoding model IDs in prose. Observed at the time of writing: open-source models from MiniMax, DeepSeek, Moonshot (Kimi), Meta (Llama), and Qwen families.
`GET /v1/models` returns per-model **pricing**, **context length**, **supported features** (chat, tools, streaming, reasoning), and **supported sampling parameters** — read these from the API rather than hardcoding assumptions about a model's capabilities. Note `GET /v1/models` requires the Bearer key (returns `401` without it), whereas the Anthropic surface's `GET /anthropic/v1/models` is reachable unauthenticated — it returns IDs with slashes aliased to `--` (because Claude Code-shaped clients reject slashes); both surfaces accept either form on calls.
See [@api-reference.md](api-reference.md) for the listing call and example response shape.
## Billing — important: not the same as Console API
AkashML billing is **USD credits** on the AkashML account, not on-chain ACT. Do not confuse with Console API (which also accepts USD but converts to `uact` server-side for the on-chain deployment escrow):
| Platform | Billing | Underlying token |
|---|---|---|
| **AkashML** | USD credits via Stripe | Off-chain; AkashML pays providers on your behalf |
| **Console API** | USD via Stripe → managed wallet | On-chain `uact` for deployment escrow |
| **CLI / SDK** | You hold AKT, burn to ACT | On-chain `uact` for deployment escrow |
See [@account-and-billing.md](account-and-billing.md) for the credit model details.
## Stability
AkashML is a production-managed service. Treat it as you would any third-party inference provider:
- **Pin model IDs** in committed code (not "latest"). The catalog can change.
- **Distinguish `402` from `429`.** `402 Payment Required` means you're out of credits — retrying won't help, you have to top up. `429 Too Many Requests` is a transient per-key rate limit; back off and retry.
- **Honor `Retry-After` and `X-RateLimit-*` headers** on `429`.
- **Capture `Inference-Id`** from response headers for support requests.
- **Expect occasional model deprecations** — verify your pinned model is still listed before a release.
## Related files
- **[@authentication.md](authentication.md)** — API key creation, header conventions, env-var rules
- **[@api-reference.md](api-reference.md)** — OpenAI + Anthropic endpoints with curl/SDK examples
- **[@quickstart.md](quickstart.md)** — Linear walkthrough from signup to first inference call
- **[@account-and-billing.md](account-and-billing.md)** — Credits, top-up, rate limits
- **[@claude-code-integration.md](claude-code-integration.md)** — Point Claude Code at AkashML
rules/deploy/akashml/quickstart.md
# AkashML Quickstart — From Signup to First Inference Call
A linear walkthrough for someone starting with zero context: signup → key → first call. We use **curl + Bash** because it's universal — but every example translates directly to any OpenAI- or Anthropic-compatible SDK by changing `baseURL` and leaving everything else alone.
> **Pick a language first if the user has signaled one.** If the user said *"in my Next.js app"* or *"from FastAPI"*, rewrite these steps using the matching SDK before showing them. Don't blindly hand a Bash script to someone integrating from Python.
## What you need
- An AkashML account (free to create).
- A verified email and a payment method on file (unlocks trial credits).
- An API key, stored in `$AKASHML_API_KEY`. **Never paste a literal key inline.**
That's it. No CLI binary, no wallet, no AKT, no SDL.
## Step 1 — Sign up and get an API key
1. Go to [playground.akashml.com](https://playground.akashml.com).
2. Sign up with email; verify your inbox.
3. Add a payment method under **Settings → Billing**. Trial credits unlock once verification + payment-method are both in place.
4. Generate an API key under **Settings → API Keys**. **Copy it immediately** — the plaintext value is shown exactly once. It begins with `akml-`.
## Step 2 — Set the env var
| Where you'll run this | How to set the key |
|---|---|
| **Local shell** (one-off) | `export AKASHML_API_KEY="akml-..."` |
| **Local shell** (persistent) | Add to `~/.zshrc` / `~/.bashrc`, or use `.env` + `direnv` |
| **`.env` file** | `AKASHML_API_KEY=akml-...`; **add `.env` to `.gitignore` immediately** |
| **GitHub Actions** | Settings → Secrets → New secret `AKASHML_API_KEY`; reference as `${{ secrets.AKASHML_API_KEY }}` |
| **Docker** | `docker run -e AKASHML_API_KEY=$AKASHML_API_KEY ...` — runtime only, never `ARG`/`ENV` |
Verify with `echo "${AKASHML_API_KEY:+set}"` — should print `set` without echoing the value.
## Step 3 — List available models
```bash
curl https://api.akashml.com/v1/models \
-H "Authorization: Bearer $AKASHML_API_KEY" \
| jq '.data[].id'
```
You'll see a list of open-source model IDs (DeepSeek, Kimi, MiniMax, Qwen, etc.). Pick one and pin it in the next call.
## Step 4 — Your first inference call
The OpenAI surface is the simplest — drop-in compatible with anything that already speaks OpenAI.
```bash
MODEL="MiniMaxAI/MiniMax-M2.5" # use whatever the list showed
curl -X POST https://api.akashml.com/v1/chat/completions \
-H "Authorization: Bearer $AKASHML_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg model "$MODEL" '{
model: $model,
messages: [
{role: "system", content: "You are a concise assistant."},
{role: "user", content: "What is Akash Network in one sentence?"}
],
max_completion_tokens: 200
}')" \
| jq -r '.choices[0].message.content'
```
If you get a sentence back, you're done — AkashML is wired up. The response carries an `Inference-Id` header you can capture for support requests; not needed for normal use.
## Step 5 — Switch to your SDK
The whole point of the OpenAI/Anthropic-compatible shape: keep your existing client, change one line.
**Python (`openai`):**
```python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["AKASHML_API_KEY"],
base_url="https://api.akashml.com/v1",
)
print(client.chat.completions.create(
model="MiniMaxAI/MiniMax-M2.5",
messages=[{"role": "user", "content": "Hi from Python."}],
).choices[0].message.content)
```
**Node (`openai`):**
```typescript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.AKASHML_API_KEY,
baseURL: "https://api.akashml.com/v1",
});
const r = await client.chat.completions.create({
model: "MiniMaxAI/MiniMax-M2.5",
messages: [{ role: "user", content: "Hi from Node." }],
});
console.log(r.choices[0].message.content);
```
**Python (`anthropic`):**
```python
import os
from anthropic import Anthropic
client = Anthropic(
api_key=os.environ["AKASHML_API_KEY"],
base_url="https://api.akashml.com/anthropic",
)
response = client.messages.create(
model="MiniMaxAI/MiniMax-M2.5", # both surfaces accept the slashed form
max_tokens=256,
messages=[{"role": "user", "content": "Hi from anthropic-py."}],
)
print(response.content[0].text)
```
See [@api-reference.md](api-reference.md) for the full endpoint surface, including streaming, tool use, and the Anthropic-surface specifics.
## What you didn't have to do
- Install any binary
- Write SDL
- Acquire AKT or burn to ACT
- Manage a wallet, mnemonic, or hardware key
- Wait for bids or accept leases
- Deal with `uact` / `uakt` denoms
If a step in your workflow ever requires one of those, you're trying to *host* a model instead of *call* one — and you want [@../../sdl/examples/gpu-workload.md](../../sdl/examples/gpu-workload.md) plus one of the four deployment methods, not AkashML.
## Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
| `401 Unauthorized` | Used `x-api-key` instead of `Authorization: Bearer` | Switch headers — AkashML uses Bearer, not `x-api-key` |
| `401 Unauthorized` | Used a Console API key (`AKASH_API_KEY`) | They're different platforms. Get an `akml-...` key from playground.akashml.com |
| `402 Payment Required` | Out of credits (or trial credits never unlocked) | Verify email + add payment method; top up if you've exhausted credits |
| `404` from Claude Code-shaped client | Slashed model ID rejected client-side | Use the `--` aliased form (`MiniMaxAI--MiniMax-M2.5`); the API itself accepts both |
| `429` Too Many Requests | Hit per-key RPM/TPM/concurrent ceiling | Honor `Retry-After` / `X-RateLimit-Reset`; raise the limit in the dashboard if persistent |
| Hangs forever | Default SDK timeout too low | Raise client timeout for long generations |
## Where to go next
- **[@api-reference.md](api-reference.md)** — Full OpenAI + Anthropic endpoint reference
- **[@authentication.md](authentication.md)** — Key management, rotation, CI/CD wiring
- **[@account-and-billing.md](account-and-billing.md)** — How credits work, auto-top-up, rate limits
- **[@claude-code-integration.md](claude-code-integration.md)** — Use AkashML as the backend for Claude Code itself
rules/deploy/akt/console-workflows.md
# akt CLI: Console workflows
Recipes for a context that deploys through Console, written for akt 1.0.1 and later. Setup, credentials and what differs on 0.1.x are in [overview.md](overview.md). Examples use dseq `1234567` and service `web`; replace both, and add `--context <name>` when the work targets a context other than the current one.
## Write and check the SDL
```bash
akt sdl scaffolds # web, gpu, multi-service, ip-lease
akt sdl init web --image nginx:1.27.3 --port 80 > deploy.yaml
akt sdl validate deploy.yaml -o json # valid, errors, warnings
```
The `sdl` commands run offline and need no context. Validation rejects untagged and `:latest` images. Price in `uact` ([../../sdl/placement-pricing.md](../../sdl/placement-pricing.md)); `uakt` pricing doesn't work on the Console rail.
## Deploy
Preview the plan, then run it:
```bash
akt deploy deploy.yaml --dry-run -o jsonl
akt deploy deploy.yaml --bid-select cheapest --yes -o jsonl
```
- `--bid-select` decides the bid: `cheapest`, or `provider=<full akash1… address>` once a provider has been chosen. The default, `interactive`, waits for a person to pick, so an agent or a CI job always passes it. Choose `cheapest` only when any provider that satisfies the SDL is acceptable; region, audit and hardware requirements belong in the SDL's placement attributes.
- `--yes` skips confirmations. It doesn't choose a bid.
- `--bid-timeout` (default `5m`) bounds the wait for bids and `--ready-timeout` (default `2m`) the wait for the service. `--no-wait-active` returns as soon as the manifest is sent.
- Pass no deposit: 1.0.1 refuses one on Console. On 0.1.x, upgrade (`brew upgrade akash-network/tap/akt`); if that isn't possible, add `--deposit 0.5`.
- The run prints the dseq. With `-o jsonl`, read it from the `create-deployment` record's `outputs.dseq`.
To look at the bids before choosing, run the steps separately: `akt console deployment create deploy.yaml`, then `akt console bid list 1234567 -o json`, then `akt console lease create 1234567 <provider-address>`.
## Secrets
Reference a secret as the whole value of an env entry, or of a registry `credentials` `username` or `password`, and keep the values in a separate JSON or YAML map:
```yaml
# deploy.yaml
env:
- "LOG_LEVEL=info"
- "DATABASE_URL=ac-secret://DATABASE_URL"
```
```yaml
# secrets.yaml, kept out of version control
DATABASE_URL: "postgres://app:<password>@db.internal:5432/app"
```
```bash
akt deploy deploy.yaml --secrets-file secrets.yaml --bid-select cheapest --yes -o jsonl
# or build the map from the environment, so it never touches disk
jq -n '{DATABASE_URL: env.DATABASE_URL}' | akt deploy deploy.yaml --secrets-file - --bid-select cheapest --yes -o jsonl
```
- `--secrets-file -` reads the map from stdin. Never put a value in a flag, a `jq --arg` or the SDL, where shell history and process listings keep it.
- Names are letters, digits and `_`, start with a letter or `_`, and run up to 64 characters. Every reference needs a value, supplied or (on redeploy) inherited, or Console answers 400: [../console-api/secrets.md](../console-api/secrets.md).
- Plain variables stay readable in the saved SDL. `akt console deployment sdl 1234567` prints the saved SDL with every secret still a reference, and no command reads a value back.
- Secrets need a Console context; the chain rail rejects `ac-secret://` and `--secrets-file`.
## Inspect
```bash
akt console deployment list active -o json # or closed; --limit, --skip
akt console deployment get 1234567 -o json # Console's record: leases, escrow
akt console status 1234567 -o json # live from the provider: services, forwarded ports, IPs
akt console logs 1234567 web --tail 50 # no service = all services; --follow streams
akt console events 1234567 -o json # Kubernetes events; --follow streams
akt console shell 1234567 web -- ls -la /app # one command; -o json captures stdout and stderr
```
Logs, events, status and shell go straight to the provider with a short-lived JWT that Console mints; no wallet or certificate is involved. The service is positional (`logs 1234567 web`), not a `--service` flag. Without `--`, `shell` opens an interactive session an agent can't drive, so always give it a command.
## Update
Edit the SDL, validate it, then pass the SDL first and the dseq second. When the original file isn't at hand, start from Console's saved copy:
```bash
akt console deployment sdl 1234567 > deploy.yaml # secrets come back as references
akt sdl validate deploy.yaml
akt update deploy.yaml 1234567 --dry-run -o jsonl
akt update deploy.yaml 1234567 --yes -o jsonl
```
akt compares the file with Console's saved SDL and sends only the difference, on the existing lease. Secrets the file still references keep their stored values. To add or replace one, reference it and pass its value with `--secrets-file`; to remove one, drop its reference.
For a small change, a patch file names only the fields to change:
```yaml
# changes.yaml
services:
web:
image: nginx:1.27.4
env:
LOG_LEVEL: debug
OLD_FLAG: null
```
```bash
akt update changes.yaml 1234567 --patch --yes -o jsonl
akt update empty.yaml 1234567 --patch --secrets-file rotated.yaml --yes -o jsonl # rotate values only; empty.yaml is `{}`
```
- A patch can change `image`, `command`, `args`, `env` (a map here, not the SDL's list), registry `credentials`, existing exposed ports and storage mounts. `null` removes a variable or clears `command`, `args` or `credentials`.
- Groups, compute resources, replica counts, placement, and the kind or number of exposed endpoints can't change in place. akt refuses them before sending anything; use `akt redeploy` below. akt's help text about reopening bids describes the chain rail.
- A version conflict means the deployment changed since akt read it. Fetch the SDL again and review it before retrying.
- `akt console deployment update` takes the arguments the other way round (`<dseq> <file>`) and accepts the same `--patch` and `--secrets-file`.
## Redeploy
```bash
akt redeploy 1234567 --bid-select cheapest --yes -o jsonl
akt redeploy 1234567 --sdl-file bigger.yaml --bid-select cheapest --yes -o jsonl
```
Redeploy creates a new deployment from the source's saved SDL, or from `--sdl-file`, and carries over its stored secrets; `--secrets-file` overrides individual values. The source can already be closed. If it isn't, redeploy leaves it running, so the user pays for both until it's closed. Read the new dseq from the `create-deployment` record, check the new deployment serves, and close the source only if the user asked for that.
## Funding and runtime
Console funds every deployment from the account's credits, so there is nothing to top up.
```bash
akt console wallet balance -o json # available, escrow and total, in USD
akt console deployment settings 1234567 # the deployment's funding record
akt console deployment settings 1234567 24 # set a 24-hour runtime limit; `none` removes it
```
On 0.1.x, `settings` toggles the retired auto top-up instead, and `akt console deployment deposit` calls a deprecated endpoint. Skip both. The runtime-limit rules (how far one request can extend it, the maximum) are in [../console-api/deployment-endpoints.md](../console-api/deployment-endpoints.md).
## Close
```bash
akt close 1234567 --dry-run -o jsonl
akt close 1234567 --yes -o jsonl
```
Closing is irreversible: the workload and its data are gone and the dseq can't be reused. Make sure the dseq is one the user asked to end, then confirm with `akt console deployment get 1234567`.
## Find capacity first
```bash
akt console screen deploy.yaml -o json # providers that could bid on this SDL
akt console screen --cpu 4 --memory 16Gi --gpu 1 --gpu-model h100 -o json
akt console gpu -o json # GPU availability and prices
akt console provider list -o json # provider catalog; also get, regions, auditors
```
These read public data and need no API key.
## When something fails
- Read the error record in the JSONL: `recovery` and `cleanup` hold the commands to run next, and `dseq` and `provider` show what already exists.
- Console can commit a create or a lease and still time out. Don't rerun `akt deploy`. Check `akt console deployment get` and `akt console bid list`, then finish the missing step (`akt console lease create`) or close what was created.
- A lease error saying the manifest wasn't delivered means the lease exists and is being paid for: retry the same `akt console lease create`, or close the deployment. A provider that couldn't be reached before the lease means choosing another bid.
- An update whose outcome is unknown may already be saved on Console. Check `akt console deployment get`, then rerun the identical `akt update` without editing the file in between.
- `akt context log --type workflow -o json` lists recent runs, and `--workflow-id <id>` shows every step of one.
- Console's read is the authority. `akt store status` only shows akt's local record.
## CI
```bash
# AKT_CONSOLE_API_KEY comes from the CI secret store
akt context create ci --deploy-via console --set-current
akt sdl validate deploy.yaml
akt deploy deploy.yaml --bid-select cheapest --yes -o jsonl > deploy.jsonl
dseq=$(jq -r 'select(.step=="create-deployment") | .outputs.dseq' deploy.jsonl)
```
When the SDL references secrets, map them from the CI secret store into the step's `env` too and pipe them in: `jq -n '{DATABASE_URL: env.DATABASE_URL}' | akt deploy deploy.yaml --secrets-file - …`. In GitHub Actions, map the secrets into the step's `env` and append `dseq=$dseq` to `$GITHUB_OUTPUT` for later steps. Install a pinned akt, 1.0.1 or later, in the job (release archive or Homebrew), since the deposit flag and secrets depend on the version.
rules/deploy/akt/overview.md
# akt CLI
`akt` is the unified Akash command-line tool. It replaces the CLI work previously split across `akash`, `provider-services` and the chain SDK CLI, and deploys through either of two rails:
| Rail | Who signs and pays | Credential | Use it when |
|---|---|---|---|
| Console (`console-api`) | Console's managed wallet, from the account's USD credits | Console API key | The user has a Console account and no wallet to manage |
| Chain (`keyring`) | The user's own key, in ACT | Local keyring account | Self-custody |
One context can hold both credentials; its preference picks the rail for `akt deploy`, `akt update`, `akt redeploy` and `akt close`. `akt console …` commands always use Console and `akt tx …` always signs locally.
Official docs: [akash.network/docs/developers/deployment/akt](https://akash.network/docs/developers/deployment/akt/). Source and releases: [github.com/akash-network/akt](https://github.com/akash-network/akt).
## When to use akt
- The user mentions akt, or `command -v akt` finds it, and wants something deployed or inspected from a terminal. Use akt rather than hand-written Console API calls or `provider-services`: one `akt deploy` creates the deployment, waits for bids, picks one, opens the lease, sends the manifest and waits for the service.
- Code that deploys on behalf of an application, or a pipeline that shouldn't install tools, is still better served by the Console API over HTTP: [../console-api/](../console-api/).
- On akt 1.0.1 and later, Console secrets work from the CLI: `ac-secret://NAME` references with values from `--secrets-file`, partial updates with `akt update --patch`, and `akt redeploy`, which carries stored secrets over to a new deployment. 0.1.x and the 1.0.0-rc0 pre-release have none of these, so upgrade first.
## Install
```bash
brew tap akash-network/tap
brew install akash-network/tap/akt # latest stable release (1.0.1 or later)
brew upgrade akash-network/tap/akt # an existing 0.1.x install
akt version
```
Homebrew ships stable releases; 1.0.1 is the first stable 1.0. Every release is also on [GitHub Releases](https://github.com/akash-network/akt/releases) (a universal macOS zip, Linux amd64 and arm64 zips, `.deb` and `.rpm`), which is the way to pin a version in CI; check the archive against the release's `akt_<version>_checksums.txt`. Full steps: [installation docs](https://akash.network/docs/developers/deployment/akt/installation/).
akt has its own agent skill, `akt-cli`, about driving the binary itself: every command family, context inspection, recovery. Take the `akt_<version>_skill.zip` from the release that matches `akt version`; the copy at `https://akash.network/skills/akt-cli.zip` can trail the latest release. Extract the whole `akt-cli/` folder into the agent's skills directory (`~/.claude/skills/` for Claude Code, `~/.agents/skills/` for Codex).
## Check the version first
Console-rail behavior changed between releases. Run `akt version` before giving commands:
| | akt 0.1.x | akt 1.0.1 and later |
|---|---|---|
| Deposit on a Console deploy | Required: `--deposit 0.5` or more, in USD (`0.5`, `0.5usd`, `$0.50`). Console ignores the amount and funds the deployment itself | Refused: any deposit on the Console rail is an error, so pass none |
| `akt console deployment create` | Takes a USD deposit argument | No deposit argument; takes `--secrets-file` |
| `akt console deployment settings <dseq>` | `true` or `false` toggles the old auto top-up | `<hours>` sets a runtime limit, `none` removes it |
| `akt console deployment deposit` | Calls an endpoint Console has deprecated; don't use it | Removed |
| Secrets (`ac-secret://`, `--secrets-file`) | Absent | On `deploy`, `update`, `redeploy` and `akt console deployment create` and `update` |
| `akt update` on Console | Resubmits the whole SDL; a resource change fails with a 422 | Sends only what changed and keeps stored secrets; `--patch` takes a patch file; a resource change is refused before sending |
| `akt redeploy`, `akt console deployment sdl` | Absent | Present |
| `akt console lease create` | Sends the manifest cached by `deployment create`, so it only works on the same machine | Console builds the manifest from its saved definition, on any machine |
| `akt console screen --gpu-model` | Ignored, so every model returns the same providers | Filters by model |
| `akt sdl init --architecture` | Absent | `amd64` or `arm64` |
The 1.0.0-rc0 pre-release matches 1.0.1 on deposits, settings, screening and architecture, but has none of the secret, patch or redeploy features and still caches the manifest. When an example here and the installed binary disagree, `akt <command> --help` wins.
## Set up a Console context
A context holds the network, keyring, Console credential, local deployment store and action log. In a terminal, the first `akt` run starts a setup wizard. Without a terminal (an agent's shell, CI) akt never prompts: it reports that no configuration exists and the command fails. Create the context explicitly:
```bash
akt context create ci --deploy-via console --set-current
```
`--deploy-via console` needs no network and no keyring. `akt deploy`, `update` and `close` refuse to run without a context, even when a key is available.
The API key resolves in this order: the `--console-api-key` flag, then `AKT_CONSOLE_API_KEY`, then the key stored with the context.
| Situation | Do this |
|---|---|
| CI, agents, anything non-interactive | Export `AKT_CONSOLE_API_KEY` from the secret store; nothing is written to disk |
| A person at a terminal | `akt console login` asks for the key with hidden input and stores it for the context (file mode 0600, never in `config.yaml`) |
Keep the key off the command line (`--console-api-key <key>`, `akt console login <key>`), where shell history and process listings keep it. Keys are created in Console under Settings → API Keys and start with `ac.sk.`.
Confirm before acting:
```bash
akt console whoami -o json
akt context show -o json # auth_method, console_api_key_configured, capabilities
```
`capabilities.console: true` only means a key resolves; `whoami` proves it works. A Console-only context reports `chain_query`, `chain_tx` and `provider` as false, and commands that need them answer "unavailable" with the missing setting.
## Chain rail (self-custody)
```bash
akt context network list
akt context create mine --network mainnet --default-account alice --set-current
```
If the network isn't listed, add it with `akt context network create` (see its `--help`). The account must already exist in the context's keyring (`akt context keys --help`); creating or importing a key is a separate task that produces sensitive backup material. On this rail `akt deploy` defaults to `--deposit auto`, the chain minimum in the network's deposit denomination, and the wallet pays deposits and fees, so it needs ACT minted from AKT first ([../../pricing.md](../../pricing.md)). Chain queries, transactions and provider-gateway commands live under `akt query`, `akt tx` and `akt provider`.
## Machine-readable output
- Use `-o json` for reads and `-o jsonl` for `deploy`, `update`, `redeploy` and `close`, which write one record per workflow step.
- Each JSONL record has `workflow`, `id`, `step`, `result` (`completed`, `skipped` or `error`; `planned` under `--dry-run`), `errors` and `txs`, plus the step's `outputs`. The new dseq is in the `create-deployment` record: `jq -r 'select(.step=="create-deployment") | .outputs.dseq'`. A failed step can also carry `dseq`, `provider`, `recovery` and `cleanup`, the last two being commands to run next.
- Check the exit status too. `akt sdl validate` exits 0 when valid, 1 when invalid and 2 when it can't read the file.
- `akt sdl init` writes raw YAML: redirect it to a file and don't pass `-o`.
- Pretty output shows amounts in dollars. JSON keeps micro-denominations (`uact`), so convert before quoting a price.
## MCP server
`akt mcp` serves Akash tools over stdio: chain reads when the context has an RPC endpoint, and Console reads (deployments, bids, providers, GPU prices, balance, usage) when a Console key resolves. It starts read-only. `--enable-writes` adds closing deployments, creating and closing leases and submitting manifests, and needs an explicitly selected context so every mutation is logged.
The user stores the key once at a terminal, then registers the server:
```bash
akt console login --context ops
claude mcp add akt -- akt mcp --context ops
```
Add `--enable-writes` to the second command only if the user wants the agent to close deployments or create leases for them. JSON-configured clients use `{"command": "akt", "args": ["mcp", "--context", "ops"]}`.
## How akt maps to the Console API
akt calls the endpoints documented in [../console-api/](../console-api/). On 1.0.1:
- `akt deploy`, `akt redeploy` and `akt console deployment create` send `POST /v1/deployments` with a `sealedSecrets` JWE built on the user's machine from `GET /v1/sdl-secrets-context` ([../console-api/secrets.md](../console-api/secrets.md)). akt seals an empty map when there are no secrets, so plain variables stay readable and only `ac-secret://` values are stored as secrets. `akt redeploy` adds `inheritSecretsFrom`.
- `akt update` and `akt console deployment update` diff the file against Console's saved SDL and send the difference through `PATCH /v1/deployments/{dseq}` with `ifManifestVersion`. Only a deployment Console holds no definition for falls back to the full-SDL `PUT`.
- `akt deploy` and `akt console lease create` send no manifest to `POST /v1/leases`; Console builds it. `--manifest` is only for a deployment with no saved definition.
akt 0.1.x resubmits the whole SDL through the deprecated `PUT` and sends a cached manifest with every lease.
Recipes for the Console rail: [console-workflows.md](console-workflows.md).
rules/deploy/cli/common-commands.md
# CLI Command Reference
Quick reference for common Akash CLI commands.
## Configuration
There is no `config` subcommand. Configure defaults with environment variables and/or pass the corresponding flags (`--node`, `--chain-id`, `--keyring-backend`, `--gas-prices`) on each command.
```bash
# Set node
export AKASH_NODE=https://rpc.akashnet.net:443
# Set chain ID
export AKASH_CHAIN_ID=akashnet-2
# Set keyring backend
export AKASH_KEYRING_BACKEND=os
# Set gas prices
export AKASH_GAS_PRICES=0.025uakt
```
Or pass the equivalent flags per command:
```bash
provider-services <command> \
--node https://rpc.akashnet.net:443 \
--chain-id akashnet-2 \
--keyring-backend os \
--gas-prices 0.025uakt
```
## Keys (Wallet)
```bash
# Create wallet
provider-services keys add wallet
# Import from mnemonic
provider-services keys add wallet --recover
# List wallets
provider-services keys list
# Show address
provider-services keys show wallet -a
# Delete wallet
provider-services keys delete wallet
# Export key
provider-services keys export wallet > wallet.key
```
## Balance & Transfers
```bash
# Check balance
provider-services query bank balances <ADDRESS>
# Send tokens
provider-services tx bank send <FROM_ADDRESS> <TO_ADDRESS> 1000000uakt --from wallet
```
## Certificates
```bash
# Create certificate (two steps: generate then publish)
provider-services tx cert generate client --from wallet
provider-services tx cert publish client --from wallet
# List certificates
provider-services query cert list --owner <ADDRESS>
# Revoke certificate
provider-services tx cert revoke --from wallet
```
## Deployments
```bash
# Create deployment
provider-services tx deployment create deploy.yaml --from wallet
# Create with deposit
provider-services tx deployment create deploy.yaml --deposit 10000000uact --from wallet
# List deployments
provider-services query deployment list --owner <ADDRESS>
# Get deployment
provider-services query deployment get --owner <ADDRESS> --dseq <DSEQ>
# Update deployment
provider-services tx deployment update deploy.yaml --dseq <DSEQ> --from wallet
# Deposit funds
provider-services tx deployment deposit 5000000uact --dseq <DSEQ> --from wallet
# Close deployment
provider-services tx deployment close --dseq <DSEQ> --from wallet
```
## Market (Bids & Leases)
```bash
# List bids
provider-services query market bid list --owner <ADDRESS> --dseq <DSEQ>
# Create lease (accept bid)
provider-services tx market lease create \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
# List leases
provider-services query market lease list --owner <ADDRESS>
# Get lease
provider-services query market lease get \
--owner <ADDRESS> \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER>
# Close lease
provider-services tx market lease close \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--from wallet
```
## Provider Operations
```bash
# Send manifest
provider-services send-manifest deploy.yaml \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
# Lease status
provider-services lease-status \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
# Lease logs
provider-services lease-logs \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
# Follow logs
provider-services lease-logs \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet \
--follow
# Specific service logs
provider-services lease-logs \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet \
--service web
# Interactive shell
provider-services lease-shell \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet \
--service web \
-- /bin/sh
# Events
provider-services lease-events \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
```
## Queries
```bash
# Network status
provider-services status
# Current block height
provider-services query block
# Provider list
provider-services query provider list
# Provider details
provider-services query provider get <PROVIDER_ADDRESS>
# Auditor list
provider-services query audit list
```
## Transaction Flags
Common flags for transactions:
```bash
# Auto gas estimation
--gas auto
# Gas adjustment
--gas-adjustment 1.5
# Explicit gas
--gas 200000
# Gas prices
--gas-prices 0.025uakt
# Skip confirmation
-y
# Output format
--output json
# Broadcast mode
--broadcast-mode sync # or async, block
```
## Environment Variables
```bash
export AKASH_NODE="https://rpc.akashnet.net:443"
export AKASH_CHAIN_ID="akashnet-2"
export AKASH_KEYRING_BACKEND="os"
export AKASH_FROM="wallet"
export AKASH_GAS="auto"
export AKASH_GAS_ADJUSTMENT="1.5"
export AKASH_GAS_PRICES="0.025uakt"
export AKASH_OUTPUT="json"
```
## Shortcut Aliases
Add to `~/.bashrc` or `~/.zshrc`:
```bash
# Quick address
alias akaddr='provider-services keys show wallet -a'
# Quick balance
alias akbal='provider-services query bank balances $(provider-services keys show wallet -a)'
# List my deployments
alias akdeps='provider-services query deployment list --owner $(provider-services keys show wallet -a)'
# List my leases
alias akleases='provider-services query market lease list --owner $(provider-services keys show wallet -a)'
# Quick deploy
akdeploy() {
provider-services tx deployment create "$1" --from wallet -y
}
# Quick close
akclose() {
provider-services tx deployment close --dseq "$1" --from wallet -y
}
```
## Output Parsing
```bash
# Get address
ADDRESS=$(provider-services keys show wallet -a)
# Get deployment dseq from create
DSEQ=$(provider-services tx deployment create deploy.yaml --from wallet -y --output json | jq -r '.events[] | select(.type=="akash.v1") | .attributes[] | select(.key=="dseq") | .value')
# Get first provider from bids
PROVIDER=$(provider-services query market bid list --owner $ADDRESS --dseq $DSEQ --output json | jq -r '.bids[0].bid.id.provider')
# Get service URIs
provider-services lease-status ... --output json | jq -r '.services.web.uris[]'
```
## Debug Options
```bash
# Verbose output
provider-services tx deployment create deploy.yaml --from wallet --log_level debug
# Dry run (simulate)
provider-services tx deployment create deploy.yaml --from wallet --dry-run
# Print transaction without broadcasting
provider-services tx deployment create deploy.yaml --from wallet --generate-only
```
## Help
```bash
# General help
provider-services --help
# Command help
provider-services tx deployment --help
provider-services tx deployment create --help
# Query help
provider-services query --help
provider-services query deployment --help
```
rules/deploy/cli/deployment-lifecycle.md
# Deployment Lifecycle
Complete guide to creating, managing, and closing deployments via CLI.
## Deployment Flow Overview
```
1. Create SDL file
2. Create certificate (first time)
3. Create deployment
4. Wait for bids
5. Accept bid (create lease)
6. Send manifest
7. Access deployment
8. Close deployment (when done)
```
## Prerequisites
```bash
# Verify CLI setup
provider-services version
# Check wallet
provider-services keys show wallet -a
# Check balance (need ~5 AKT minimum)
provider-services query bank balances $(provider-services keys show wallet -a)
```
## Step 1: Create SDL File
Create `deploy.yaml`:
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
## Step 2: Create Certificate (First Time Only)
Generate and broadcast client certificate (two steps):
```bash
provider-services tx cert generate client --from wallet
provider-services tx cert publish client --from wallet
```
Verify certificate:
```bash
provider-services query cert list --owner $(provider-services keys show wallet -a)
```
## Step 3: Create Deployment
```bash
provider-services tx deployment create deploy.yaml --from wallet
```
Capture the deployment sequence (dseq) from output:
```bash
# Or query your deployments
provider-services query deployment list --owner $(provider-services keys show wallet -a)
```
### With Explicit Deposit
```bash
provider-services tx deployment create deploy.yaml --deposit 10000000uact --from wallet
```
## Step 4: Wait for Bids
Query bids for your deployment:
```bash
provider-services query market bid list --owner $(provider-services keys show wallet -a) --dseq <DSEQ>
```
Wait until bids appear (usually 15-60 seconds).
Example output:
```json
{
"bids": [
{
"bid": {
"id": {
"owner": "akash1...",
"dseq": "12345678",
"gseq": 1,
"oseq": 1,
"provider": "akash1provider..."
},
"price": {
"denom": "uact",
"amount": "950"
},
"state": "open"
}
}
]
}
```
## Step 5: Accept Bid (Create Lease)
```bash
provider-services tx market lease create \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet
```
Verify lease:
```bash
provider-services query market lease list --owner $(provider-services keys show wallet -a) --dseq <DSEQ>
```
## Step 6: Send Manifest
```bash
provider-services send-manifest deploy.yaml \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet
```
## Step 7: Access Deployment
### Get Lease Status
```bash
provider-services lease-status \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet
```
Output includes service URIs:
```json
{
"services": {
"web": {
"name": "web",
"available": 1,
"total": 1,
"uris": [
"abc123.provider.akash.network"
]
}
}
}
```
### Get Logs
```bash
provider-services lease-logs \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet
```
### Follow Logs
```bash
provider-services lease-logs \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet \
--follow
```
### Interactive Shell
```bash
provider-services lease-shell \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet \
--service web \
-- /bin/sh
```
## Step 8: Close Deployment
When done, close to stop billing and reclaim escrow:
```bash
provider-services tx deployment close --dseq <DSEQ> --from wallet
```
Verify closed:
```bash
provider-services query deployment get --owner $(provider-services keys show wallet -a) --dseq <DSEQ>
```
## Resource Reclamation Lifecycle (v2.1.0+)
When a deployment declares a [`reclamation` block](../../sdl/reclamation.md), a
**provider** may reclaim the lease's resources — but only after honoring the
tenant's grace window. This is opt-in: deployments without a `reclamation` block,
and all SDL 2.0 deployments, never enter this flow.
```
Active ──MsgLeaseStartReclaim (provider)──▶ Reclaiming
│ deadline = block_time + window
│
provider close BEFORE deadline ──▶ rejected
provider close AFTER deadline ──▶ group PAUSED (no auto-re-order)
```
1. **Provider starts reclamation** — the provider signs `MsgLeaseStartReclaim` on an
`Active` lease with a `reason` (a `LeaseClosedReason` in the provider range
10000–19999). The lease moves to `Reclaiming` and a deadline is set at
`block_time + window`.
2. **Tenant reacts within the window** — drain, snapshot, or migrate. The provider
**cannot** close the lease before the deadline; an early close is rejected.
3. **After the deadline** — the provider may close the lease. The group is left
**paused**: there is **no automatic re-order**. Recover by resuming the group
(`MsgStartGroup`) or by closing and redeploying.
### Tenant-initiated vs provider-initiated close
| Action | Effect |
|--------|--------|
| `MsgLeaseStartReclaim` (provider) | Starts the window; close only allowed after the deadline; post-deadline close pauses the group. |
| `MsgCloseLease` (tenant, single lease) | Allowed anytime; **auto-re-orders** — the new order inherits the deployment's reclamation requirement. |
| `MsgCloseDeployment` (tenant, whole deployment) | Escrow-level close; **bypasses** the reclamation window. |
### Recovering a paused group
A paused group does not re-order on its own. Resume it with `MsgStartGroup` (new
orders inherit the reclamation requirement), or close the deployment and redeploy.
Reclamation of a lease is terminal — there is no "restart" of the reclaimed lease
itself.
See [terminology.md](../../terminology.md#resource-reclamation-aep-82) for the
message and state glossary.
## Managing Deployments
### Add Funds
```bash
provider-services tx deployment deposit 5000000uact --dseq <DSEQ> --from wallet
```
### Update Deployment
```bash
provider-services tx deployment update deploy-updated.yaml --dseq <DSEQ> --from wallet
```
Then send updated manifest:
```bash
provider-services send-manifest deploy-updated.yaml \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER_ADDRESS> \
--from wallet
```
### Close Specific Lease
```bash
provider-services tx market lease close \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--from wallet
```
## Scripting Deployments
### Bash Script Example
```bash
#!/bin/bash
set -e
SDL_FILE="deploy.yaml"
WALLET="wallet"
# Get address
ADDRESS=$(provider-services keys show $WALLET -a)
# Create deployment
echo "Creating deployment..."
TX_RESULT=$(provider-services tx deployment create $SDL_FILE --from $WALLET -y --output json)
DSEQ=$(echo $TX_RESULT | jq -r '.events[] | select(.type=="akash.v1") | .attributes[] | select(.key=="dseq") | .value')
echo "Deployment DSEQ: $DSEQ"
# Wait for bids
echo "Waiting for bids..."
sleep 30
# Get first bid
BID=$(provider-services query market bid list --owner $ADDRESS --dseq $DSEQ --output json | jq -r '.bids[0]')
PROVIDER=$(echo $BID | jq -r '.bid.id.provider')
echo "Selected provider: $PROVIDER"
# Create lease
echo "Creating lease..."
provider-services tx market lease create \
--dseq $DSEQ \
--gseq 1 \
--oseq 1 \
--provider $PROVIDER \
--from $WALLET \
-y
# Wait for lease
sleep 10
# Send manifest
echo "Sending manifest..."
provider-services send-manifest $SDL_FILE \
--dseq $DSEQ \
--gseq 1 \
--oseq 1 \
--provider $PROVIDER \
--from $WALLET
# Wait for deployment
sleep 15
# Get status
echo "Getting status..."
provider-services lease-status \
--dseq $DSEQ \
--gseq 1 \
--oseq 1 \
--provider $PROVIDER \
--from $WALLET
echo "Deployment complete! DSEQ: $DSEQ"
```
## Troubleshooting
### No Bids Received
- Check pricing in SDL (may be too low)
- Verify SDL syntax
- Check provider availability
### Manifest Send Failed
- Verify certificate is valid
- Check provider is online
- Retry after a few seconds
### Deployment Not Starting
```bash
# Check lease status
provider-services lease-status --dseq <DSEQ> ...
# Check logs for errors
provider-services lease-logs --dseq <DSEQ> ...
```
### Escrow Depleted
```bash
# Check escrow balance
provider-services query deployment get --dseq <DSEQ> --owner $(provider-services keys show wallet -a)
# Deposit more funds
provider-services tx deployment deposit 5000000uact --dseq <DSEQ> --from wallet
```
rules/deploy/cli/installation.md
# Akash CLI Installation
Install the `provider-services` CLI for command-line deployments and provider interaction.
The official docs now point new CLI users to `akt`, which covers these self-custody flows on a keyring context and adds Console-backed deploys: see [../akt/overview.md](../akt/overview.md). This folder documents `provider-services` for existing scripts.
## Quick Install
### Linux/macOS (Recommended)
```bash
curl -sfL https://raw.githubusercontent.com/akash-network/provider/main/install.sh | bash
sudo mv ./bin/provider-services /usr/local/bin/
```
Or install straight to a directory:
```bash
curl -sfL https://raw.githubusercontent.com/akash-network/provider/main/install.sh | sudo bash -s -- -b /usr/local/bin
```
### Homebrew (macOS)
```bash
brew tap akash-network/tap
brew install akash-provider-services
```
### From Binary
This resolves the latest stable release automatically — no version to keep in sync.
(`/releases/latest` skips pre-releases/rc builds. Requires `curl` + `jq`.)
```bash
# Resolve the latest stable tag (e.g. v0.12.0) and the bare number (0.12.0)
VERSION=$(curl -s https://api.github.com/repos/akash-network/provider/releases/latest | jq -r .tag_name)
NUM=${VERSION#v}
# Pick your platform asset
ASSET="provider-services_${NUM}_linux_amd64.zip" # Linux AMD64
# ASSET="provider-services_${NUM}_linux_arm64.zip" # Linux ARM64
# ASSET="provider-services_${NUM}_darwin_all.zip" # macOS (universal)
wget "https://github.com/akash-network/provider/releases/download/${VERSION}/${ASSET}"
unzip "$ASSET"
sudo mv provider-services /usr/local/bin/
```
To pin a specific release instead, set `VERSION=v0.12.0` (and `NUM=0.12.0`) manually
— see [GitHub Releases](https://github.com/akash-network/provider/releases).
### From Source
```bash
git clone https://github.com/akash-network/provider
cd provider
make deps-install
make install
```
## Verify Installation
```bash
provider-services version
```
## Initial Configuration
`provider-services` has no `config` subcommand. Configure it with environment variables and/or per-command flags.
### Environment Variables
```bash
export AKASH_NODE="https://rpc.akashnet.net:443"
export AKASH_CHAIN_ID="akashnet-2"
export AKASH_KEYRING_BACKEND="os" # use "test" for development (keys stored unencrypted)
export AKASH_GAS_PRICES="0.025uakt"
```
Add to `~/.bashrc` or `~/.zshrc` for persistence.
### Per-command Flags
Alternatively, pass values on each command:
```bash
--node https://rpc.akashnet.net:443
--chain-id akashnet-2
--keyring-backend os
--gas-prices 0.025uakt
```
## Provider Interaction
Provider communication is handled by `provider-services` itself (installed above):
```bash
curl -sfL https://raw.githubusercontent.com/akash-network/provider/main/install.sh | bash
sudo mv ./bin/provider-services /usr/local/bin/
```
This enables:
- Lease log streaming
- Interactive shell access
- Service status queries
## Shell Completion
### Bash
```bash
provider-services completion bash > /etc/bash_completion.d/provider-services
```
### Zsh
```bash
provider-services completion zsh > "${fpath[1]}/_provider-services"
```
### Fish
```bash
provider-services completion fish > ~/.config/fish/completions/provider-services.fish
```
## Troubleshooting
### Command Not Found
```bash
# Add to PATH
export PATH=$PATH:$(go env GOPATH)/bin
# Or move binary
sudo mv provider-services /usr/local/bin/
```
### Permission Denied
```bash
chmod +x provider-services
```
### Version Mismatch
Ensure CLI version matches network version:
```bash
# Check network version
provider-services query upgrade plan --node https://rpc.akashnet.net:443
# Update CLI if needed
curl -sfL https://raw.githubusercontent.com/akash-network/provider/main/install.sh | bash
sudo mv ./bin/provider-services /usr/local/bin/
```
### Connection Refused
```bash
# Test RPC connection
curl https://rpc.akashnet.net:443/status
# Try alternative endpoints
export AKASH_NODE=https://akash-rpc.polkachu.com:443
# or pass --node https://akash-rpc.polkachu.com:443 per command
```
## Multiple Environments
Use environment variables or aliases for different networks:
```bash
# Mainnet
alias akash-main='provider-services --node https://rpc.akashnet.net:443 --chain-id akashnet-2'
# Testnet/Sandbox
alias akash-test='provider-services --node https://rpc.sandbox-01.aksh.pw:443 --chain-id sandbox-01'
```
## Next Steps
- **@wallet-setup.md** - Create and manage wallets
- **@deployment-lifecycle.md** - Deploy applications
- **@common-commands.md** - Command reference
rules/deploy/cli/lease-management.md
# Lease Management
Managing active leases, monitoring deployments, and handling lease lifecycle.
## Viewing Leases
### List Active Leases
```bash
provider-services query market lease list --owner $(provider-services keys show wallet -a) --state active
```
### Get Specific Lease
```bash
provider-services query market lease get \
--owner $(provider-services keys show wallet -a) \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER>
```
### Get Lease Status (from Provider)
```bash
provider-services lease-status \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
```
## Monitoring
### Service Health
```bash
# Check service availability
provider-services lease-status \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet | \
jq '.services | to_entries[] | {name: .key, available: .value.available, total: .value.total}'
```
### View Logs
```bash
# All service logs
provider-services lease-logs \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet
# Specific service
provider-services lease-logs \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet \
--service web
# Follow logs in real-time
provider-services lease-logs \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet \
--follow --tail 100
```
### View Events
```bash
provider-services lease-events \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet
```
## Lease Operations
### Close Lease (Keep Deployment Open)
Close a single lease without closing the entire deployment:
```bash
provider-services tx market lease close \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--from wallet
```
After closing, new bids will come in. Accept a new bid to create a fresh lease.
### Close Entire Deployment
Close deployment and all associated leases:
```bash
provider-services tx deployment close --dseq <DSEQ> --from wallet
```
### Migrate Provider
To move a deployment to a different provider:
```bash
# 1. Close current lease
provider-services tx market lease close \
--dseq <DSEQ> --gseq 1 --oseq 1 --from wallet
# 2. Wait for new bids
sleep 30
# 3. List bids
provider-services query market bid list --owner $(provider-services keys show wallet -a) --dseq <DSEQ>
# 4. Accept new bid
provider-services tx market lease create \
--dseq <DSEQ> --gseq 1 --oseq 2 \
--provider <NEW_PROVIDER> --from wallet
# 5. Send manifest to new provider
provider-services send-manifest deploy.yaml \
--dseq <DSEQ> --gseq 1 --oseq 2 \
--provider <NEW_PROVIDER> --from wallet
```
Note: OSEQ increments when creating a new lease for the same group.
## Escrow Management
### Check Escrow Balance
```bash
provider-services query deployment get \
--owner $(provider-services keys show wallet -a) \
--dseq <DSEQ> | jq '.escrow_account'
```
### Deposit More Funds
```bash
provider-services tx deployment deposit 5000000uact --dseq <DSEQ> --from wallet
```
### Check Remaining Time
```bash
# Get current balance and price
BALANCE=$(provider-services query deployment get --owner $(provider-services keys show wallet -a) --dseq <DSEQ> -o json | jq -r '.escrow_account.balance.amount')
PRICE=$(provider-services query market lease get --owner $(provider-services keys show wallet -a) --dseq <DSEQ> --gseq 1 --oseq 1 --provider <PROVIDER> -o json | jq -r '.lease.price.amount')
echo "Blocks remaining: $(( $BALANCE / $PRICE ))"
echo "Hours remaining: $(( $BALANCE / $PRICE / 600 ))"
echo "Days remaining: $(( $BALANCE / $PRICE / 14400 ))"
```
## Interactive Shell
Access containers directly:
```bash
# Open shell
provider-services lease-shell \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet \
--service web \
-- /bin/sh
# Run specific command
provider-services lease-shell \
--dseq <DSEQ> --gseq 1 --oseq 1 \
--provider <PROVIDER> --from wallet \
--service web \
-- cat /etc/nginx/nginx.conf
```
## Troubleshooting
### Lease Not Active
```bash
# Check lease state
provider-services query market lease get --dseq <DSEQ> ... -o json | jq '.lease.state'
```
Possible states:
- `active` - Running normally
- `reclaiming` - Provider started resource reclamation (v2.1.0+); a deadline is set, after which the provider may close the lease (see below)
- `closed` - Terminated
- `insufficient_funds` - Escrow depleted
The `reclaiming` state and paused groups only appear on v2.1.0+ leases; pre-2.1
leases render with the classic active/closed/insufficient_funds states only.
### Recovering a Reclaimed (Paused) Group
If a provider reclaims a lease (v2.1.0+) and the reclamation window expires, the
group is left **paused** with no automatic re-order. To recover:
```bash
# Option 1: resume the paused group (re-orders; new order inherits the reclamation requirement)
provider-services tx deployment group start --dseq <DSEQ> --gseq 1 --from wallet
# Option 2: close the deployment and redeploy
provider-services tx deployment close --dseq <DSEQ> --from wallet
provider-services tx deployment create deploy.yaml --from wallet
```
See [deployment-lifecycle.md](deployment-lifecycle.md#resource-reclamation-lifecycle-v210)
for the full reclamation flow.
### Service Not Available
```bash
# Check events for errors
provider-services lease-events --dseq <DSEQ> ...
# Check logs for crash loops
provider-services lease-logs --dseq <DSEQ> ... --tail 50
```
### Cannot Send Manifest
- Verify certificate is active
- Check provider is online
- Wait and retry (provider may be processing)
### Escrow Depleted
```bash
# Deposit immediately
provider-services tx deployment deposit 10000000uact --dseq <DSEQ> --from wallet
# If deployment closed, create new one
provider-services tx deployment create deploy.yaml --from wallet
```
## Best Practices
1. **Monitor escrow** - Set up alerts before funds run out
2. **Use sufficient deposit** - Deposit at least 7 days worth
3. **Check logs regularly** - Catch issues early
4. **Keep SDL versioned** - Track changes to deployment config
5. **Test updates locally** - Validate SDL before updating
rules/deploy/cli/mtls-legacy.md
# mTLS Authentication (Legacy)
Mutual TLS certificate-based authentication for Akash provider communication.
> **Console API users: this is not for you.**
>
> `POST /v1/certificates` on the Console API now returns **400 Bad Request** with the message:
> *"This endpoint has been removed. mTLS certificates are no longer required as identity is now verified via API key."*
>
> If you are on the Console API path, do **not** create certificates. Authentication is `x-api-key` for the Console API calls, and a JWT minted via `POST /v1/create-jwt-token` for direct calls to the provider (logs, events, status, shell). See **@../console-api/authentication.md** and **@../console-api/operations.md**.
>
> The remainder of this file documents the **CLI path**, where mTLS is still used for self-custody clients calling providers directly (e.g. `provider-services lease-status`, `provider-services lease-logs`). AEP-64 JWT auth is gradually replacing mTLS even on the CLI path; new code should prefer JWT.
## Overview
mTLS (Mutual TLS) was the original authentication method for Akash provider communication. JWT authentication ([AEP-64](https://akash.network/roadmap/aep-64/)) is now preferred everywhere, and mTLS is being phased out. The Console API has already removed it; the CLI keeps it as a fallback for now.
## How mTLS Works
```
1. Client generates X.509 certificate
2. Client broadcasts certificate to blockchain
3. Provider verifies certificate against on-chain record
4. Both client and provider authenticate each other
```
## Certificate Creation
### CLI
```bash
# Generate certificate locally
provider-services tx cert generate client --from wallet
# Publish the certificate to the blockchain
provider-services tx cert publish client --from wallet
```
This generates:
- Client certificate (public)
- Client private key (kept local)
- Broadcasts certificate hash to blockchain
### TypeScript SDK
```typescript
import { certificateManager } from "@akashnetwork/chain-sdk";
// Generate certificate (legacy — prefer JwtTokenManager for new code)
const cert = await certificateManager.createCertificate(address);
// cert contains:
// - cert: X.509 certificate (PEM)
// - privateKey: Private key (PEM)
// - publicKey: Public key (PEM)
```
### Broadcast Certificate
Use the SDK proxy for the chain message. The cert module is at `akash.cert.v1`:
```typescript
// Via the SDK proxy:
await sdk.akash.cert.v1.createCertificate({
cert: certPem,
pubkey: pubKeyPem,
// signed transaction handled by the SDK
});
// Or build the raw message manually from private-types:
import { MsgCreateCertificate } from "@akashnetwork/chain-sdk/private-types/akash.v1";
const msg = {
typeUrl: "/akash.cert.v1.MsgCreateCertificate",
value: {
owner: address,
cert: Buffer.from(cert.cert).toString("base64"),
pubkey: Buffer.from(cert.publicKey).toString("base64")
}
};
await client.signAndBroadcast(address, [msg], "auto");
```
## Certificate Storage
### CLI
Certificates stored in `~/.akash/` directory.
### Application
Store securely:
```typescript
// Save to secure storage
const certData = {
cert: cert.cert,
privateKey: cert.privateKey,
publicKey: cert.publicKey,
address: address,
createdAt: new Date().toISOString()
};
// Encrypt before persisting
```
## Using Certificates
### Provider Communication
```typescript
import https from "https";
const agent = new https.Agent({
cert: certPem,
key: privateKeyPem,
rejectUnauthorized: false // Provider uses self-signed certs
});
const response = await fetch(providerUrl, {
agent,
method: "PUT",
body: JSON.stringify(manifest)
});
```
### CLI Provider Operations
Certificate is used automatically:
```bash
provider-services send-manifest deploy.yaml \
--dseq <DSEQ> \
--gseq 1 \
--oseq 1 \
--provider <PROVIDER> \
--from wallet
```
## Certificate Lifecycle
| Operation | Command |
|-----------|---------|
| Generate | `provider-services tx cert generate client --from wallet` |
| Publish | `provider-services tx cert publish client --from wallet` |
| Query | `provider-services query cert list --owner <address>` |
| Revoke | `provider-services tx cert revoke --from wallet` |
### Certificate States
```
Active → Revoked
```
Only one active certificate per address.
### Renewing Certificates
```bash
# Revoke old
provider-services tx cert revoke --from wallet
# Generate and publish new
provider-services tx cert generate client --from wallet
provider-services tx cert publish client --from wallet
```
## Query Certificates
### CLI
```bash
provider-services query cert list --owner $(provider-services keys show wallet -a)
```
### REST API
```bash
curl "https://api.akashnet.net/akash/cert/v1/certificates/list?filter.owner=akash1..."
```
## Troubleshooting
### Certificate Mismatch
```
Error: certificate verification failed
```
**Cause:** Local certificate doesn't match on-chain record.
**Solution:** Revoke and recreate certificate.
### Expired Certificate
```
Error: certificate expired
```
**Solution:** Revoke old certificate and create new one.
### Provider Rejection
```
Error: provider rejected manifest
```
**Cause:** Certificate issue or manifest mismatch.
**Solution:**
1. Verify certificate is active on-chain
2. Check manifest matches deployment
## Migration to JWT
For new integrations, prefer JWT authentication:
- Simpler setup (no certificate management)
- Works with Console API
- Better for web applications
- No on-chain certificate needed
See **@../console-api/operations.md** for JWT setup (the canonical reference for both Console API and self-custody JWT minting).
## When to Use mTLS
| Scenario | Recommended |
|----------|-------------|
| New projects | JWT |
| CLI usage | mTLS (automatic) |
| Existing integrations | mTLS (continue) |
| Web applications | JWT |
| Provider development | mTLS |
rules/deploy/cli/wallet-setup.md
# Wallet Setup
Create and manage wallets for Akash CLI operations.
## Create New Wallet
### Generate New Key
```bash
provider-services keys add wallet
```
Output:
```
- address: akash1abc123...
name: wallet
pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"..."}'
type: local
**Important** write this mnemonic phrase in a safe place.
It is the only way to recover your account if you ever forget your password.
word1 word2 word3 ... word24
```
**Save the mnemonic phrase securely!**
### With Custom Name
```bash
provider-services keys add production-wallet
provider-services keys add staging-wallet
```
## Import Existing Wallet
### From Mnemonic
```bash
provider-services keys add wallet --recover
```
Enter your 24-word mnemonic when prompted.
### From Keystore File
```bash
provider-services keys import wallet keyfile.json
```
### From Ledger
```bash
provider-services keys add wallet --ledger
```
## List Wallets
```bash
provider-services keys list
```
Output:
```json
[
{
"name": "wallet",
"type": "local",
"address": "akash1abc123...",
"pubkey": "{...}"
}
]
```
## Show Wallet Details
```bash
provider-services keys show wallet
```
### Show Address Only
```bash
provider-services keys show wallet -a
```
### Show Public Key Only
```bash
provider-services keys show wallet --pubkey
```
## Delete Wallet
```bash
provider-services keys delete wallet
```
**Warning:** This removes the key from your keyring. Ensure you have the mnemonic backed up.
## Export Wallet
### Export Private Key (Encrypted)
```bash
provider-services keys export wallet > wallet.key
```
### Export Unencrypted (Dangerous)
```bash
provider-services keys export wallet --unarmored-hex --unsafe
```
(Both flags are required together and the command prompts for confirmation.)
**Outputs the raw private key in plaintext hex. Never share it or store it unencrypted.**
## Fund Wallet
### Check Balance
```bash
provider-services query bank balances $(provider-services keys show wallet -a)
```
### Get Tokens
1. **Purchase AKT** from exchanges (Kraken, Coinbase, etc.)
2. **Withdraw to your address** shown by `provider-services keys show wallet -a`
3. **Or use testnet faucet** for sandbox network
### Verify Funds
```bash
provider-services query bank balances $(provider-services keys show wallet -a) --node https://rpc.akashnet.net:443
```
## Multiple Wallets
### Use Different Keys
```bash
# Default wallet
provider-services tx bank send ... --from wallet
# Specific wallet
provider-services tx bank send ... --from production-wallet
```
### Environment Variable
```bash
export AKASH_FROM=wallet
provider-services tx deployment create deploy.yaml # Uses $AKASH_FROM
```
## Security Best Practices
### Keyring Backends
| Backend | Security | Use Case |
|---------|----------|----------|
| `os` | High | Production (uses OS keychain) |
| `file` | Medium | Servers (encrypted file) |
| `test` | Low | Development only |
There is no `config` subcommand. Set the keyring backend with an environment variable or a per-command flag:
```bash
# Production
export AKASH_KEYRING_BACKEND=os
# Server deployment
export AKASH_KEYRING_BACKEND=file
# Development
export AKASH_KEYRING_BACKEND=test
# Or pass it per command
provider-services keys list --keyring-backend os
```
### Password Management
With `os` or `file` backend, you'll be prompted for password:
```bash
provider-services keys add wallet --keyring-backend file
# Enter and confirm password
```
For scripting, use environment:
```bash
export AKASH_KEYRING_BACKEND=file
echo "password" | provider-services tx deployment create deploy.yaml --from wallet
```
### Hardware Wallets
For maximum security, use Ledger:
```bash
# Create Ledger-backed key
provider-services keys add wallet --ledger
# Sign transaction (requires physical confirmation)
provider-services tx deployment create deploy.yaml --from wallet --ledger
```
## Wallet Permissions
### Query Operations (No Key Needed)
```bash
# Anyone can query
provider-services query deployment list --owner akash1abc...
```
### Transaction Operations (Key Required)
```bash
# Requires key for signing
provider-services tx deployment create deploy.yaml --from wallet
```
## Troubleshooting
### Key Not Found
```bash
Error: wallet.info: key not found
```
**Solution:** Check keyring backend matches where key was created:
```bash
provider-services keys list --keyring-backend os
provider-services keys list --keyring-backend test
```
### Wrong Password
```bash
Error: invalid passphrase
```
**Solution:** Re-enter correct password or recover from mnemonic:
```bash
provider-services keys delete wallet
provider-services keys add wallet --recover
```
### Insufficient Funds
```bash
Error: insufficient funds
```
**Solution:** Check balance and fund wallet:
```bash
provider-services query bank balances $(provider-services keys show wallet -a)
# Fund from exchange or transfer from another wallet
```
## Address Derivation
Akash uses standard Cosmos SDK derivation:
```
HD Path: m/44'/118'/0'/0/0
Prefix: akash
```
You can derive multiple accounts:
```bash
# Account 0 (default)
provider-services keys add wallet
# Account 1
provider-services keys add wallet2 --account 1
# Account 2
provider-services keys add wallet3 --account 2
```
## Next Steps
- **@deployment-lifecycle.md** - Create deployments
- **@common-commands.md** - Command reference
rules/deploy/console-api/account-and-funding.md
# Console Account & Funding
Console holds the wallet. A Console user has a personal account and can belong to teams, and each of them has its own managed wallet and credits. An API key acts in the one it was created in, and deployments spend from that wallet's credits automatically. There is no separate wallet to enable and nothing to fund per deployment.
## The model
```
Console user
├── login (email + password, or OAuth)
├── personal account (type "personal"; the user is its only member)
│ ├── managed wallet: signs every deployment action
│ │ └── credits (USD), split into
│ │ ├── Available: free to fund new and running deployments
│ │ └── Escrow: held by running deployments, returned as each one closes
│ ├── projects: a default one, plus any others
│ └── API keys: act here only, optionally limited to one project
└── teams the user belongs to, each with
├── its own managed wallet, credits, cards and Auto recharge
├── projects, starting with a default one
├── members with roles (owner, admin, member, billing, viewer)
└── the user's API keys for this team: act here only, with the user's role, optionally limited to one project
```
Each deployment still has its own escrow account on Akash; Console fills it for you. The Console UI shows the split as **Available** and **Escrow**, the same words the API uses. Teams, projects and roles: **@teams-and-projects.md**.
## Setup happens in the UI
None of these steps has a supported API:
1. **Sign up** at [console.akash.network](https://console.akash.network) and verify the email address. New accounts may start on a free trial with a small credit and some limits (certain GPU models, a restricted provider list); a trial account may have to accept the Fair Use Policy in the UI before its first deploy (`403 fair_use_policy_required` otherwise).
2. **Add credits** under **Billing**, by card through Stripe.
3. **Optionally turn on Auto recharge** under Billing, so the card is charged before credits run out.
4. **Create an API key** under **Settings → API Keys** (**@authentication.md**).
A team does the same on its own **Billing** page, separately from any member's personal account. Only its owners and billing members add credits, manage cards and set Auto recharge. Teams never get a free trial, so a new team can't deploy until someone adds credits.
## Reading the balance
```bash
curl -s https://console-api.akash.network/v1/balances -H "x-api-key: $AKASH_API_KEY" | jq .data
```
```json
{ "balance": 42.17, "deployments": 6.3, "total": 48.47 }
```
All values are USD. `balance` is **Available**, `deployments` is the **Escrow** held by running deployments (not spent: it returns to `balance` as they close), and `total` is the sum. With the API key and no query string, the call reads the wallet the key acts in: its creator's personal account, or its team's wallet for a team key, which every member shares. `?address=akash1...` reads any address without a key.
Related reads:
| Endpoint | Returns |
|---|---|
| `GET /v1/weekly-cost` | USD per week across the running deployments of the key's account or team |
| `GET /v1/usage/history?address=&startDate=&endDate=` | Daily spend for an address (public; at most 366 days). The wallet address is the `owner` of any deployment it pays for (`deployment.id.owner`); a team deployment's owner is the team wallet. |
| `GET /v1/project-spend`, `GET /v1/deployment-spend` | A team's spend per project, and per deployment and day (**@teams-and-projects.md**) |
| `GET /v1/deployment-funding-config` | The constants automatic funding runs on (below) |
## How automatic funding works
Console funds every deployment from the account's credits; there is no deposit to send and no top-up to call. `GET /v1/deployment-funding-config` (public) returns the constants:
```json
{ "data": { "targetRunwayHours": 48, "balanceHeadroomUsd": 5, "defaultDepositUsd": 0.5 } }
```
- Each deployment starts with `defaultDepositUsd` in escrow. Creating one with less available answers **402 `insufficient_balance`**.
- Once its lease starts, Console tops the escrow up toward `targetRunwayHours` of runtime and keeps it there while credits last.
- Top-ups leave about `balanceHeadroomUsd` of the available balance untouched, so a new deployment can still be created.
- When the credits run out, running deployments live on what their escrow holds, up to about `targetRunwayHours`, then close. Adding credits is the fix; it is a UI action.
The one programmatic knob is a deployment's **runtime limit**, which closes it after a set number of hours: `runtimeLimitHours` on create, or `PATCH /v2/deployment-settings/{dseq}` (**@deployment-endpoints.md**). Don't call `POST /v1/deposit-deployment`: it is deprecated and does nothing automatic funding doesn't already do.
## When a create is refused for money
| Response | Meaning | Do this |
|---|---|---|
| 402 `insufficient_balance` | Available credit is below what the new deployment needs. `data.requiredAmountUsd` and `data.availableAmountUsd` give the gap. | Tell the user to add credits in the Console UI, or close deployments they no longer need. For a team key, the team's credits are short: an owner or billing member adds them on the team's Billing page. |
| 402 `balance_top_up_pending` | Auto recharge is already charging the card | Retry after the `Retry-After` header |
| 402 `payment_required` | The fee allowance ran out, or a trial account asked for something the trial excludes | Add credits |
| 403 `project_budget_exceeded` | The project's monthly budget is spent | An owner or admin raises it (**@teams-and-projects.md**); retrying won't help |
Credits never move between a personal account and a team, and a key can't switch to another one's wallet.
## Not programmatic
These are Console UI features, and the endpoints behind them are internal (listed in **@overview.md**):
- Signup, login, email verification, account deletion
- Creating a team and accepting an invitation (a Console session only; an API key gets 403 `session_required`)
- Payment methods, adding credits, invoices and billing history, for a personal account or a team
- Auto recharge settings
- Profile, favorites, saved templates, alerts and notification channels
- **Signing arbitrary transactions.** The managed wallet's key can't be exported, and no endpoint signs a message of your choosing; `/v1/tx` is the Console UI's legacy signer and accepts only six message types (deployment, lease, deposit and certificate messages). Use the dedicated endpoints, and switch to a self-custody SDK for anything they don't cover.
- **Self-custody wallets.** Keplr and Ledger never touch the Console API; they use the CLI or an SDK (`../cli/`, `../../sdk/`).
## Related files
- **@authentication.md** — API keys
- **@deployment-endpoints.md** — Deployments, leases, bids, runtime limits
- **@api-key-quickstart.md** — End-to-end walkthrough
- **@teams-and-projects.md**: Teams, projects, roles and team billing
rules/deploy/console-api/api-key-quickstart.md
# Console API Quickstart — From API Key to Running Deployment
The whole flow for the API-key path: no CLI, no certificates, no private keys. It is written in curl + Bash; when the user works in Node, Python or Go, translate the same calls into their language (see SKILL.md, "Language for the Console API").
## What you need
- An API key in the environment variable `AKASH_API_KEY`. No key yet? See **@authentication.md**. A key created in a team deploys for that team, and a key bound to a project deploys into that project (**@teams-and-projects.md**).
- Credits on the Console account, or on the team for a team key. A few dollars covers a small test.
- An SDL file, here `deploy.yaml`, and `jq`.
### Where the key lives
| Runtime | How |
|---|---|
| Local shell | `export AKASH_API_KEY=...`, or a `.env` file loaded by `direnv`, with `.env` in `.gitignore` |
| GitHub Actions | Repository secret `AKASH_API_KEY`, exposed as `env: AKASH_API_KEY: ${{ secrets.AKASH_API_KEY }}` |
| GitLab CI | Masked, protected CI/CD variable `AKASH_API_KEY` |
| Docker | `docker run -e AKASH_API_KEY ...` at run time, never `ARG` or `ENV` in the Dockerfile |
| Production | A secrets manager that injects `AKASH_API_KEY` into the process environment |
`echo "${AKASH_API_KEY:+set}"` prints `set` without revealing the key.
## Step 1 — The SDL
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
`denom: uact` is the deployment-payment denom. SDL syntax is in `../../sdl/`.
## Step 2 — Create the deployment
```bash
API=https://console-api.akash.network
RESPONSE=$(curl -sX POST "$API/v1/deployments" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --rawfile sdl deploy.yaml '{data: {sdl: $sdl, name: "nginx-test"}}')")
DSEQ=$(echo "$RESPONSE" | jq -r '.data.dseq')
echo "Deployment $DSEQ created"
```
No deposit: Console funds the deployment from the credits of the key's account or team. A `402` here means those credits are short; the body's `code` says why (**@account-and-funding.md**). In a team, add `projectId` to `data` to pick the project; a key bound to a project needs nothing extra, and `400 project_required` means a project must be named (**@teams-and-projects.md**). Secrets in `env` need nothing extra to stay out of later reads, but to seal only some values and keep the rest readable, see **@secrets.md**.
## Step 3 — Wait for bids
```bash
for i in $(seq 1 12); do
BIDS=$(curl -s "$API/v1/bids?dseq=$DSEQ" -H "x-api-key: $AKASH_API_KEY")
COUNT=$(echo "$BIDS" | jq '[.data[] | select(.bid.state == "open")] | length')
[ "$COUNT" -gt 0 ] && break
sleep 5
done
echo "$BIDS" | jq '.data[] | {provider: .bid.id.provider, price: .bid.price.amount}'
```
Bids usually arrive within 5 to 30 seconds. None after a minute means no provider matches the SDL or the price is too low: run the matcher in `rules/bid-matching/`.
## Step 4 — Accept the cheapest bid
```bash
BID=$(echo "$BIDS" | jq -c '[.data[] | select(.bid.state == "open")] | sort_by(.bid.price.amount | tonumber) | .[0].bid.id')
curl -sX POST "$API/v1/leases" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --argjson id "$BID" '{leases: [{dseq: $id.dseq, gseq: $id.gseq, oseq: $id.oseq, provider: $id.provider}]}')" \
| jq '.data.leases[0].id'
```
The body is flat (no `data` wrapper) and carries no manifest: Console sends the provider the manifest it recorded in step 2. If the call fails with `code: "provider_unreachable"`, no lease was made, so try the next bid. With `manifest_not_delivered` the lease exists: send the same request again.
## Step 5 — Wait until it serves, then get the URL
```bash
until curl -s "$API/v1/deployments/$DSEQ" -H "x-api-key: $AKASH_API_KEY" \
| jq -e '.data.leases[0].status.services.web.available >= 1' > /dev/null; do
sleep 5
done
HOST=$(curl -s "$API/v1/deployments/$DSEQ" -H "x-api-key: $AKASH_API_KEY" \
| jq -r '.data.leases[0].status.services.web.uris[0]')
echo "http://$HOST"
```
A global expose with `as: 80` gets a hostname in `services.<svc>.uris`. Any other port appears under `forwarded_ports.<svc>` as `host` plus a random `externalPort`:
```bash
jq -r '.data.leases[0].status.forwarded_ports.api[0] | "\(.host):\(.externalPort)"'
```
## Step 6 — Logs and events
Logs come from the provider, not the Console API. The short version, with the full flow in **@operations.md**:
```bash
LEASE=$(curl -s "$API/v1/deployments/$DSEQ" -H "x-api-key: $AKASH_API_KEY" | jq '.data.leases[0].id')
PROVIDER=$(echo "$LEASE" | jq -r .provider); GSEQ=$(echo "$LEASE" | jq -r .gseq); OSEQ=$(echo "$LEASE" | jq -r .oseq)
HOSTURI=$(curl -s "$API/v1/providers/$PROVIDER" | jq -r .hostUri)
JWT=$(curl -sX POST "$API/v1/create-jwt-token" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"ttl": 1800, "leases": {"access": "scoped", "scope": ["logs", "events", "status"]}}}' \
| jq -r .data.token)
websocat -k "wss://${HOSTURI#https://}/lease/$DSEQ/$GSEQ/$OSEQ/logs?follow=true&tail=200" \
-H "Authorization: Bearer $JWT"
```
`-k` is there because providers serve self-signed certificates; the JWT is the authentication. A key bound to a project, or a team `member`'s or `viewer`'s key, gets 403 `project_forbidden` for this `scoped` token: it names the deployment with the `granular` shape instead (**@operations.md**).
## Step 7 — Change it, then close it
Roll a new image, or change env, command, ports or the name, with `PATCH`. Only what you name changes:
```bash
curl -sX PATCH "$API/v1/deployments/$DSEQ" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"services": {"web": {"image": "nginx:1.27.2"}}}}'
```
CPU, memory, storage, GPUs and replica counts can't change in place: that needs a new deployment (**@deployment-endpoints.md**, "Patch a deployment").
Close it when you're done; unspent escrow returns to the account:
```bash
curl -sX DELETE "$API/v1/deployments/$DSEQ" -H "x-api-key: $AKASH_API_KEY"
```
## What you didn't have to do
- Install `provider-services` or run `provider-services keys add`
- Hold a private key, mnemonic or hardware wallet
- Create mTLS certificates (deprecated for this path: `../cli/mtls-legacy.md`)
- Buy AKT, or mint ACT
- Send a deposit or a manifest
If a step ever needs one of these, the workflow has drifted onto the CLI or SDK path; re-read SKILL.md, "Choosing a Deployment Method".
## Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| `401 Invalid API key` | Key revoked, expired or mistyped | Check it starts `ac.sk.production.`; create a new one if needed |
| `401` with a valid key | Key sent as `Authorization: Bearer` | Use `x-api-key` |
| `400 validation_error` on create | Body not wrapped in `{"data": {...}}`, or the SDL built by string splicing | Build the body with `jq --rawfile` |
| `402 insufficient_balance` | Not enough credit | Add credits in the Console UI; for a team key, on the team's Billing page (owner or billing member) |
| `400 project_required` on create | The key reaches several projects and can't deploy into the default one | Pass `data.projectId` from `GET /v1/projects` |
| `403 project_forbidden` | The deployment or project is outside the key's projects | Stay within the key's project, or use a key that reaches the other one |
| `400 organization_mismatch` | `x-organization-id` names another team than the key's | Drop the header; use a key created in that team |
| No bids | Resources or price match no provider | `rules/bid-matching/` |
| `502 provider_unreachable` on lease | The provider is down | Accept another bid |
| `PUT` used to update | Deprecated full-SDL update | `PATCH` with only the fields that change |
| `422 deployment_resources_changed` | Tried to resize in place | New deployment, then close the old one |
| URL is `null` | Read `forwarded_ports` for an `as: 80` service | Read `status.services.<svc>.uris` |
| Logs call times out | Sent to `console-api.akash.network` | Logs come from the provider (step 6) |
## Related files
- **@overview.md** — Envelopes, errors, deprecations, the endpoint map
- **@authentication.md** — API keys
- **@deployment-endpoints.md** — Every endpoint in detail
- **@secrets.md** — Sealed secrets
- **@account-and-funding.md** — Balance and automatic funding
- **@operations.md** — Logs, events, status, shell, attestation
- **@teams-and-projects.md**: Team keys, projects and the team errors
rules/deploy/console-api/authentication.md
# Console API Authentication
| Credential | Header | Use |
|---|---|---|
| **API key** | `x-api-key: <key>` | Everything a script, CI job or backend does |
| Console session JWT | `Authorization: Bearer <jwt>` | The Console web app's login session. Scripts don't use it. |
| Provider JWT | `Authorization: Bearer <jwt>`, sent to the **provider** | Logs, events, status, shell, attestation. Minted with `POST /v1/create-jwt-token`; see **@operations.md**. |
**API keys go in `x-api-key`, never in `Authorization: Bearer`.**
## API keys
### Format
```
ac.sk.production.<64 hex characters>
```
A key starting `ac.sk.` is a Console API key. AkashML keys start `akml-` and belong to a different service (`rules/deploy/akashml/`).
### Getting one
1. Sign up at [console.akash.network](https://console.akash.network). The account comes with its managed wallet.
2. Add credits under **Billing** (card via Stripe).
3. To deploy for a team, switch to that team first. A key acts in the personal account or team selected when it is created, and only there.
4. Open **Settings → API Keys** (`console.akash.network/user/api-keys`) and create a key. Leave it on **All projects**, or limit it to one project.
5. Copy it immediately. The full key is shown once; afterwards only a masked form (`ac.sk.production.abc123***def456`) is visible.
The first key always comes from the UI: every `/v1/api-keys` call needs an existing key or a Console session.
### Using one
Reference the key through an environment variable, never as a literal. The canonical name is `AKASH_API_KEY`.
```bash
curl https://console-api.akash.network/v1/deployments -H "x-api-key: $AKASH_API_KEY"
```
```typescript
const res = await fetch("https://console-api.akash.network/v1/deployments", {
headers: { "x-api-key": process.env.AKASH_API_KEY! }
});
```
```python
import os, requests
res = requests.get(
"https://console-api.akash.network/v1/deployments",
headers={"x-api-key": os.environ["AKASH_API_KEY"]},
)
```
```go
req, _ := http.NewRequest("GET", "https://console-api.akash.network/v1/deployments", nil)
req.Header.Set("x-api-key", os.Getenv("AKASH_API_KEY"))
resp, err := http.DefaultClient.Do(req)
```
### Managing keys
| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/v1/api-keys` | List your keys in the current team: `id`, `name`, `keyFormat` (masked), `organizationId`, `projectId`, `createdAt`, `lastUsedAt`, `expiresAt` |
| `POST` | `/v1/api-keys` | Create a key in the current team, optionally limited to one project with `projectId`. The response's `apiKey` is the only time the full key is returned. |
| `GET` | `/v1/api-keys/{id}` | One key's metadata |
| `PATCH` | `/v1/api-keys/{id}` | Rename: `{ "data": { "name": "..." } }`. Nothing else can change. |
| `DELETE` | `/v1/api-keys/{id}` | Revoke |
```bash
curl -sX POST https://console-api.akash.network/v1/api-keys \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"name": "ci-prod-deploy", "expiresAt": "2027-01-01T00:00:00Z"}}'
```
```json
{
"data": {
"id": "6f1c...",
"name": "ci-prod-deploy",
"apiKey": "ac.sk.production.<copy this now>",
"keyFormat": "ac.sk.production.9f2e4d***a17c03",
"organizationId": "1b6e...",
"projectId": null,
"expiresAt": "2027-01-01T00:00:00.000Z",
"createdAt": "...",
"updatedAt": "...",
"lastUsedAt": null
}
}
```
`expiresAt` is optional and at most 1 year away; omit it and the key expires 365 days after creation. A later date answers 400. Every new key expires, so plan its rotation (below).
### What a key can do
A key acts as the user who created it, in one place, never more than that user can:
- **One team.** `organizationId` is the personal account or team the key was created in, and every request runs there. It is `null` on keys from before teams, which act in their creator's personal account. A key can't switch teams: create one in each team it should deploy for.
- **The creator's role.** In a team, the key can do what its creator's role allows today: a `viewer` or `billing` member's key can't deploy, and once the creator leaves the team the key answers 403 `organization_forbidden`.
- **Optionally one project.** `projectId` limits the key to one project of its team; `null` means every project its creator reaches. A project key deploys into that project, can't touch other projects' deployments (403 `project_forbidden`), and can't manage keys, members or billing.
The binding is fixed: `PATCH` only renames. Give each service its own key, bound as narrowly as it needs, and revoke the ones you stop using. Details and the team errors: **@teams-and-projects.md**.
### Rotation
1. Create the new key (UI or `POST /v1/api-keys`).
2. Roll it out to every consumer.
3. Confirm traffic moved: the old key's `lastUsedAt` stops advancing.
4. Delete the old key.
## Public endpoints
These need no key:
| Endpoint | Returns |
|---|---|
| `GET /v1/providers`, `GET /v1/providers/{address}`, `GET /v1/provider-search` | Providers |
| `GET /v1/placement-options` | Regions and GPUs available now |
| `POST /v1/bid-screening` | Providers that would bid on a group |
| `GET /v1/deployment-funding-config` | Automatic funding constants |
| `GET /v1/deployment/{owner}/{dseq}` | Read-only view of any deployment |
| `GET /v1/balances?address=<akash1...>` | Balance split for any address. With a key and no `address`, the balance of the key's personal account or team. |
| `GET /v1/blockchain-status` | `{ "isBlockchainReachable": true }` |
## Auth errors
Errors use the shared envelope (see **@overview.md**).
| Status | Cause | Fix |
|---|---|---|
| 401 `Invalid API key` | The key is malformed, revoked, expired, or from another Console environment. The response doesn't say which. | Check the value starts `ac.sk.production.`, then list keys in the UI |
| 401 | The key went in `Authorization: Bearer`, or no credential was sent | Send it in `x-api-key` |
| 400 | Both `Authorization` and `x-api-key` were sent | Send only `x-api-key` |
| 403 | The key is valid but the account may not do this, such as a free-trial account over the trial's limits or without the Fair Use Policy accepted (`fair_use_policy_required`) | Act on the `code` and `message` |
| 403 `organization_forbidden` | The key's creator is no longer a member of the key's team | Use a key of a current member |
| 400 `organization_mismatch` | `x-organization-id` names another team than the key's | Drop the header; a key only acts in its own team |
| 403 `project_forbidden` | The request reaches a project the key can't, such as another project than the one it is bound to | Stay within the key's project (**@teams-and-projects.md**) |
| 403 `session_required` | The action needs a signed-in Console session: creating, leaving or deleting a team, accepting an invitation, deleting the account | Do it in the Console web app |
## CI/CD
Build JSON bodies with `jq` so a multi-line SDL is escaped correctly; never splice `$SDL` into a hand-written JSON string.
### GitHub Actions
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
env:
AKASH_API_KEY: ${{ secrets.AKASH_API_KEY }}
steps:
- uses: actions/checkout@v4
- name: Create the deployment
run: |
curl -sfX POST https://console-api.akash.network/v1/deployments \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --rawfile sdl deploy.yaml '{data: {sdl: $sdl}}')"
```
### GitLab CI
```yaml
deploy:
image: alpine:3.20
before_script:
- apk add --no-cache curl jq
script:
- >
curl -sfX POST https://console-api.akash.network/v1/deployments
-H "x-api-key: $AKASH_API_KEY"
-H "Content-Type: application/json"
-d "$(jq -nc --rawfile sdl deploy.yaml '{data: {sdl: $sdl}}')"
```
Store `AKASH_API_KEY` as a masked, protected CI/CD variable.
### Docker
Pass the key at run time: `docker run -e AKASH_API_KEY my-deployer`. Never bake it in with `ARG` or `ENV`; both survive in the image history.
rules/deploy/console-api/deployment-endpoints.md
# Console API — Deployment Endpoint Reference
Every endpoint below lives under `https://console-api.akash.network` and takes `x-api-key: <key>` unless marked public. Write endpoints wrap the body in `{ "data": { ... } }`; the exceptions are called out. The error envelope and the cross-cutting error codes are in **@overview.md**.
## Quick links
- [Deployments](#deployments): create, list, read, patch, close, move between projects
- [Leases](#leases)
- [Bids](#bids)
- [Runtime limits](#runtime-limits-deployment-settings-v2)
- [Providers and placement](#providers-and-placement)
- [Bid screening](#bid-screening)
- [Deprecated endpoints](#deprecated-endpoints)
- [Endpoints that do not exist](#endpoints-that-do-not-exist)
Secrets (`sealedSecrets`, `inheritSecretsFrom`, `ac-secret://` references) have their own file: **@secrets.md**. Logs, events, status and shell come from the provider: **@operations.md**.
## Deployments
### Create a deployment
```
POST /v1/deployments
```
**Body (`data`):**
| Field | Required | Description |
|---|---|---|
| `sdl` | yes | The SDL as a YAML string, at most 512 KiB |
| `name` | no | 1 to 256 characters, shown wherever the deployment is listed. Defaults to the SDL's service names joined with `+`. |
| `runtimeLimitHours` | no | 1 to 48. Console closes the deployment that many hours after its lease starts and returns the unspent credits. Omit for always-on funding. |
| `sealedSecrets` | no | A JWE carrying secret values the SDL references as `ac-secret://NAME`. See **@secrets.md**. |
| `inheritSecretsFrom` | no | Dseq of a deployment you can change in the same account or team, open or closed, whose stored secrets this one starts from. See **@secrets.md**. |
| `projectId` | no | The project to file the deployment in. Without it: the key's project for a key bound to one, otherwise the default project, and 400 `project_required` when the caller can't deploy there. See **@teams-and-projects.md**. |
Don't send `deposit`. Console funds every deployment from the credits of the key's account or team and ignores the field.
**Example:**
```bash
SDL=$(cat deploy.yaml)
curl -sX POST https://console-api.akash.network/v1/deployments \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg sdl "$SDL" '{data: {sdl: $sdl, name: "web-prod"}}')"
```
**Response (201):**
```json
{
"data": {
"dseq": "1785245500842",
"manifest": "<the manifest Console derived from the SDL>",
"signTx": { "code": 0, "transactionHash": "...", "rawLog": "..." }
}
}
```
Keep `dseq`. You don't need `manifest`: Console records the deployment's definition and sends the manifest to the provider itself when you create the lease.
**Errors:**
| Status | When |
|---|---|
| 400 | The SDL is invalid, or a secret reference has no value, or `sealedSecrets` is malformed, expired or names a value no service references. `code: project_required`: no project could be chosen, so pass `projectId`. |
| 402 | Not enough credit. `code` is `insufficient_balance` or `balance_top_up_pending` (see **@overview.md**). Free-trial refusals, such as a GPU model the trial excludes, answer `payment_required`. |
| 403 | `sealedSecrets` was sealed for another user or another SDL. Free-trial accounts also get 403 when the deployment exceeds the trial's resource limits, or `code: fair_use_policy_required` until the Fair Use Policy is accepted in the Console UI. `code: project_budget_exceeded`: the project's monthly budget is spent. A team `viewer` or `billing` member's key can't deploy at all. |
| 404 | `inheritSecretsFrom` names no deployment you can change, or `projectId` names a project you can't deploy into |
| 409 | The seal used a retired key (refetch `GET /v1/sdl-secrets-context` and seal again), or `code` is `inherited_secrets_unreadable` (permanent: supply the values in `sealedSecrets` instead) |
| 503 | The key service was briefly unreachable. Retry. |
### List deployments
```
GET /v1/deployments?state=active&skip=0&limit=100
```
| Query | Default | Description |
|---|---|---|
| `state` | `active` | `active` or `closed`. Closed deployments only appear with `state=closed`. |
| `search` | none | Case-insensitive substring of the deployment's name or dseq. `total` and `hasMore` then describe the matches. |
| `reverse` | `false` | `true` lists newest first; the default is oldest first |
| `skip` | `0` | Offset |
| `limit` | `100` | Page size, 1 to 100 |
| `projectId` | none | Only the deployments filed in this project. A project you can't reach lists nothing. |
**Response:**
```json
{
"data": {
"deployments": [
{
"deployment": { "id": { "owner": "akash1...", "dseq": "1785245500842" }, "state": "active", "created_at": "..." },
"leases": [ ... ],
"escrow_account": { ... },
"name": "web-prod",
"projectId": "3f0c...",
"groups": [ ... ],
"settings": { "name": "web-prod", "runtimeLimitHours": null, "runtimeEndsAt": null, "closed": false }
}
],
"pagination": { "total": 7, "skip": 0, "limit": 100, "hasMore": false }
}
}
```
Page on `hasMore`, not on `total`: `total` comes from Console's index, can briefly lag, and is `null` when the index can't answer. Leases in this list carry no live `status`; read one deployment for that. A `search` over an account with more deployments than a search can span answers 422; page without `search` instead. In a team, the list covers the projects the key reaches, each deployment carries its `projectId`, and a team with more open deployments than one list can check answers 422 too: filter with `projectId`.
```bash
for STATE in active closed; do
SKIP=0
while :; do
PAGE=$(curl -s "https://console-api.akash.network/v1/deployments?state=$STATE&skip=$SKIP&limit=100" \
-H "x-api-key: $AKASH_API_KEY")
echo "$PAGE" | jq -r '.data.deployments[] | "\(.deployment.id.dseq)\t\(.name // "-")\t\(.deployment.state)"'
[ "$(echo "$PAGE" | jq -r '.data.pagination.hasMore')" = "true" ] || break
SKIP=$((SKIP + 100))
done
done
```
For names alone, `GET /v1/deployment-names?dseq=1&dseq=2` answers `{ "data": { "1": "web-prod", "2": null } }`.
### Get a deployment
```
GET /v1/deployments/{dseq}
```
**Response:**
```json
{
"data": {
"deployment": { "id": { "owner": "akash1...", "dseq": "1785245500842" }, "state": "active", "created_at": "..." },
"leases": [
{
"id": { "owner": "akash1...", "dseq": "1785245500842", "gseq": 1, "oseq": 1, "provider": "akash1prov...", "bseq": 21 },
"state": "active",
"price": { "denom": "uact", "amount": "1.234" },
"status": {
"services": { "web": { "available": 1, "total": 1, "uris": ["abc123.ingress.provider.example"], "ready_replicas": 1 } },
"forwarded_ports": { "api": [{ "host": "provider.example", "port": 8080, "externalPort": 31234 }] },
"ips": {}
}
}
],
"escrow_account": { "state": { "state": "open", "funds": [{ "denom": "uact", "amount": "..." }] } },
"name": "web-prod",
"consoleSettings": { "sdl": "version: \"2.0\"\n...", "manifestVersion": "..." }
}
}
```
This is the canonical read for a running deployment:
- **Where the service is reachable.** A global expose with `as: 80` gets an HTTP hostname: `status.services.<svc>.uris[0]`. Any other global port gets a random public port: `status.forwarded_ports.<svc>[]` gives `host` and `externalPort`. Leased IPs appear under `status.ips`.
- **Health.** `status.services.<svc>.available` against `total`. `status` is `null` when Console couldn't reach the provider; poll again.
- **The stored definition.** `consoleSettings.sdl` is the SDL Console recorded, re-serialized, with secret values replaced by `ac-secret://` references. `consoleSettings.manifestVersion` is what `PATCH` takes as `ifManifestVersion`. Both are `null` for a deployment Console holds no definition for.
- **Reclamation.** A lease whose provider has flagged its capacity for reclamation carries `reclamation.deadline` (unix seconds). See `rules/sdl/reclamation.md`.
- **GPUs.** `offeredGpus` is what the provider bid with, which is what an "any model" request resolved to. `detectedGpus` is what Console observed inside the containers, with the driver version.
A deployment outside the projects the key reaches answers 404, as if it didn't exist.
### Patch a deployment
```
PATCH /v1/deployments/{dseq}
```
Changes the definition Console stored, then sends the deployment update and the new manifest. The lease and the provider stay; the provider restarts the touched services. Only what you name changes, and the SDL itself is never part of the request.
**Body (`data`):**
| Field | Description |
|---|---|
| `services.<name>.image` | New image tag |
| `services.<name>.env` | Map of variable name to value, merged into the service's env. `null` removes a variable. |
| `services.<name>.command`, `.args` | Arrays of strings, or `null` to clear |
| `services.<name>.credentials` | Private registry `{ host, username, password }`, or `null` to clear. Always stored sealed. |
| `services.<name>.expose.<containerPort>` | Keyed by the container port the stored SDL declares: `port`, `as`, `accept` (replaces the custom-domain list), `httpOptions` (`maxBodySize`, `readTimeout`, `sendTimeout`, `nextTries`, `nextTimeout`, `nextCases`) |
| `services.<name>.storage.<volume>` | `mount` and `readOnly` only |
| `name` | Renames the deployment. On its own it sends no update and works even when Console holds no definition. |
| `sealedSecrets` | New values for the secret names this patch replaces; omitted names keep their stored values. See **@secrets.md**. |
| `ifManifestVersion` | Optimistic lock: the `manifestVersion` you read. Without it the patch still refuses to overwrite a concurrent one. |
```bash
curl -sX PATCH "https://console-api.akash.network/v1/deployments/$DSEQ" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"services": {
"worker": {
"image": "ghcr.io/acme/worker:3.0.1",
"env": { "QUEUE_CONCURRENCY": "8", "LEGACY_MODE": null }
}
}
}
}'
```
The response is the deployment, plus the `manifestVersion` the patch recorded. Env values written by a patch without `sealedSecrets` are stored sealed: the container gets them, but `GET` shows `ac-secret://` references in their place. To keep plain values readable, send a seal of an empty map with the patch (**@secrets.md**).
**What a patch can't change.** Compute resources (CPU, memory, storage sizes, GPUs), replica counts, groups and placement, and the protocol, kind or number of exposed endpoints are fixed when the deployment is created. Asking for them answers 422 `deployment_resources_changed`, and nothing is sent. To resize, create a new deployment from the stored SDL (`consoleSettings.sdl`, edited), pass `inheritSecretsFrom: <old dseq>` so its secrets carry over, lease it, then close the old one. Moving a global port onto or off `as: 80` also changes the endpoint kind and is refused.
**Errors:**
| Status | `code` | When |
|---|---|---|
| 400 | | Names a service, port or volume the stored SDL doesn't declare, leaves a secret reference without a value, or moves a port in a way that is refused |
| 403 | | `sealedSecrets` was sealed for another user, or bound to an SDL other than the stored one |
| 403 | `project_forbidden` | The deployment is filed in a project the key can't change |
| 404 | | Console holds no definition for this deployment; record one first (below) |
| 409 | `deployment_definition_changed` | Another patch landed first. Re-read, then re-send. A 409 without this code means the seal used a retired key. |
| 422 | `deployment_resources_changed` | The change needs a new deployment |
| 500 | `stored_secrets_unreadable`, `stored_sdl_unreadable` | The stored state is permanently unreadable; a retry won't help |
| 503 | | The key service was briefly unreachable. Retry. |
If the deployment update fails after the definition was recorded, re-send the identical request: it recomputes the same manifest version and finishes the update.
### Record a definition
```
POST /v1/deployments/{dseq}/definition
```
For a deployment Console holds no definition for (`consoleSettings` is `null`), such as one created before Console recorded definitions. Send `{ "data": { "sdl": "...", "sealedSecrets": "..." } }`. The SDL must produce exactly the manifest the deployment already runs, so nothing is sent to the provider; afterwards the deployment takes patches. Answers 201 with `{ sdl, manifestVersion }`, 409 `deployment_definition_exists` if a definition exists (patch instead), or 422 `deployment_definition_mismatch` if the SDL differs from what runs.
### Close a deployment
```
DELETE /v1/deployments/{dseq}
DELETE /v1/deployments/{dseq}?async=true
```
Closes the deployment and its leases. Whatever escrow is left returns to the account's available balance.
- Without `async`, the call returns `200 { "data": { "success": true } }` once the close went through.
- With `async=true`, Console checks the request, then answers `202 { "data": { "activityId": "<uuid>" } }` and finishes in the background. Poll `GET /v1/activities/{activityId}` until `status` is `succeeded` or `failed` (`meta.error` says why). The same call can still answer 200: when the deployment was already closed, or when background close isn't enabled for the account.
Closing an already-closed deployment is a no-op, so a retried close is safe. A deployment filed in a project the key can't change answers 403 `project_forbidden`.
```bash
RESP=$(curl -s -w '\n%{http_code}' -X DELETE "https://console-api.akash.network/v1/deployments/$DSEQ?async=true" \
-H "x-api-key: $AKASH_API_KEY")
CODE=$(echo "$RESP" | tail -n1)
if [ "$CODE" = "202" ]; then
ACTIVITY=$(echo "$RESP" | sed '$d' | jq -r .data.activityId)
until STATUS=$(curl -s "https://console-api.akash.network/v1/activities/$ACTIVITY" \
-H "x-api-key: $AKASH_API_KEY" | jq -r .data.status) && [ "$STATUS" != "pending" ]; do
sleep 5
done
echo "close $STATUS"
fi
```
`GET /v1/activities` lists recent actions newest first (`limit` up to 100, `cursor`, `status`, `type=deployment_close`), with a `nextCursor` for the next page.
### Move a deployment to another project
```
PATCH /v1/deployments/{dseq}/project
```
```bash
curl -sX PATCH "https://console-api.akash.network/v1/deployments/$DSEQ/project" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg project "$TARGET_PROJECT_ID" '{data: {projectId: $project}}')"
```
Refiles the deployment in another project of the same account or team and answers `{ "data": { "dseq", "projectId" } }`. Nothing changes on chain: the lease, the provider and the escrow stay. Only team owners and admins may move (403 otherwise), and 404 means the deployment or the project is out of the key's reach. See **@teams-and-projects.md**.
### Other deployment reads
| Endpoint | Auth | Returns |
|---|---|---|
| `GET /v1/deployment-names?dseq=…&dseq=…` | key | `{ data: { "<dseq>": name or null } }` |
| `GET /v1/deployment-locations/{dseq}` | key | `{ data: { organizationId, organizationSlug, projectId } }`: where the deployment is filed, within the key's reach |
| `GET /v1/weekly-cost` | key | `{ data: { weeklyCost } }`: USD per week across the running deployments of the key's account or team |
| `GET /v1/deployment-funding-config` | public | The constants automatic funding runs on (see **@account-and-funding.md**) |
| `GET /v1/deployment/{owner}/{dseq}` | public | A read-only view of any deployment: monthly cost, provider, events. For your own, prefer `GET /v1/deployments/{dseq}`. |
## Leases
### Accept bids
```
POST /v1/leases
```
**Body: flat, not wrapped in `data`.**
```json
{
"leases": [
{ "dseq": "1785245500842", "gseq": 1, "oseq": 1, "provider": "akash1providerA..." }
]
}
```
One call creates a lease per entry and sends each provider the manifest Console recorded at create. Copy `dseq`, `gseq`, `oseq` and `provider` from the bid's `bid.id`; leave out `owner` and `bseq`.
A deployment with several groups needs one entry per group, **all in the same call**. A second call for the same dseq finds the first call's lease, treats the request as a retry and creates nothing.
Don't send `manifest`. The field is deprecated, and Console only falls back to it for a deployment it holds no definition for (it answers 422 when it has neither).
```bash
BIDS=$(curl -s "https://console-api.akash.network/v1/bids?dseq=$DSEQ" -H "x-api-key: $AKASH_API_KEY")
CHEAPEST=$(echo "$BIDS" | jq -c '[.data[] | select(.bid.state == "open")] | sort_by(.bid.price.amount | tonumber) | .[0].bid.id')
curl -sX POST https://console-api.akash.network/v1/leases \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --argjson id "$CHEAPEST" '{leases: [{dseq: $id.dseq, gseq: $id.gseq, oseq: $id.oseq, provider: $id.provider}]}')"
```
The response is the deployment object. Lease status fills in once the provider starts the workload; poll `GET /v1/deployments/{dseq}`.
**Retries are safe.** If the leases already exist, the call skips creating them and only re-sends the manifest.
| Status | `code` | Meaning |
|---|---|---|
| 502 | `provider_unreachable` | The provider didn't answer. No lease was created; choose another bid. |
| 502 | `manifest_not_delivered` | The lease exists but the provider didn't take the manifest. Send the same request again, or close the deployment to stop paying for it. |
| 403 | `project_forbidden` | The deployment is filed in a project the key can't change |
| 400 | | A bid is no longer open, or the body is malformed |
There is no endpoint to close a single lease, and none to read one: close the deployment, and read leases from `GET /v1/deployments/{dseq}`.
## Bids
```
GET /v1/bids?dseq={dseq}
```
Poll every few seconds after creating the deployment; bids usually arrive within 5 to 30 seconds, and `data` is `[]` until then. `GET /v1/bids/{dseq}` returns the same thing.
```json
{
"data": [
{
"bid": {
"id": { "owner": "akash1...", "dseq": "1785245500842", "gseq": 1, "oseq": 1, "provider": "akash1prov...", "bseq": 21 },
"state": "open",
"price": { "denom": "uact", "amount": "1.523" },
"created_at": "...",
"resources_offer": [ { "resources": { "cpu": { "units": { "val": "500" } }, "gpu": { "units": { "val": "0" } } }, "count": 1 } ],
"reclamation_window": "3600s"
},
"escrow_account": { ... }
}
]
}
```
- `price.amount` is `uact` per block, as a decimal string. Compare with `tonumber`.
- Only `state: "open"` bids can be accepted.
- `resources_offer` is what the provider committed to; check it before accepting a GPU bid.
- `reclamation_window` is present when the provider may reclaim capacity with that much notice. See `rules/sdl/reclamation.md`.
No bids after a minute usually means no provider matches the SDL or the price is too low. Run the matcher in `rules/bid-matching/`.
## Runtime limits (deployment settings v2)
Automatic funding is always on. What these endpoints control is the **runtime limit**: an optional cap after which Console closes the deployment and returns the unspent credits.
```
GET /v2/deployment-settings/{dseq}
PATCH /v2/deployment-settings/{dseq}
POST /v2/deployment-settings
```
`GET` returns `runtimeLimitHours` (`null` for always-on), `runtimeEndsAt` (`null` until the lease starts), `sdl`, and the funding estimate fields (`estimatedTopUpAmount`, `topUpFrequencyMs`). A deployment created through `POST /v1/deployments` already has its settings row. One that answers 404 (an older deployment) gets one from `POST /v2/deployment-settings` with `{ "data": { "dseq": "..." } }`.
```bash
curl -sX PATCH "https://console-api.akash.network/v2/deployment-settings/$DSEQ" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"runtimeLimitHours": 96}}'
```
- Send the new **total**, not an increment. A limit can be raised by at most 48 hours per request, up to 8760; a deployment without a limit can be given one of at most 48.
- Lowering a limit is not supported. Send `null` to drop it and return to always-on funding.
- `closeReason` (`no_longer_needed`, `cost_or_budget`, `migrating_elsewhere`, `performance_or_reliability`, `testing_or_project_complete`, `other`) and `closeReasonDetails` record why a deployment was closed. They can't be sent together with `runtimeLimitHours`.
- 409 means another change to the limit landed first; re-read and retry.
`autoTopUpEnabled` is accepted for backwards compatibility and an explicit `false` is rejected. The older `/v1/deployment-settings/*` paths are UI-internal.
## Providers and placement
All public, no key needed.
| Endpoint | Returns |
|---|---|
| `GET /v1/providers` | Every provider, offline ones included, as a bare array. Filter on `isOnline` and `isAudited`. `?addresses=a,b` narrows it; `?scope=trial` lists providers a free-trial account can lease from. |
| `GET /v1/providers/{address}` | One provider: `hostUri`, `attributes`, `isOnline`, `isAudited`, uptime, `stats` (capacity), `gpuModels`, `reclamationWindow` (seconds of notice before reclaiming capacity, or `null`), `gpuDrivers` (NVIDIA driver and CUDA versions Console confirmed on its leases), `reportedCpuArchs` |
| `GET /v1/provider-search` | Providers filtered and paged: `search`, `online`, `audited`, `regions`, `gpu`, `gpuModels`, `sort`, `skip`, `limit` (≤100). Answers `{ data: { providers, pagination } }`. |
| `GET /v1/placement-options` | Regions with audited online providers (`regions`, `regionProviderCounts`) and the GPUs they can serve right now: per model, `providerCount`, `availableUnits`, `maxNodeFreeUnits`, and the memory/interface variants providers would bid on |
| `GET /v1/provider-regions`, `/v1/provider-attributes-schema`, `/v1/auditors` | Placement-attribute vocabularies for SDL editors |
`GET /v1/providers/{address}` is where the provider's `hostUri` comes from for logs and shell. It is the object itself: `.hostUri`, not `.data.hostUri`.
Check `GET /v1/placement-options` before writing a GPU SDL: a model with `providerCount: 0` gets no bids, and a replica asking for more GPUs than `maxNodeFreeUnits` can't fit on one node.
## Bid screening
```
POST /v1/bid-screening
```
Public. Predicts which providers would bid on a group without creating anything. **Body: flat**, with resources in their on-chain form (what `GET /v1/deployments/{dseq}` returns under `groups[].group_spec`):
```json
{
"requirements": { "signedBy": { "allOf": [], "anyOf": [] }, "attributes": [] },
"resources": [
{
"resource": {
"id": 1,
"cpu": { "units": { "val": "500" } },
"memory": { "quantity": { "val": "536870912" } },
"gpu": { "units": { "val": "0" } },
"storage": [{ "name": "default", "quantity": { "val": "1073741824" } }]
},
"count": 1,
"price": { "denom": "uact", "amount": "1000" }
}
],
"timezone": "UTC"
}
```
CPU is in millicores, memory and storage in bytes, all as strings. `timezone` is required (an IANA zone; it buckets the incident history). `reclamationWindow` (seconds) keeps only providers that give at least that much notice.
**Response:** `{ "providers": [{ "owner", "hostUri", "isAudited", "location", "organization", "availableGpus", "incidents": [...] }] }`.
From an SDL file, the matcher in `rules/bid-matching/` is simpler: it reads the SDL directly and reports why providers drop out.
## Deprecated endpoints
### Full-SDL update
```
PUT /v1/deployments/{dseq}
```
Deprecated in favor of `PATCH` and scheduled for removal. It resubmits the whole SDL, so every secret value has to be resupplied, and it refuses resource changes with the same 422 `deployment_resources_changed`. Don't generate new code for it.
### Deposit
```
POST /v1/deposit-deployment
```
Deprecated and scheduled for removal. Console tops every deployment up automatically while the account has credits, so there is nothing for it to do. If an account runs low, the fix is adding credits in the Console UI. See [How Funding Works](https://akash.network/docs/getting-started/how-funding-works/).
## Endpoints that do not exist
Older docs, and models trained on them, invent these. None exist:
| Invented | Use |
|---|---|
| `POST /deployment`, `GET /deployment/{dseq}`, `DELETE /deployment/{dseq}` | `/v1/deployments`, `/v1/deployments/{dseq}` |
| `POST /deployment/{dseq}/deposit`, `POST /wallet/deposit` | Nothing: funding is automatic; credits are added in the Console UI |
| `POST /lease` (single), `GET` or `DELETE /lease/{dseq}/{gseq}/{oseq}` | `POST /v1/leases` (batch); read leases from `GET /v1/deployments/{dseq}`; close the deployment |
| `GET /providers/{address}/status` | `GET /v1/providers/{address}` |
| `POST /sdl/validate`, `POST /v1/sdl/validate` | Validate client-side; `POST /v1/deployments` answers 400 for an invalid SDL |
| `POST /sdl/price` | No pricing endpoint. Prices come from bids (`GET /v1/bids?dseq=`). |
| `POST /wallet/create` | Accounts are created in the Console UI |
| `GET /wallet/balance` | `GET /v1/balances` |
| `POST /v1/auth/refresh` | Re-mint with `POST /v1/create-jwt-token` |
| `console-api.akash.network/v1/proxy/...`, `/v1/logs/...` | Logs come from the provider (**@operations.md**) |
rules/deploy/console-api/operations.md
# Operations — Logs, Events, Status, Shell and Attestation
Once a deployment runs, logs, Kubernetes events, live status, shell access and TEE attestation all come from the **provider**, not from `console-api.akash.network`. Each call carries a short-lived JWT. Managed-wallet users mint it with the Console API; self-custody users sign it locally. Everything after minting is identical.
```
your client ──HTTPS/WSS, Authorization: Bearer <jwt>──▶ provider hostUri
```
There is no Console API passthrough: `console-api.akash.network/v1/proxy/...` and `/v1/logs/...` don't exist.
## Step 1 — Find the lease and the provider's hostUri
```bash
DEPL=$(curl -s "https://console-api.akash.network/v1/deployments/$DSEQ" -H "x-api-key: $AKASH_API_KEY")
PROVIDER=$(echo "$DEPL" | jq -r '.data.leases[0].id.provider')
GSEQ=$(echo "$DEPL" | jq -r '.data.leases[0].id.gseq')
OSEQ=$(echo "$DEPL" | jq -r '.data.leases[0].id.oseq')
HOSTURI=$(curl -s "https://console-api.akash.network/v1/providers/$PROVIDER" | jq -r '.hostUri')
```
`GET /v1/providers/{address}` is public and returns the provider object itself, so it's `.hostUri`, not `.data.hostUri`. The provider's on-chain record is the only source of the URL; don't derive it from anything else. A deployment with several groups has one lease per group; pick the lease by `gseq` and `oseq`.
## Step 2 — Mint a JWT
### Managed wallet (Console API)
```bash
JWT=$(curl -sX POST https://console-api.akash.network/v1/create-jwt-token \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"ttl": 1800, "leases": {"access": "scoped", "scope": ["status", "logs", "events"]}}}' \
| jq -r .data.token)
```
`ttl` is in seconds and required; the Console UI uses 1800. There is no refresh endpoint: mint a new token when one expires. The response is `201 { "data": { "token": "<jwt>" } }`.
**Keys limited to some projects.** A key bound to a project, a request with `x-project-id`, and the key of a team `member` or `viewer` (only owners and admins reach every project) can't mint `full` or `scoped` tokens: those answer 403 `project_forbidden`. They name each deployment with the `granular` shape below, `granular` on every provider entry too, and only deployments the key reaches. In a team, `ttl` is at most 3600. See **@teams-and-projects.md**.
### Self-custody (CLI or SDK)
Console can't sign for a key it doesn't hold. Sign locally with `@akashnetwork/chain-sdk`:
```typescript
import { JwtTokenManager } from "@akashnetwork/chain-sdk";
const jwt = await new JwtTokenManager(wallet).generateToken({
version: "v1",
iss: address,
exp: Math.floor(Date.now() / 1000) + 1800,
leases: { access: "scoped", scope: ["status", "logs", "events"] }
});
```
`iat` and `nbf` default to now. The payload follows [AEP-64](https://akash.network/roadmap/aep-64/), and the provider checks it the same way whoever signed it.
### The `leases` claim
Three shapes; the JWT schema rejects anything else.
```json
{ "access": "full" }
```
Every action on every lease of the wallet (your personal account's, or the key's team's). **`full` takes no `scope`**; adding one makes the token invalid.
```json
{ "access": "scoped", "scope": ["logs", "status"] }
```
The listed actions on every lease of the wallet.
```json
{
"access": "granular",
"permissions": [
{ "provider": "akash1provA...", "access": "scoped", "scope": ["logs", "status"] },
{
"provider": "akash1provB...",
"access": "granular",
"deployments": [{ "dseq": 1785245500842, "gseq": 1, "oseq": 1, "services": ["web"], "scope": ["shell"] }]
}
]
}
```
Per provider: `full`, `scoped` with a `scope`, or `granular` with `deployments`. Each `deployments` entry needs `dseq` (a number), `scope` and `services` (at least one service name); `gseq` and `oseq` are optional. Each provider may appear once. A key limited to some projects may only use the `granular` form, on every provider.
**Scopes:** `send-manifest`, `get-manifest`, `logs`, `shell`, `events`, `status`, `restart`, `hostname-migrate`, `ip-migrate`, `attestation`. Grant the narrowest set that works: a token with `shell` can read the container's environment, sealed secrets included.
## Step 3 — Call the provider
All paths sit under `{hostUri}/lease/{dseq}/{gseq}/{oseq}`.
### Status (HTTPS)
```bash
curl -sk "$HOSTURI/lease/$DSEQ/$GSEQ/$OSEQ/status" -H "Authorization: Bearer $JWT"
```
Scope `status`. Returns per-service readiness, forwarded ports and IPs, freshly polled: the same data as `leases[].status` in `GET /v1/deployments/{dseq}`.
### Logs (WebSocket)
```
WSS {hostUri}/lease/{dseq}/{gseq}/{oseq}/logs?follow=true&tail=200&service=web
```
Scope `logs`. `follow`, `tail` and `service` are optional.
```bash
websocat -k "wss://${HOSTURI#https://}/lease/$DSEQ/$GSEQ/$OSEQ/logs?follow=true&tail=200" \
-H "Authorization: Bearer $JWT"
```
### Events (WebSocket)
```
WSS {hostUri}/lease/{dseq}/{gseq}/{oseq}/kubeevents
```
Scope `events`. These are the Kubernetes events of the deployment's pods: image pulls, crash loops, scheduling failures. The path is always `kubeevents`; `events` is only an alias inside the Console UI.
### Shell (WebSocket)
```
WSS {hostUri}/lease/{dseq}/{gseq}/{oseq}/shell?service=<svc>&podIndex=<n>&tty=0&stdin=0&cmd0=<arg0>&cmd1=<arg1>...
```
Scope `shell`. Required query params: `service`, `podIndex`, `tty` (`1` or `0`), `stdin` (`1` or `0`), and the command as separate numbered params `cmd0`, `cmd1`, … (`cmd0=/bin/sh&cmd1=-c&cmd2=ls`). There is no single `cmd` param, and nothing is base64-encoded.
Frames are binary, and the **first byte is the stream code**: from the server `100` stdout, `101` stderr, `102` result (JSON such as `{"exit_code":0}`), `103` failure; from the client `104` stdin, `105` terminal resize. Strip the first byte before printing the payload.
```typescript
import WebSocket from "ws";
import https from "https";
const params = new URLSearchParams({ service, podIndex: "0", tty: "0", stdin: "0" });
["/bin/sh", "-c", "echo hello && hostname"].forEach((arg, i) => params.append(`cmd${i}`, arg));
const ws = new WebSocket(`wss://${hostUri.replace(/^https?:\/\//, "")}/lease/${dseq}/${gseq}/${oseq}/shell?${params}`, {
headers: { Authorization: `Bearer ${jwt}` },
agent: new https.Agent({ rejectUnauthorized: false })
});
ws.on("message", (frame: Buffer) => {
const code = frame[0], data = frame.subarray(1);
if (code === 100) process.stdout.write(data);
else if (code === 101) process.stderr.write(data);
else if (code === 102) { console.log("result:", data.toString()); ws.close(); }
});
```
That is a one-shot command. For an interactive session set `tty=1&stdin=1`, send keystrokes as `104`-prefixed frames and resizes as `105` frames, or use `provider-services lease-shell --tty` (see [Shell Access](https://akash.network/docs/learn/core-concepts/shell-access/)).
### Attestation (Confidential Compute)
For a deployment with `params.tee` (see `rules/sdl/confidential-compute.md`), the provider returns hardware-signed evidence, and the Console API checks it against the hardware vendors.
1. Mint a JWT with the `attestation` scope.
2. Ask the provider for a quote, sending a fresh nonce of 64 random bytes, base64-encoded:
```bash
NONCE=$(openssl rand -base64 64 | tr -d '\n')
QUOTE=$(curl -skX POST "$HOSTURI/lease/$DSEQ/$GSEQ/$OSEQ/attestation/quote?service=web" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg nonce "$NONCE" '{nonce: $nonce, bind_tls: false}')")
```
The response carries `report`, `tee_platform` (`snp`, `tdx`, `snp-gpu` or `tdx-gpu`), `cert_chain`, `auxblob` and, on GPU platforms, `gpu_reports`. `service` and `podIndex` pick the pod; both are optional.
3. Validate it, sending the same nonce:
```bash
curl -sX POST https://console-api.akash.network/v1/confidential-compute/attestation/validate \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(echo "$QUOTE" | jq -c --arg nonce "$NONCE" '{nonce: $nonce, report, tee_platform, cert_chain: (.cert_chain // ""), auxblob: (.auxblob // ""), gpu_reports: (.gpu_reports // [])}')"
```
The body is flat. The answer has `overall` (`valid`, `invalid` or `unverifiable`) and one verdict per report under `reports`, with `checks` for the certificate chain, signature, nonce binding and revocation. `unverifiable` means a check couldn't run, for example because a vendor service was unreachable; it is not a failure of the hardware.
### Push a manifest (HTTPS PUT)
```
PUT {hostUri}/deployment/{dseq}/manifest
```
Scope `send-manifest`. Managed-wallet users never call this: `POST /v1/leases` and `PATCH /v1/deployments/{dseq}` send manifests for you. Self-custody users usually let the SDK do it.
## Provider TLS
Providers serve HTTPS with a **self-signed** certificate, so default HTTPS clients reject it. The JWT is the authentication; there is no client certificate to manage (the old mTLS flow is in **@../cli/mtls-legacy.md**).
For direct, server-side calls, turn certificate verification off for the provider connection only: `curl -k`, `websocat -k`, `tls.Config{InsecureSkipVerify: true}` in Go. In Node it depends on the client:
- `ws` WebSockets take `agent: new https.Agent({ rejectUnauthorized: false })`, as in the examples on this page.
- `fetch` ignores `agent`. Use the `undici` package (`npm install undici`) and pass its `Agent` as `dispatcher`:
```typescript
import { Agent, fetch } from "undici";
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
try {
const res = await fetch(`${hostUri}/lease/${dseq}/${gseq}/${oseq}/status`, {
headers: { Authorization: `Bearer ${jwt}` },
dispatcher
});
console.log(await res.json());
} finally {
await dispatcher.close();
}
```
Don't hand-roll a check of the certificate against the provider's on-chain address; first-class provider-certificate verification is planned for `@akashnetwork/chain-sdk`.
Browsers can't skip verification. A browser app routes through a provider proxy, a server that holds the provider connection and exposes a CORS-friendly endpoint; Console runs one built from `apps/provider-proxy/` in the [console monorepo](https://github.com/akash-network/console). Its hosted URL is not a public contract, so don't hard-code it.
## Full Node.js example: stream logs
```typescript
import WebSocket from "ws";
import https from "https";
const API = "https://console-api.akash.network";
const headers = { "x-api-key": process.env.AKASH_API_KEY!, "Content-Type": "application/json" };
const dseq = process.env.DSEQ!;
const { data: deployment } = await (await fetch(`${API}/v1/deployments/${dseq}`, { headers })).json();
const { provider, gseq, oseq } = deployment.leases[0].id;
const { hostUri } = await (await fetch(`${API}/v1/providers/${provider}`)).json();
const { data: jwt } = await (
await fetch(`${API}/v1/create-jwt-token`, {
method: "POST",
headers,
body: JSON.stringify({ data: { ttl: 1800, leases: { access: "scoped", scope: ["logs"] } } })
})
).json();
const ws = new WebSocket(`wss://${hostUri.replace(/^https?:\/\//, "")}/lease/${dseq}/${gseq}/${oseq}/logs?follow=true&tail=100`, {
headers: { Authorization: `Bearer ${jwt.token}` },
agent: new https.Agent({ rejectUnauthorized: false })
});
ws.on("message", chunk => process.stdout.write(chunk.toString()));
ws.on("close", () => process.exit(0));
```
## Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| 401 or 403 from the provider | Token expired, or its scope lacks the action | Mint a new token with the needed scope |
| 400 from `create-jwt-token` | Malformed `leases` claim, such as `scope` on `access: "full"`, or a `ttl` over 3600 in a team | Use one of the three shapes above |
| 403 `project_forbidden` from `create-jwt-token` | The key is limited to some projects and the token doesn't name its deployments, or names one outside them | `granular`, listing each deployment of the key's projects |
| 404 on `/events` | `events` is a UI alias | Use `/kubeevents` |
| Connection refused | Wrong host | Resolve `hostUri` from `GET /v1/providers/{address}` |
| TLS certificate rejected | Provider certificate is self-signed | Skip verification server-side, or use a provider proxy |
| Empty log stream | The workload hasn't started | Check `/status` first |
| Stream dies after a while | The token expired | Mint a new one and reconnect |
| Attestation quote fails | Not a TEE deployment, or no `attestation` scope | Check `params.tee` in the SDL and the token's scope |
## Related files
- **@authentication.md** — API keys
- **@deployment-endpoints.md** — Where `provider`, `gseq` and `oseq` come from
- **@api-key-quickstart.md** — End to end, with logs at the end
- **@teams-and-projects.md**: Tokens for keys limited to some projects
- **@../../sdk/typescript/provider-sdk.md** — The same flow through the SDK's provider client
- **AEP-64** — https://akash.network/roadmap/aep-64/ — the JWT specification
rules/deploy/console-api/overview.md
# Console API Overview
The Console API is the REST surface behind [Akash Console](https://console.akash.network). An API key authenticates as the Console user who created it, acting in one personal account or team, and that account's or team's managed wallet funds and signs every deployment action. A key can also be limited to one project; see **@teams-and-projects.md**. This file covers the deployment-management subset; the rest of the spec powers the Console UI.
With `akt` installed, `akt deploy` and the `akt console` commands make these calls for you: see [../akt/overview.md](../akt/overview.md).
## Base URL and spec
```
https://console-api.akash.network
```
Every path carries its own version prefix: `/v1/...` for almost everything, `/v2/deployment-settings` for runtime limits.
| Resource | URL |
|---|---|
| OpenAPI spec (JSON) | `https://console-api.akash.network/v1/doc` |
| Swagger UI | `https://console-api.akash.network/v1/swagger` |
| Official API reference | [akash.network/docs/api-documentation/console-api/api-reference](https://akash.network/docs/api-documentation/console-api/api-reference/) |
**The live spec is the contract.** It flags deprecated endpoints and fields with `deprecated: true` and describes each error case per endpoint. If this skill, the official reference and the spec disagree, trust the spec; the reference can trail a release by weeks.
## Authentication
| Credential | Header | Use |
|---|---|---|
| **API key** (`ac.sk.production.…`) | `x-api-key: <key>` | Scripts, CI/CD, backends |
| Console session JWT | `Authorization: Bearer <jwt>` | The Console web app's own login session; not for scripts |
**Never put an API key in `Authorization: Bearer`.** Keys are created in the Console UI; see **@authentication.md**. Calls to a *provider* (logs, shell, status) use a different JWT, minted by `POST /v1/create-jwt-token`; see **@operations.md**.
A key always acts in the team it was created in. Two optional headers narrow a request: `x-organization-id` (can only repeat the key's own team) and `x-project-id` (one project the key reaches). Scripts rarely need either; see **@teams-and-projects.md**.
## Envelopes
**Requests.** Write endpoints wrap the payload in `data`:
```json
{ "data": { "sdl": "version: \"2.0\"\n..." } }
```
Three endpoints take a flat body instead: `POST /v1/leases`, `POST /v1/bid-screening` and `POST /v1/confidential-compute/attestation/validate`.
**Responses.** Account and deployment endpoints answer `{ "data": ... }`. A few answer with the object itself: `GET /v1/providers`, `GET /v1/providers/{address}`, `GET /v1/placement-options`, `POST /v1/bid-screening`, `GET /v1/blockchain-status` and `POST /v1/confidential-compute/attestation/validate`. So a provider's URL is `.hostUri`, not `.data.hostUri`.
## Errors
Every error body has the same shape:
```json
{
"error": "BadRequestError",
"message": "Validation error",
"code": "validation_error",
"type": "validation_error",
"data": [ ... ]
}
```
Branch on the HTTP status, then on `code` where it is specific. `message` is for humans and changes wording without notice. When a status has no specific code, `code` is the generic one for it: `bad_request`, `unauthorized`, `payment_required`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `service_unavailable` or `internal_server_error`. A request that fails schema validation answers 400 `validation_error`, with the failing fields in `data`.
The specific codes a deployment script should handle:
| Status | `code` | Meaning | Do this |
|---|---|---|---|
| 402 | `insufficient_balance` | Not enough available credit to fund a new deployment. `data.requiredAmountUsd` and `data.availableAmountUsd` give the shortfall. | Add credits in the Console UI, then retry |
| 402 | `balance_top_up_pending` | Auto recharge is already charging the card for the shortfall | Wait `Retry-After` seconds, then retry |
| 402 | `payment_required` | The fee allowance ran out, or a free-trial account asked for a GPU the trial doesn't allow | Add credits; trial accounts upgrade by adding credits |
| 502 | `provider_unreachable` | `POST /v1/leases`: the provider did not answer. No lease was created. | Choose another bid |
| 502 | `manifest_not_delivered` | `POST /v1/leases`: the lease exists but the provider did not take the manifest | Send the identical request again, or close the deployment |
| 422 | `deployment_resources_changed` | An update tried to change compute resources, replica counts, groups or globally exposed ports | Create a new deployment instead |
| 409 | `deployment_definition_changed` | A concurrent `PATCH` changed the deployment first | Re-read the deployment, then re-send |
| 403 | `project_forbidden` | The request reaches outside its projects: a write on a deployment filed in a project the key can't change, an `x-project-id` it can't reach, or a provider JWT that doesn't name its deployments | Stay within the key's projects; see **@teams-and-projects.md** |
| 400 | `project_required` | `POST /v1/deployments` found no project to file the deployment in | Pass `data.projectId` |
| 403 | `project_budget_exceeded` | The deposit would take the project past its monthly budget | An owner or admin raises the budget; don't retry |
| 403 | `organization_forbidden` | The key's creator is no longer a member of its team | Use a key of a current member |
| 400 | `organization_mismatch` | `x-organization-id` names another team than the key's | Drop the header, or use that team's key |
| 403 | `session_required` | Creating, leaving or deleting a team, or accepting an invitation, with an API key | Do it in the Console web app |
`PATCH /v1/deployments/{dseq}` and the secrets flow have a few more; they are listed next to those endpoints in **@deployment-endpoints.md** and **@secrets.md**.
## Pagination and rate limits
- `GET /v1/deployments` pages by offset: `skip` plus `limit` (at most 100), and the response carries `data.pagination.hasMore`. `GET /v1/activities` pages by cursor (`nextCursor`).
- On `429`, wait for `Retry-After` and back off exponentially. Don't hard-code a request budget: limits are set server-side and change.
## Endpoint map
| Task | Endpoint | Reference |
|---|---|---|
| Create a deployment | `POST /v1/deployments` | @deployment-endpoints.md |
| List or read deployments | `GET /v1/deployments`, `GET /v1/deployments/{dseq}` | @deployment-endpoints.md |
| Change image, env, command, ports or name | `PATCH /v1/deployments/{dseq}` | @deployment-endpoints.md |
| Close a deployment | `DELETE /v1/deployments/{dseq}` | @deployment-endpoints.md |
| List bids, accept bids | `GET /v1/bids?dseq=`, `POST /v1/leases` | @deployment-endpoints.md |
| Runtime limit | `GET`, `PATCH /v2/deployment-settings/{dseq}` | @deployment-endpoints.md |
| Provider details, regions, GPU availability | `GET /v1/providers/{address}`, `GET /v1/placement-options` | @deployment-endpoints.md |
| Secrets that can't be read back | `GET /v1/sdl-secrets-context` + `sealedSecrets` | @secrets.md |
| Balance and funding | `GET /v1/balances` | @account-and-funding.md |
| API keys | `/v1/api-keys` | @authentication.md |
| The key's team and projects | `GET /v1/organizations`, `GET /v1/projects` | @teams-and-projects.md |
| Deploy into a project, list by project | `data.projectId` on create, `GET /v1/deployments?projectId=` | @teams-and-projects.md |
| Move a deployment to another project | `PATCH /v1/deployments/{dseq}/project` | @teams-and-projects.md |
| Logs, events, status, shell, attestation | `POST /v1/create-jwt-token`, then the provider | @operations.md |
For a linear walkthrough from an API key to a running deployment: **@api-key-quickstart.md**.
## Deprecated: don't generate code for these
| Deprecated | Use instead |
|---|---|
| `PUT /v1/deployments/{dseq}` (resubmits the whole SDL) | `PATCH /v1/deployments/{dseq}` |
| `manifest` in the `POST /v1/leases` body | Leave it out. Console sends the manifest it recorded when the deployment was created. |
| `POST /v1/deposit-deployment`, and `deposit` on create | Nothing. Console funds every deployment from the account's credits. |
| `POST /v1/certificates` (always answers 400) | Nothing. The API key replaces mTLS certificates. |
## UI-internal endpoints: not covered
The spec also exposes endpoints that exist to power the Console UI. They change without notice; don't write code against them.
| Surface | Endpoint pattern | Instead |
|---|---|---|
| Signup, login, email verification, account deletion | `/v1/auth/*`, `/v1/register-user`, `/v1/send-verification-*`, `/v1/verify-email*`, `/v1/user/me/*-deletion` | Console UI |
| Stripe payments, adding credits, Auto recharge | `/v1/stripe/*`, `/v1/wallet-settings` | Console UI → Billing (for a team, its owner or a billing member, on the team's Billing page) |
| Profile, favorites, saved templates, newsletter | `/v1/user/*`, `/v1/favorite-providers` | Console UI |
| Alerts and notification channels | `/v1/alerts/*`, `/v1/deployment-alerts/*`, `/v1/notification-channels/*` | Console UI → Alerts |
| Deploy-flow drafts, hardware requests, feed state | `/v1/configure-drafts/*`, `/v1/hardware-requests`, `/v1/activities/seen` | Console UI |
| Legacy UI signing | `/v1/tx` (accepts six message types) | The dedicated deployment endpoints |
| Dashboards, analytics, explorer | `/v1/dashboard-data`, `/v1/graph-data/*`, `/v1/bme/*`, `/v1/blocks/*`, `/v1/transactions/*`, `/v1/validators/*`, `/v1/proposals/*`, `/v1/gpu*`, `/v1/provider-dashboard/*`, `/v1/market-data/*` | Console UI, or a chain node for raw chain data |
| Template gallery | `/v1/templates-list`, `/v1/templates/{id}` | [awesome-akash](https://github.com/akash-network/awesome-akash) |
## Related files
- **@authentication.md** — API keys: format, creation, CRUD, CI/CD
- **@deployment-endpoints.md** — Every deployment endpoint with bodies, responses and errors
- **@secrets.md** — `ac-secret://` references and sealed secrets
- **@api-key-quickstart.md** — From an API key to a running deployment
- **@account-and-funding.md** — Account model, balance, automatic funding, 402s
- **@operations.md** — Provider JWT, logs, events, status, shell, attestation
- **@teams-and-projects.md**: Teams, projects, keys bound to them, team billing, the team errors
rules/deploy/console-api/secrets.md
# Console API — Secrets
Console records each deployment's SDL so it can send manifests and take patches later. This file covers how values in that SDL are protected, and how to pass secrets that no endpoint ever returns.
## What Console stores readable
The stored SDL comes back from `GET /v1/deployments/{dseq}` (`consoleSettings.sdl`) and `GET /v2/deployment-settings/{dseq}` (`sdl`). Which values it shows depends on one thing: whether the create sent `sealedSecrets`.
| Create request | Env values | Registry credentials |
|---|---|---|
| No `sealedSecrets` | Every value is stored encrypted and shown as an `ac-secret://` reference | Encrypted |
| With `sealedSecrets`, even a seal of `{}` | Stored as written, except the ones the SDL references as `ac-secret://NAME` | Encrypted |
The container always receives the real values. So a plain SDL with secrets in `env` is already safe from later reads, at the cost of every env value becoming unreadable. For readable config plus a few real secrets, reference the secrets and seal them, as below. That also keeps the secrets out of the SDL text and out of the request body in plaintext.
## Referencing a secret in the SDL
Replace the value with `ac-secret://NAME`:
```yaml
services:
app:
image: ghcr.io/acme/app:1.8.0
env:
- "NODE_ENV=production"
- "SMTP_PASSWORD=ac-secret://SMTP_PASSWORD"
- "SESSION_SIGNING_KEY=ac-secret://SESSION_SIGNING_KEY"
```
- `NAME` matches `[A-Za-z_][A-Za-z0-9_]{0,63}`, and the reference must be the whole value. `KEY=prefix-ac-secret://X` is not a reference.
- References work in `env` values and in a private registry's `credentials.username` and `credentials.password`.
- Any value starting with `ac-` is reserved: a value that starts with `ac-` but isn't a valid reference is refused.
- Every reference needs a value, and every sealed value needs a reference. Either mismatch answers 400.
- At most 100 secrets per deployment.
## Sealing the values
`sealedSecrets` is a compact JWE whose plaintext is a flat JSON object of names to string values, encrypted to Console's public key.
1. `GET /v1/sdl-secrets-context` returns `{ "data": { "kid", "sub", "jwk", "requiredClaims" } }`: the key to seal to, its key id, and your user id.
2. Encrypt the JSON with `alg: "RSA-OAEP-256"` and `enc: "A256GCM"`. Put these in the protected header:
- `kid` and `sub`, copied from the context
- `exp`: unix seconds, at most 15 minutes ahead (5 is a good default)
- `sdlHash` (recommended on create): base64url of the SHA-256 of the **exact** `sdl` string you send. It binds the seal to that SDL, so the seal can't be replayed with another one.
3. Send it as `data.sealedSecrets` beside `data.sdl`.
Node.js with [`jose`](https://www.npmjs.com/package/jose):
```typescript
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { CompactEncrypt, importJWK } from "jose";
const API = "https://console-api.akash.network";
const headers = { "x-api-key": process.env.AKASH_API_KEY!, "Content-Type": "application/json" };
async function sealSecrets(secrets: Record<string, string>, sdl?: string): Promise<string> {
const { data: context } = await (await fetch(`${API}/v1/sdl-secrets-context`, { headers })).json();
const key = await importJWK({ ...context.jwk, alg: "RSA-OAEP-256" }, "RSA-OAEP-256");
const sdlHash = sdl === undefined ? {} : { sdlHash: createHash("sha256").update(sdl, "utf8").digest("base64url") };
return new CompactEncrypt(new TextEncoder().encode(JSON.stringify(secrets)))
.setProtectedHeader({
alg: "RSA-OAEP-256",
enc: "A256GCM",
kid: context.kid,
sub: context.sub,
exp: Math.floor(Date.now() / 1000) + 5 * 60,
...sdlHash
})
.encrypt(key);
}
const sdl = readFileSync("deploy.yaml", "utf8");
const sealedSecrets = await sealSecrets(
{
SMTP_PASSWORD: process.env.SMTP_PASSWORD!,
SESSION_SIGNING_KEY: process.env.SESSION_SIGNING_KEY!
},
sdl
);
const res = await fetch(`${API}/v1/deployments`, {
method: "POST",
headers,
body: JSON.stringify({ data: { sdl, sealedSecrets } })
});
const { data } = await res.json();
```
Hash the same string you put in `data.sdl`. Reformatting the YAML after hashing it gives a 403.
Python can do the same with `jwcrypto` or `authlib`; any JOSE library that supports `RSA-OAEP-256` with `A256GCM` and custom protected-header fields works.
## Changing secrets later
`PATCH /v1/deployments/{dseq}` takes a `sealedSecrets` holding only the names to replace; every other stored value stays. Seal it without `sdlHash`, because a patch is checked against the SDL Console stores rather than one you send. The new value reaches the workload through the deployment update the patch sends.
```json
{ "data": { "sealedSecrets": "<JWE of {\"SMTP_PASSWORD\": \"new-value\"}>" } }
```
A patch that writes env values through `services.<name>.env` stores them encrypted unless it carries `sealedSecrets`. To write a plain, readable variable, send a seal of `{}` with the patch.
## Redeploying without re-entering values
`inheritSecretsFrom: "<dseq>"` on create starts the new deployment from the secrets stored for another deployment you can change in the same account or team, even a closed one. Values in `sealedSecrets` override inherited names. This is how to resize: take `consoleSettings.sdl` from the old deployment (its references are already in place), change the resources, create with `inheritSecretsFrom`, lease it, then close the old one.
## Errors
| Status | When | Fix |
|---|---|---|
| 400 | Malformed, tampered or expired seal; wrong `alg` or `enc`; a reference without a value; a sealed name nothing references | Fix the request |
| 403 | `sub` is not you, or `sdlHash` doesn't match the SDL | Reseal from a fresh context, hashing the exact SDL string |
| 409 | The seal names a key Console no longer holds | Fetch `GET /v1/sdl-secrets-context` again and reseal |
| 409 `inherited_secrets_unreadable` | The source deployment's secrets can no longer be decrypted | Permanent. Supply the values in `sealedSecrets`. |
| 503 | The key service is briefly unreachable | Retry |
## What secrets don't cover
- **Shell access.** A JWT with the `shell` scope can read the container's environment. Grant `shell` only where it's needed (**@operations.md**).
- **Self-custody deployments.** The CLI and SDKs send the manifest straight to the provider; nothing is sealed or stored.
rules/deploy/console-api/teams-and-projects.md
# Console API: Teams and Projects
Every Console user has a **personal account** and can belong to any number of **teams**. The API calls both *organizations* (`type` is `personal` or `team`). Each one has its own managed wallet and credits, and files its deployments into **projects**. Every organization, personal accounts included, has a **default project**.
An API key is created in one organization and acts only there, with its creator's role, optionally limited to one project. It never reaches its creator's other teams or personal account. A user who belongs to no team sees no change: requests run in the personal account and deployments land in its default project.
## Roles
Team roles, as `role` in `GET /v1/organizations`:
| Role | Deploys and changes deployments | Reads deployments | Changes team billing | Members, projects, budgets |
|---|---|---|---|---|
| `owner` | Every project | Every project | Yes | Everything, including deleting the team |
| `admin` | Every project | Every project | No | Everything except owners |
| `member` | Its projects | Its projects | No | Only projects where it is a project admin |
| `billing` | No | No | Yes | No |
| `viewer` | No | Its projects | No | No |
A member or viewer reaches **all projects** or **selected ones**. On a selected project its project role decides: `viewer` reads it, `member` also deploys into it and changes its deployments, `admin` also renames it and manages who else reaches it. A team `viewer` only reads, whatever its project role. In a personal account the user is the owner.
## Which team and project a request runs in
| Credential | Team | Projects |
|---|---|---|
| API key | The team the key was created in, always. A key from before teams (`organizationId: null`) acts in its creator's personal account. | A key bound to a project reaches only that one. Otherwise, every project its creator reaches. |
| Console session (`Authorization: Bearer`, the web app) | `x-organization-id`, else the team used last, else the personal account | Every project the user reaches |
The key follows its creator: if the creator's role changes, so do the key's rights, and once the creator leaves or is removed from the team, the key answers 403 `organization_forbidden`.
Two optional headers narrow a request:
| Header | Value | With an API key | Refused with |
|---|---|---|---|
| `x-organization-id` | Team id or slug | Can only repeat the key's own team; a key never switches teams | 400 `organization_mismatch` for another team than the key's; 403 `organization_forbidden` for a session that isn't a member |
| `x-project-id` | Project id (a UUID) | Limits the request to one project the key reaches | 403 `project_forbidden` |
Scripts with an API key need neither header. To act in another team, use a key created in that team.
### Find where a key acts
```bash
API=https://console-api.akash.network
curl -s "$API/v1/organizations" -H "x-api-key: $AKASH_API_KEY" | jq '.data[] | {id, name, slug, type, role}'
curl -s "$API/v1/projects" -H "x-api-key: $AKASH_API_KEY" | jq '.data[] | {id, name, isDefault, canDeploy: .access.canDeploy}'
```
With an API key, `GET /v1/organizations` lists only the key's team. `GET /v1/projects` lists the projects the request reaches, most recently active first; a key bound to a project sees only its own. `access.canDeploy` says whether the caller may deploy into that project and change its deployments.
## API keys bound to a team or a project
In Console, switch to the team, then open **Settings → API Keys**; the dialog offers **All projects** or one project. Over the API, `POST /v1/api-keys` creates the key in the team the request runs in, and `data.projectId` binds it to one project the caller reaches (404 otherwise):
```bash
curl -sX POST "$API/v1/api-keys" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"name": "ci-checkout", "projectId": "<project id>"}}'
```
Every key in `GET /v1/api-keys` shows where it acts:
| Field | Meaning |
|---|---|
| `organizationId` | The team the key acts in. `null` for a key created before teams, which acts in its creator's personal account. |
| `projectId` | The only project the key reaches, or `null` when it reaches every project its creator does |
`GET /v1/api-keys` lists the caller's own keys in the current team only. A binding never changes (`PATCH` only renames), so to move a key, create a new one and delete the old one.
A key bound to a project:
- deploys into its project without being told (below), and reads and changes only that project's deployments
- reads its team and its project, but can't manage API keys, members, invitations, projects or billing settings (403), nor rename the team
- mints provider JWTs only for deployments it names (see [Provider tokens](#provider-tokens))
To give someone access to one project only, invite them to the team as a `member` with that project selected, and let them create their own key. A key you create acts with your role, so anyone holding it acts as you.
## Deploy into a project
`POST /v1/deployments` takes `data.projectId`. Without it, Console files the deployment:
1. in the only project the request may deploy into, when there is exactly one: a key bound to a project, an `x-project-id` header, or a member who may deploy into a single project;
2. otherwise in the team's default project.
When the caller may not deploy into that project, the create answers **400 `project_required`**: pass `data.projectId`, taken from `GET /v1/projects` where `access.canDeploy` is true. A `projectId` the caller can't deploy into, or that doesn't exist, answers 404.
```bash
PROJECT_ID=$(curl -s "$API/v1/projects" -H "x-api-key: $AKASH_API_KEY" \
| jq -r '.data[] | select(.name == "prod" and .access.canDeploy) | .id')
curl -sX POST "$API/v1/deployments" \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -nc --rawfile sdl deploy.yaml --arg project "$PROJECT_ID" '{data: {sdl: $sdl, projectId: $project}}')"
```
Bids, `POST /v1/leases`, `PATCH`, the provider JWT and the close work on the dseq as usual; the deployment stays in its project until someone moves it.
## List by project, move, locate
```
GET /v1/deployments?projectId=<id>
PATCH /v1/deployments/{dseq}/project
GET /v1/deployment-locations/{dseq}
```
- **List.** `projectId` keeps the deployments filed in one project, and each listed deployment carries its `projectId`. A project the caller can't reach lists nothing, like one that doesn't exist. Without the filter the list spans every project the caller reaches; a team with more open deployments than one list can check answers 422, so filter by project.
- **Move.** `PATCH /v1/deployments/{dseq}/project` with `{ "data": { "projectId": "<target>" } }` refiles a deployment in another project of the same team and answers `{ "data": { "dseq", "projectId" } }`. Owners and admins only (403 otherwise; 404 when the deployment or the project is out of reach). Nothing changes on chain: the lease, the provider and the escrow stay; the deployment's alerts follow it. A deployment never moves between teams.
- **Locate.** `GET /v1/deployment-locations/{dseq}` answers `{ "data": { "organizationId", "organizationSlug", "projectId" } }`. With an API key it only looks within the key's reach, and answers 404 outside it.
## Project budgets
Owners and admins can give a project a monthly budget in USD (calendar months, UTC) with `POST /v1/project-budgets` (`projectId`, `monthlyAmountUsd`, `alertThresholdPercent`, `stopTopUps`). A create whose first deposit would take the project past it answers **403 `project_budget_exceeded`**, and `data.projectIds` names the project. With `stopTopUps`, the automatic top-ups of the project's running deployments stop at the budget too, so they run on their escrow until it is spent. Retrying doesn't help: an owner or admin raises the budget (`PATCH /v1/project-budgets/{id}`), or the next month starts. `GET /v1/project-budgets` shows `currentPeriod.committedUsd`.
## Team billing
A team pays from its own wallet, separate from every member's personal account: its own on-chain address, credits, cards and Auto recharge.
- Teams never get a free trial. A new team has no credits until an owner or billing member adds them.
- `GET /v1/balances` with a team's key reads the team's balance, in the same `balance`, `deployments` and `total` fields (**@account-and-funding.md**). Every member can read it.
- A team deployment's `deployment.id.owner` is the team wallet's address, not a member's.
- Credits, cards and Auto recharge are set on the team's **Billing** page in Console, by an owner or a billing member. There is no supported API for them, and nothing moves credits between a personal account and a team.
- A 402 (`insufficient_balance`, `balance_top_up_pending`) on a team key is about the team's credits. Ask an owner or billing member to add credits. `hasPaymentMethod` in `GET /v1/organizations` says whether the team has a card saved.
## Provider tokens
`POST /v1/create-jwt-token` mints tokens for the wallet of the request's team. Two rules apply on top of **@operations.md**:
- In a team, and for any request limited to some projects, `ttl` is at most 3600 seconds; more answers 400.
- A request is **limited to some projects** when its key is bound to a project, it sends `x-project-id`, or the key's creator is a team `member` or `viewer` (only owners and admins reach every project). Such a request must name each deployment: `access: "granular"` at the top and on every provider entry, each with its `deployments`. `full` and `scoped` answer 403 `project_forbidden`. Granting more than `status`, `events` and `logs` also needs the right to change those deployments.
```json
{
"data": {
"ttl": 1800,
"leases": {
"access": "granular",
"permissions": [
{
"provider": "akash1prov...",
"access": "granular",
"deployments": [{ "dseq": 1785245500842, "gseq": 1, "oseq": 1, "services": ["web"], "scope": ["logs", "events", "status"] }]
}
]
}
}
}
```
## Only from a signed-in session
Creating a team, accepting an invitation, leaving a team, deleting a team and deleting the account need a signed-in Console session. With an API key they answer **403 `session_required`**: do them in the Console web app.
## Errors
| Status | `code` | Meaning | Do this |
|---|---|---|---|
| 400 | `organization_mismatch` | `x-organization-id` names another team than the API key's | Drop the header, or use a key created in that team |
| 403 | `organization_forbidden` | The caller isn't a member of the team: the header names a team the user doesn't belong to, or the key's creator left or was removed | Use a key of a current member |
| 403 | `project_forbidden` | The request reaches outside its projects: `x-project-id` names a project the caller can't reach (or another project than the key's), a write targets a deployment filed in a project the caller can't change, or a provider JWT doesn't name its deployments one by one | Stay within the key's projects, name the deployments in the JWT, or use a key that reaches the other project |
| 400 | `project_required` | A create found no project to file the deployment in | Pass `data.projectId` |
| 403 | `project_budget_exceeded` | The deposit would take the project past its monthly budget; `data.projectIds` names it | An owner or admin raises the budget. Don't retry. |
| 403 | `project_limit_reached` | `POST /v1/projects` found the team at its project limit, given in `data.limit`: 10, counting the default project | Use an existing project, or have an owner or admin delete one it no longer uses (one with no open deployment) |
| 403 | `organization_limit_reached` | Creating a team found the user at their limit of teams they created that still exist, given in `data.limit`: 3 | Delete a team you created and no longer use, or join an existing team by invitation |
| 403 | `session_required` | A session-only action was sent with an API key | Do it in the Console web app |
A read of a deployment outside the caller's projects answers 404, as for one that doesn't exist. A `viewer` or `billing` member's key gets a plain 403 (`forbidden`) on a create, lease, patch or close.
## Team endpoints
All of these run in the request's team and take the same `x-api-key`; the live spec has their bodies.
| Task | Endpoint |
|---|---|
| The team, rename it (owner, admin) | `GET /v1/organizations`, `PATCH /v1/organizations/{id}` |
| Projects (only owners and admins create one; up to 10 per team, counting the default) | `GET`, `POST /v1/projects`; `GET`, `PATCH`, `DELETE /v1/projects/{id}` (delete only with no open deployment; the default project can't be deleted) |
| Who reaches a project | `GET /v1/projects/{id}/members`; `POST /v1/project-members`; `PATCH`, `DELETE /v1/project-members/{id}` |
| Members | `GET /v1/organization-members`; `PATCH`, `DELETE /v1/organization-members/{id}` |
| Invitations by email (accepting is session-only) | `GET`, `POST /v1/organization-invitations`; `DELETE /v1/organization-invitations/{id}` |
| Budgets | `GET`, `POST /v1/project-budgets`; `PATCH`, `DELETE /v1/project-budgets/{id}` |
| Spend per project, per deployment | `GET /v1/project-spend`, `GET /v1/deployment-spend` |
| What happened in the team | `GET /v1/organization-activities` |
## Related files
- **@authentication.md**: API keys
- **@deployment-endpoints.md**: create, list, move and close deployments
- **@account-and-funding.md**: balance and automatic funding
- **@operations.md**: provider JWTs, logs and shell
rules/deploy/overview.md
# Deployment Methods Overview
There are five ways to **deploy** to Akash and one way to **consume** managed inference on Akash. **Pick one and commit to it for the conversation** — methods are not interchangeable mid-flow because the auth model, command surface, and wallet semantics all differ.
This page exists because conflating these methods is the most common mistake. The SKILL.md's "Choosing a Deployment Method" section is the canonical decision rule; this page is the longer reference for each option.
## Two questions, in order
**1. Are you *deploying* a workload or *consuming* a hosted model?**
- **Deploying** (you write SDL, you own the runtime, you pay in ACT) → continue to question 2.
- **Consuming** (you call an LLM API, AkashML hosts it, you pay in USD credits) → **AkashML**. Stop here; the wallet/auth questions below don't apply. See [@akashml/overview.md](akashml/overview.md).
**2. (Deployment only) Do you want Console to manage the wallet, or do you want self-custody?**
- **Console-managed wallet** → Console API, or the akt CLI on a Console context. You sign up on console.akash.network, generate an API key, and authenticate every request with `x-api-key` (akt sends it for you). Console holds the on-chain wallet of your personal account, or of the team the key belongs to, keeps its funds, and signs transactions on your behalf. No private keys ever leave Console's infrastructure.
- **Self-custody** → the akt CLI on a keyring context, the Akash CLI (`provider-services`), TypeScript SDK, or Go SDK. You hold the private key (in `~/.akash/keys`, a browser wallet like Keplr, a Ledger, or wherever), and your code signs each transaction locally before broadcasting.
Within the deployment paths there is no "Console API + my own wallet." Console holds the wallet: an API key acts in the personal account or team it was created in, with its creator's role and optionally limited to one project, and that wallet pays (`console-api/teams-and-projects.md`). If a user wants to deploy from a self-custody wallet (Keplr or hardware), they need the CLI or an SDK.
AkashML sits *outside* the deploy/self-custody axis entirely: it is a managed consumption surface, not a wallet model. Bringing it up alongside Console API in a "managed vs self-custody" comparison is a category error — they solve different problems.
## Console has two web UIs — disambiguate
- **Standard Console** (`console.akash.network`) — managed wallet. The Console API in this skill drives this product.
- **Console Air** ([github.com/akash-network/console-air](https://github.com/akash-network/console-air)) — self-custody UI for Keplr or hardware wallets. **Self-hosted** — there is no hosted URL at `console-air.akash.network`; users clone the repo and run it locally. It is a **UI**, not an API. This skill does not cover it; programmatic self-custody users go CLI or SDK.
## The five deployment methods at a glance
| Method | Wallet | Auth | Language surface | When to pick it |
|---|---|---|---|---|
| **Console API** | Managed | `x-api-key` header | HTTP + JSON | CI/CD, server-to-server, integrations, anything where you have a deploy account but no private keys |
| **akt CLI** | Managed or self-custody | Console API key, or local key | `akt` binary | Terminal and agent sessions: one command from SDL to running service, plus logs, status and shell without a wallet |
| **Akash CLI (`provider-services`)** | Self-custody | Local key + signature | `provider-services` binary | Existing scripts, manual chain workflows, full local control |
| **TypeScript SDK** | Self-custody | Wallet adapter (Keplr or mnemonic) | `@akashnetwork/chain-sdk` | dApps, Node.js services, any TS/JS integration |
| **Go SDK** | Self-custody | Local key | Go modules | Backend Go services, custom tooling |
## Consumption — AkashML
Plus one consumption path, served by the same network but on a different problem:
| Surface | Wallet | Auth | Language surface | When to pick it |
|---|---|---|---|---|
| **AkashML** | No wallet — USD credits on AkashML account | `Authorization: Bearer akml-...` | HTTP + JSON (OpenAI- and Anthropic-compatible) | You want to **call** an LLM, not host one. RAG, chat, batch inference, agent backends |
## Console API
REST API at `https://console-api.akash.network/v1`. Authentication is `x-api-key: <key>` (not `Authorization: Bearer` — that header is for JWT-based session auth).
**Strengths**
- Programmatic deployments via simple HTTP
- No certificate management
- No private-key handling
- Deployments funded automatically from account credits, in USD
- Stripe-funded; no need to acquire AKT manually
- Console keeps each deployment's SDL: change the image, env or ports in place with `PATCH`, and keep secrets sealed
**Limitations**
- Depends on Console as a service
- Can only spend from the wallet of the key's personal account or team, within what the key's role and project allow
- Rate limits apply
See **@console-api/** for full reference.
## akt CLI
The unified `akt` binary. On a Console context it drives the Console API with the account's API key; on a keyring context it signs chain transactions itself. The official docs present it as the replacement for the `akash` and `provider-services` CLIs.
**Strengths**
- One `akt deploy` creates the deployment, waits for bids, opens the lease, sends the manifest and waits for the service
- Logs, events, status and shell through JWTs that Console mints, with no wallet or certificate
- JSON and JSONL output, an offline SDL validator, and an MCP server that starts read-only
**Limitations**
- A binary to install and a context to configure first
- Secrets, patches and redeploy need akt 1.0.1 or later; 0.1.x has none of them
- Flags differ between 0.1.x and 1.0.1 (the Console deposit above all), so check `akt version`
See **@akt/** for setup and recipes.
## Akash CLI (`provider-services`)
Command-line `provider-services` binary against a self-custody wallet. akt's keyring rail now covers the same ground; this skill keeps the `provider-services` reference for existing scripts.
**Strengths**
- Full control
- Scriptable
- Works against any RPC node
- No third-party dependency
**Limitations**
- Requires local key management
- Requires acquiring AKT
- More moving parts (keys, mnemonics, fees, gas)
See **@cli/** for installation and command reference.
## TypeScript SDK
`@akashnetwork/chain-sdk` for Node.js and browsers.
**Strengths**
- Native JS/TS integration
- Works with browser wallet adapters (Keplr)
- Type safety
- Self-custody — keys stay client-side
**Limitations**
- More code than curl
- Requires SDK knowledge
See **@sdk/typescript/** for documentation.
## Go SDK
Go modules built on the Cosmos SDK and Akash chain-api.
**Strengths**
- High performance
- Native blockchain integration
- Self-custody
**Limitations**
- Go-only
- More setup than other paths
See **@sdk/go/** for documentation.
## AkashML (consumption path)
REST API at `https://api.akashml.com/v1` (OpenAI-compatible) and `https://api.akashml.com/anthropic` (Anthropic-compatible). Authentication is `Authorization: Bearer <key>`. Keys begin with `akml-`. **Not** an `x-api-key` header — that belongs to Console API on a different host.
**Strengths**
- Drop-in for the OpenAI and Anthropic SDKs (just change `baseURL`)
- No SDL, no wallet, no AKT/ACT, no leases
- USD-credit billing on AkashML account
- Open-source models running on Akash compute
**Limitations**
- Consumption only — you cannot host your own model here
- No on-chain visibility (billing is off-chain USD credits, not `uact`)
- Model catalog is dynamic; pin IDs, not "latest"
See **@akashml/** for full reference.
## Quick start by method
### Console API (your API key)
```bash
curl -X POST https://console-api.akash.network/v1/deployments \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"sdl": "version: \"2.0\"\nservices:\n web:\n image: nginx:1.25.3\n..."
}
}'
```
No `deposit`: Console funds the deployment from the account's credit balance. The response carries the `dseq`; accept a bid with `POST /v1/leases` and Console sends the provider its manifest. Full flow: **@console-api/api-key-quickstart.md**.
### akt (Console context)
```bash
brew install akash-network/tap/akt
# With AKT_CONSOLE_API_KEY exported from your secret store
akt context create main --deploy-via console --set-current
akt sdl validate deploy.yaml
akt deploy deploy.yaml --bid-select cheapest --yes # akt 0.1.x: add --deposit 0.5
```
Full recipes: **@akt/console-workflows.md**.
### Akash CLI (`provider-services`, self-custody)
```bash
# Install
curl -sfL https://raw.githubusercontent.com/akash-network/provider/main/install.sh | bash
sudo mv ./bin/provider-services /usr/local/bin/
# Create a wallet
provider-services keys add deploy
# Fund it (acquire AKT externally), then create the deployment
provider-services tx deployment create deploy.yaml --from deploy --gas auto --gas-adjustment 1.3
# After bids arrive
provider-services tx market lease create --dseq <dseq> --provider <provider> --from deploy
```
### TypeScript SDK (self-custody)
```typescript
import { SigningStargateClient } from "@cosmjs/stargate";
import { MsgCreateDeployment } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4";
const client = await SigningStargateClient.connectWithSigner(rpcUrl, wallet);
const result = await client.signAndBroadcast(address, [{
typeUrl: "/akash.deployment.v1beta4.MsgCreateDeployment",
value: MsgCreateDeployment.fromPartial({ ... }),
}], "auto");
```
### Go SDK (self-custody)
```go
import (
"pkg.akt.dev/go/node/deployment/v1beta4"
"github.com/cosmos/cosmos-sdk/client"
)
msg := &v1beta4.MsgCreateDeployment{ ... }
// build, sign, and broadcast with a configured cosmos-sdk client.Context
```
### AkashML (consumption, not deployment)
```bash
curl -X POST https://api.akashml.com/v1/chat/completions \
-H "Authorization: Bearer $AKASHML_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-ai/DeepSeek-V4-Flash",
"messages": [{"role": "user", "content": "Hello from AkashML"}]
}'
```
No SDL is involved. The response is a standard OpenAI chat completion. See **@akashml/quickstart.md** for the end-to-end signup-to-inference walkthrough.
## Switching methods mid-conversation
If the user explicitly says they want to switch ("can we do this via the API instead?"):
1. Acknowledge the switch.
2. Treat any state from the prior method (deployments created on-chain) as still valid — they don't need to be migrated, since the chain doesn't care which client created them.
3. Apply the new method's rules from that point on: new commands, new auth, new examples.
Do **not** silently mix methods. If the user is mid-CLI workflow and you suggest a Console API call, you are violating their chosen path.
## Authentication summary
| Auth mechanism | Console API | akt | Akash CLI (`provider-services`) | SDK | AkashML |
|---|---|---|---|---|---|
| `x-api-key` header | ✅ primary | ✅ sent for you on a Console context | ❌ | ❌ | ❌ |
| `Authorization: Bearer <key>` (API key) | ❌ (Bearer is reserved for JWTs) | ❌ | ❌ | ❌ | ✅ primary (`akml-...`) |
| Local wallet key | ❌ | ✅ on a keyring context | ✅ primary | ✅ primary | ❌ |
| Browser wallet adapter (Keplr) | ❌ | ❌ | ❌ | ✅ (TS SDK in browser) | ❌ |
| `Authorization: Bearer <jwt>` | ✅ (for Console-account JWT session auth) | Provider calls only, with JWTs minted for you | ❌ | ❌ | ❌ |
| mTLS certificate | ❌ ([deprecated](cli/mtls-legacy.md) for Console API; CLI direct-to-provider calls still use it where applicable) | Optional for provider calls (`--provider-auth-type mtls`); JWT is the default | ✅ (CLI direct provider calls) | ✅ (SDK direct provider calls) | ❌ |
The collision worth flagging: **`Authorization: Bearer`** means a JWT on the Console API and an API key on AkashML. They are different services on different hosts; don't carry credentials across.
rules/overview.md
# Akash Network Overview
Akash Network is a decentralized cloud computing marketplace that connects users who need compute resources with providers who have underutilized capacity.
## How It Works
```
┌─────────────┐ 1. Create SDL ┌─────────────┐
│ Tenant │ ───────────────────► │ Akash │
│ (Deployer) │ │ Blockchain │
└─────────────┘ └─────────────┘
│ │
│ 4. Accept Bid │ 2. Broadcast Order
│ Send Manifest │
▼ ▼
┌─────────────┐ 3. Submit Bid ┌─────────────┐
│ Provider │ ◄─────────────────── │ Provider │
│ (Selected) │ │ Network │
└─────────────┘ └─────────────┘
│
│ 5. Deploy Containers
▼
┌─────────────┐
│ Kubernetes │
│ Cluster │
└─────────────┘
```
### Deployment Flow
1. **Create SDL** - Tenant writes a Stack Definition Language (SDL) file describing their workload
2. **Create Deployment** - Tenant broadcasts deployment order to the blockchain
3. **Bidding** - Providers submit bids for the deployment
4. **Accept Bid** - Tenant accepts a bid, creating a lease
5. **Send Manifest** - Tenant sends the SDL manifest to the provider
6. **Deploy** - Provider deploys containers on their Kubernetes cluster
## Key Components
### Blockchain (akash-node)
The Akash blockchain handles:
- Deployment orders and leases
- Payment escrow and settlement
- Provider registration and auditing
- Governance and staking
### Providers
Providers run Kubernetes clusters and the Akash provider software to:
- Monitor for deployment orders
- Submit bids based on pricing rules
- Deploy and manage workloads
- Report lease status
### Console
[Akash Console](https://console.akash.network) is a web interface for:
- Creating deployments without CLI
- Managing leases and providers
- Viewing deployment logs and status
### Console API
The [Console API](https://console-api.akash.network/v1/swagger) provides REST endpoints, authenticated with an API key, for:
- Creating, updating and closing deployments from the managed wallet of a Console account or team, filed into projects
- Accepting bids and reading lease status
- Provider data, GPU and region availability
There is no SDL-validation endpoint; `POST /v1/deployments` answers 400 for an invalid SDL. See `rules/deploy/console-api/`.
## Network Details
| Property | Value |
|----------|-------|
| Chain ID | `akashnet-2` |
| Chain Token | AKT — denom `uakt` (1 AKT = 1,000,000 uakt). Used for gas, staking, validator rewards. |
| Deployment Token | ACT — denom `uact` (1 ACT = 1,000,000 uact). Used for SDL pricing, bid prices (the rate providers offer), deployment escrow/deposit, and lease payments. **Separate from AKT.** The bid *deposit* (`bidMinDeposit` / `MsgCreateBid.deposit`, anti-spam collateral) defaults to `uakt` — the provider posts 500000 uakt (0.5 AKT) from its balance; `uact` only via a rare burn-mint fallback. |
| Self-custody deployers | Must mint ACT by burning AKT before depositing to a deployment. Console API users skip this (Console funds the managed wallet with ACT directly). |
| Block Time | ~6 seconds |
| Consensus | Tendermint BFT |
## Payment Model
- **Escrow-based** - Funds deposited upfront
- **Per-block billing** - Charged every ~6 seconds
- **Native ACT** - SDL pricing and lease payments in `uact` (ACT's micro-denomination, minted by burning AKT)
- **Refundable** - Unused escrow returned on close
## Architecture Layers
### Application Layer
- SDL files define workloads
- Docker containers run applications
- Kubernetes manages orchestration
### Protocol Layer
- Deployment marketplace on-chain
- Bid/lease management
- Payment settlement
### Network Layer
- Cosmos SDK blockchain
- IBC for cross-chain assets
- Tendermint consensus
## Comparison with Traditional Cloud
| Aspect | Traditional Cloud | Akash Network |
|--------|------------------|---------------|
| Pricing | Fixed rates | Competitive bidding |
| Payment | Monthly billing | Per-block escrow |
| Providers | Single vendor | Multiple providers |
| Lock-in | Vendor-specific | Portable SDL |
| Control | Provider managed | User controlled |
| Cost | Market rates | Up to 85% lower |
## Use Cases
- **Web Applications** - Deploy any containerized web app
- **AI/ML Workloads** - Access GPU compute at competitive rates
- **Development/Testing** - Spin up ephemeral environments
- **Blockchain Nodes** - Run validator and full nodes
- **Game Servers** - Deploy with dedicated IPs
- **Batch Processing** - Run compute-intensive jobs
rules/pricing.md
# Akash Network Pricing
Understanding payment options, pricing calculations, and cost optimization on Akash Network.
## Payment Denominations
Akash has **two tokens**. Don't conflate them:
| Token | Denom | What it's for |
|---|---|---|
| **AKT** | `uakt` | Chain token — gas (`--gas-prices`), staking, validator rewards. `1 AKT = 1,000,000 uakt`. |
| **ACT** | `uact` | Deployment-payment token — SDL pricing, bid *prices* (the rate providers offer), deployment escrow/deposit, lease payments. `1 ACT = 1,000,000 uact`. Note: the provider-side bid *deposit* (`MsgCreateBid.deposit`, anti-spam collateral) defaults to `uakt` — the provider posts `500000 uakt` (0.5 AKT) from its balance; `uact` is accepted only via a rare burn-mint (BME) fallback. |
**AKT and ACT are separate tokens.** Self-custody deployers must mint ACT by burning AKT before depositing to a deployment. Console API users skip this — Console funds the managed wallet with ACT directly (Stripe → USD → ACT server-side).
### uact (deployment-payment denom)
Used in SDL pricing, deployment deposits, and lease payments:
```yaml
pricing:
web:
denom: uact
amount: 1000
```
`1 ACT = 1,000,000 uact`. Values in deployment messages (`MsgDepositDeployment`, escrow balances, bid amounts) are denominated in `uact`. `uact` is the SDL pricing, deposit, and lease-payment denom; `uakt` is only accepted via a rare burn-mint (BME) fallback.
## Pricing Model
### Per-Block Billing
Akash charges per block (~6 seconds):
```
Blocks per minute: ~10
Blocks per hour: ~600
Blocks per day: ~14,400
Blocks per month: ~438,000
```
### Price Calculation
```
Monthly Cost = bid_amount × blocks_per_month
```
**Illustrative example with uact** (multiply the per-block `amount` by blocks-per-month to estimate cost — actual rates come from live bids, not this figure):
```yaml
pricing:
web:
denom: uact
amount: 1000
# Monthly cost ≈ amount × blocks_per_month (uact). Convert to ACT by dividing by 1,000,000.
# Use live price discovery and real awesome-akash templates for realistic numbers.
```
## Pricing Guidelines
These are illustrative starting points only. Real market rates come from live bids — use price discovery (Akash Console or CLI) and current awesome-akash templates rather than treating these as quotes.
### By Workload Type
| Workload | CPU | Memory | Storage |
|----------|-----|--------|---------|
| Static Site | 0.25 | 256Mi | 512Mi |
| Web App | 0.5 | 512Mi | 1Gi |
| API Server | 1-2 | 1-2Gi | 5Gi |
| Database | 2 | 2-4Gi | 20Gi+ |
| ML Inference | 4 | 8Gi | 20Gi |
### GPU Pricing
GPU per-block rates vary widely by model, VRAM, and provider. Discover current rates from live bids rather than relying on fixed figures:
| GPU Model | VRAM |
|-----------|------|
| T4 | 16GB |
| RTX 3080 | 10GB |
| RTX 3090 | 24GB |
| RTX A6000 | 48GB |
| A100 40GB | 40GB |
| A100 80GB | 80GB |
## Escrow System
### How Escrow Works
1. **Deposit** - Funds locked when deployment created
2. **Drawdown** - Provider withdraws per block during lease
3. **Refund** - Remaining balance returned on close
### Minimum Escrow
Recommended escrow for deployment stability:
```
Minimum Escrow = bid_amount × blocks_per_day × 7
```
Multiply your per-block `amount` (in uact) by blocks-per-day and your desired runway in days; divide by 1,000,000 to express the result in ACT. Size this against your actual bid amount rather than a fixed figure.
### Escrow Monitoring
Check escrow balance regularly:
```bash
provider-services query deployment get --owner <address> --dseq <dseq>
```
Add funds before depletion:
```bash
provider-services tx deployment deposit <amount>uact --owner <address> --dseq <dseq>
```
## Cost Optimization
### Right-Size Resources
Start small and scale up:
```yaml
# Start with minimum viable resources
profiles:
compute:
web:
resources:
cpu:
units: 0.25 # Quarter core
memory:
size: 256Mi # Minimal memory
storage:
size: 512Mi # Minimal storage
```
### Use Competitive Bidding
Set a maximum price and let providers compete:
```yaml
pricing:
web:
denom: uact
amount: 2000 # Max you're willing to pay
```
Providers will bid at or below this amount.
### Payment Denomination
`uact` is the SDL pricing, deposit, and lease-payment denom; `uakt` is only accepted via a rare burn-mint (BME) fallback. Console-API users get `uact` funded automatically (Stripe → USD → ACT, server-side). Self-custody deployers mint ACT by burning AKT before depositing.
### Persistent vs Ephemeral Storage
Persistent storage costs more but survives restarts:
```yaml
# Ephemeral (cheaper, data lost on restart)
storage:
size: 10Gi
# Persistent (more expensive, data survives)
storage:
- name: data
size: 10Gi
attributes:
persistent: true
class: beta2
```
### Multi-Instance Deployment
Running multiple instances increases costs linearly:
```yaml
deployment:
web:
dcloud:
profile: web
count: 3 # 3x the cost of count: 1
```
## Comparing Costs
### Akash vs Traditional Cloud
| Service | AWS/GCP | Akash (est.) | Savings |
|---------|---------|--------------|---------|
| 1 vCPU, 2GB RAM | $30-50/mo | $5-15/mo | 70-85% |
| 4 vCPU, 16GB RAM | $120-200/mo | $20-50/mo | 70-85% |
| GPU (T4) | $300-500/mo | $50-150/mo | 70-80% |
| GPU (A100) | $2000-3000/mo | $300-700/mo | 70-85% |
*Actual prices vary based on provider bids and market conditions*
### Price Discovery
Use Akash Console or CLI to see current market rates:
```bash
# Query active leases for pricing data
provider-services query market lease list --state active
```
## IP Endpoint Costs
IP endpoints have additional costs:
```yaml
endpoints:
public-ip:
kind: ip
```
IP leases are billed separately from compute. Factor this into total deployment cost.
## Payment Flow
```
1. Tenant deposits escrow
└── Funds locked in deployment account
2. Provider accepts bid
└── Lease created at bid price
3. Per-block settlement
└── Provider withdraws bid_amount each block
4. Deployment closed
└── Remaining escrow returned to tenant
```
## Tax and Compliance
- Payments are on-chain and publicly visible
- Keep records of deployment transactions for tax purposes
- Consider the tax implications of AKT appreciation/depreciation
rules/reference/gpu-models.md
# GPU Models Reference
Supported NVIDIA GPU models on Akash Network.
## GPU Configuration
### Basic GPU Request
```yaml
profiles:
compute:
ml:
resources:
gpu:
units: 1
attributes:
vendor:
nvidia:
```
### Specific Model Request
```yaml
profiles:
compute:
ml:
resources:
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: a100
```
### With VRAM and Interface
```yaml
profiles:
compute:
ml:
resources:
gpu:
units: 2
attributes:
vendor:
nvidia:
- model: a100
ram: 80Gi
interface: sxm
```
## Available GPU Models
### Data Center GPUs
| Model | VRAM | Architecture | Best For |
|-------|------|--------------|----------|
| `a100` | 40GB/80GB | Ampere | Large model training, HPC |
| `rtxa6000` | 48GB | Ampere | Professional visualization, training |
| `a40` | 48GB | Ampere | Professional visualization |
| `v100` | 16GB/32GB | Volta | Training, HPC |
| `t4` | 16GB | Turing | Inference, light training |
| `l4` | 24GB | Ada Lovelace | Video processing, inference |
| `l40` | 48GB | Ada Lovelace | Training, visualization |
| `l40s` | 48GB | Ada Lovelace | Training, inference |
| `h100` | 80GB | Hopper | Large model training |
### Consumer/Prosumer GPUs
| Model | VRAM | Architecture | Best For |
|-------|------|--------------|----------|
| `rtx3060` | 12GB | Ampere | Light ML, gaming |
| `rtx3070` | 8GB | Ampere | Light ML, gaming |
| `rtx3080` | 10GB/12GB | Ampere | ML inference, training |
| `rtx3080ti` | 12GB | Ampere | ML inference, training |
| `rtx3090` | 24GB | Ampere | Training, inference |
| `rtx3090ti` | 24GB | Ampere | Training, inference |
| `rtx4070` | 12GB | Ada Lovelace | ML inference |
| `rtx4080` | 16GB | Ada Lovelace | ML training, inference |
| `rtx4090` | 24GB | Ada Lovelace | Training, inference |
## GPU Specifications
### Model String Format
```yaml
# Model only
nvidia:
- model: a100
# Model with VRAM
nvidia:
- model: a100
ram: 80Gi
# Model with interface
nvidia:
- model: a100
interface: sxm
# Full specification
nvidia:
- model: a100
ram: 80Gi
interface: sxm
```
### VRAM Options
Specify GPU memory requirements:
```yaml
# 40GB A100
nvidia:
- model: a100
ram: 40Gi
# 80GB A100
nvidia:
- model: a100
ram: 80Gi
```
### Interface Options
| Interface | Description |
|-----------|-------------|
| `pcie` | PCIe connected (standard) |
| `sxm` | SXM socket (NVLink capable) |
```yaml
nvidia:
- model: a100
interface: sxm # For NVLink connectivity
```
## Multi-GPU Configurations
### Multiple GPUs of Same Type
```yaml
profiles:
compute:
training:
resources:
gpu:
units: 4
attributes:
vendor:
nvidia:
- model: a100
interface: sxm
```
### GPU Requirements
When requesting multiple GPUs:
- Ensure sufficient CPU and memory
- Consider NVLink for multi-GPU training
- Scale memory with GPU count
```yaml
profiles:
compute:
multi-gpu:
resources:
cpu:
units: 32 # Scale CPU with GPUs
memory:
size: 128Gi # ~32GB per GPU
storage:
size: 500Gi
gpu:
units: 4
attributes:
vendor:
nvidia:
- model: a100
```
## Use Case Recommendations
### LLM Inference (Small Models)
```yaml
# Llama 2 7B, Mistral 7B
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: t4
```
### LLM Inference (Large Models)
```yaml
# Llama 2 70B, large models
gpu:
units: 2
attributes:
vendor:
nvidia:
- model: a100
ram: 80Gi
```
### Stable Diffusion
```yaml
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: rtx3090
```
### Training (Medium)
```yaml
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: a100
ram: 40Gi
```
### Training (Large Scale)
```yaml
gpu:
units: 8
attributes:
vendor:
nvidia:
- model: a100
ram: 80Gi
interface: sxm
```
## Provider Availability
Not all providers have all GPU models. Check availability:
1. **Console**: Filter by GPU type in deployment wizard
2. **CLI**: Query provider attributes
3. **Market**: Higher-end GPUs may have limited availability
## Pricing Expectations
GPU pricing varies by model and market conditions:
| GPU Tier | Examples | uact/block Range |
|----------|----------|------------------|
| Entry | T4, RTX 3060 | 5,000 - 15,000 |
| Mid | RTX 3080/3090 | 15,000 - 30,000 |
| High | A100 40GB, rtxa6000 | 30,000 - 60,000 |
| Top | A100 80GB, H100 | 60,000 - 150,000+ |
## Validation Rules
### GPU with units > 0 Must Have Vendor
```yaml
# INVALID
gpu:
units: 1
# Missing vendor attribute
# VALID
gpu:
units: 1
attributes:
vendor:
nvidia:
```
### GPU with units = 0 Cannot Have Attributes
```yaml
# INVALID
gpu:
units: 0
attributes:
vendor:
nvidia:
# VALID
gpu:
units: 0
```
## Docker Images for GPU Workloads
Common base images:
```yaml
# NVIDIA CUDA base
image: nvidia/cuda:12.0-runtime-ubuntu22.04
# PyTorch
image: pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
# TensorFlow
image: tensorflow/tensorflow:2.13.0-gpu
# Hugging Face TGI
image: ghcr.io/huggingface/text-generation-inference:1.1.0
# Triton Inference Server
image: nvcr.io/nvidia/tritonserver:23.08-py3
```
rules/reference/rpc-endpoints.md
# RPC Endpoints Reference
Public RPC and API endpoints for interacting with the Akash Network.
## Official Endpoints
### Akash Network (Mainnet)
| Service | URL |
|---------|-----|
| RPC | `https://rpc.akashnet.net:443` |
| REST/LCD | `https://api.akashnet.net:443` |
| gRPC | `akash-grpc.publicnode.com:443` (or `akash.lavenderfive.com:443`) |
> Note: `grpc.akashnet.net` is Cloudflare-fronted and does **not** serve usable raw gRPC — use a registry-listed community gRPC endpoint (above) for SDK / `grpcurl` access.
### Chain ID
```
akashnet-2
```
## Alternative RPC Providers
### Community Endpoints
| Provider | RPC | REST |
|----------|-----|------|
| Polkachu | `https://akash-rpc.polkachu.com:443` | `https://akash-api.polkachu.com:443` |
| PublicNode | `https://akash-rpc.publicnode.com:443` | `https://akash-rest.publicnode.com` |
### Archive Nodes
For historical queries:
| Provider | RPC |
|----------|-----|
| Polkachu | `https://akash-rpc.polkachu.com:443` |
## Using RPC Endpoints
### CLI Configuration
```bash
# Set default node via env vars
export AKASH_NODE=https://rpc.akashnet.net:443
export AKASH_CHAIN_ID=akashnet-2
# Or use per-command
provider-services query bank balances <address> --node https://rpc.akashnet.net:443
```
### SDK Configuration
**TypeScript:**
```typescript
import { createChainNodeSDK, createStargateClient } from "@akashnetwork/chain-sdk";
const signer = createStargateClient({
baseUrl: "https://rpc.akashnet.net:443",
signerMnemonic: process.env.MNEMONIC,
});
const sdk = createChainNodeSDK({
query: { baseUrl: "https://akash-grpc.publicnode.com:443" },
tx: { signer },
});
```
**Go:**
```go
import "github.com/cosmos/cosmos-sdk/client"
clientCtx := client.Context{}.
WithNodeURI("https://rpc.akashnet.net:443")
```
## API Types
### RPC (Tendermint)
For blockchain queries and transaction broadcast:
```bash
# Query block
curl https://rpc.akashnet.net/block?height=1000000
# Broadcast transaction
curl -X POST https://rpc.akashnet.net/broadcast_tx_commit \
-d '{"tx": "..."}'
```
### REST/LCD (Cosmos SDK)
For account and module queries:
```bash
# Query account
curl https://api.akashnet.net/cosmos/auth/v1beta1/accounts/<address>
# Query deployments
curl https://api.akashnet.net/akash/deployment/v1beta4/deployments/list
```
### gRPC
For efficient binary protocol access:
```bash
# Using grpcurl
grpcurl -d '{"owner": "akash1..."}' \
akash-grpc.publicnode.com:443 \
akash.deployment.v1beta4.Query/Deployments
```
## Console API
The Akash Console API provides additional functionality:
| Service | URL |
|---------|-----|
| Console API | `https://console-api.akash.network/v1` |
| Swagger Docs | `https://console-api.akash.network/v1/swagger` |
### Console API Endpoints
```bash
# Your deployment (x-api-key)
GET https://console-api.akash.network/v1/deployments/<dseq>
# Any deployment, read-only (public)
GET https://console-api.akash.network/v1/deployment/<owner>/<dseq>
# Providers (public)
GET https://console-api.akash.network/v1/providers
```
There is no SDL-validation endpoint. The full reference is `rules/deploy/console-api/`.
## Provider Endpoints
Each provider exposes endpoints for lease management:
| Endpoint | Purpose |
|----------|---------|
| `/status` | Provider status |
| `/lease/<lease-id>/status` | Lease status |
| `/lease/<lease-id>/logs` | Container logs |
| `/lease/<lease-id>/shell` | Interactive shell |
### Accessing Provider Endpoints
Provider URLs are returned in lease info:
```bash
provider-services query market lease get \
--owner <owner> \
--dseq <dseq> \
--gseq 1 \
--oseq 1 \
--provider <provider>
```
## WebSocket Endpoints
For real-time updates:
```
wss://rpc.akashnet.net/websocket
```
### Subscribe to Events
```javascript
const ws = new WebSocket('wss://rpc.akashnet.net/websocket');
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'subscribe',
params: ["tm.event='NewBlock'"],
id: 1
}));
```
## Testnet Endpoints
For development and testing:
| Network | RPC | REST |
|---------|-----|------|
| Sandbox | `https://rpc.sandbox-01.aksh.pw:443` | `https://api.sandbox-01.aksh.pw:443` |
**Sandbox Chain ID:** `sandbox-01`
## Rate Limits
Public endpoints may have rate limits:
- **RPC**: Varies by provider
- **REST**: Varies by provider
- **Console API**: Check documentation
For high-volume applications:
1. Run your own node
2. Use multiple endpoints with failover
3. Contact providers for dedicated access
## Health Checks
### Check RPC Status
```bash
curl https://rpc.akashnet.net/status
```
### Check REST Status
```bash
curl https://api.akashnet.net/cosmos/base/tendermint/v1beta1/node_info
```
## Recommended Configuration
### High Availability Setup
Use multiple endpoints with failover:
```typescript
const endpoints = [
"https://rpc.akashnet.net:443",
"https://akash-rpc.polkachu.com:443",
"https://akash-rpc.publicnode.com:443"
];
async function getRpcWithFallback() {
for (const endpoint of endpoints) {
try {
const rpc = await getRpc(endpoint);
return rpc;
} catch (e) {
console.warn(`Failed to connect to ${endpoint}`);
}
}
throw new Error("All RPC endpoints failed");
}
```
### Local Development
Run a local node for development:
```bash
akash start --pruning nothing
```
Access at `http://localhost:26657` (RPC) and `http://localhost:1317` (REST).
rules/reference/storage-classes.md
# Storage Classes Reference
Akash Network supports multiple storage classes for different use cases.
## Available Storage Classes
| Class | Type | Persistent | Speed | Use Case |
|-------|------|------------|-------|----------|
| `default` | Ephemeral | No | Fast | Temporary data, caches |
| `beta1` | HDD | Yes | Standard | Bulk/cold persistent storage |
| `beta2` | SSD | Yes | Fast | Databases, general persistent |
| `beta3` | NVMe | Yes | Fastest | High-performance workloads |
| `ram` | RAM Disk | No | Ultra-fast | Caches, temp files |
## Storage Configuration
### Ephemeral Storage (Default)
Data is lost when container restarts:
```yaml
profiles:
compute:
web:
resources:
storage:
size: 10Gi
```
### Named Volumes
Multiple volumes with different classes:
```yaml
profiles:
compute:
db:
resources:
storage:
- name: default
size: 1Gi
- name: data
size: 50Gi
attributes:
persistent: true
class: beta2
```
### Mounting Volumes
Connect named storage to container paths:
```yaml
services:
db:
image: postgres:16
params:
storage:
data:
mount: /var/lib/postgresql/data
readOnly: false
```
## Storage Class Details
### beta2 (SSD)
Standard persistent SSD storage:
```yaml
storage:
- name: data
size: 100Gi
attributes:
persistent: true
class: beta2
```
**Characteristics:**
- SSD-backed
- Survives container restarts
- Survives provider maintenance
- Good balance of speed and cost
**Best for:**
- Databases (PostgreSQL, MySQL, MongoDB)
- Application data
- File uploads
- General persistence
### beta3 (NVMe)
High-performance NVMe storage:
```yaml
storage:
- name: fast-data
size: 100Gi
attributes:
persistent: true
class: beta3
```
**Characteristics:**
- NVMe-backed
- Highest I/O performance
- Lower latency than beta2
- May have limited availability
**Best for:**
- High-IOPS databases
- Real-time analytics
- Performance-critical workloads
### ram (RAM Disk)
Ultra-fast volatile storage:
```yaml
storage:
- name: cache
size: 4Gi
attributes:
class: ram
```
**Note:** RAM storage cannot be persistent.
**Characteristics:**
- Backed by system RAM
- Fastest possible I/O
- Data lost on restart
- Counts against memory quota
**Best for:**
- Application caches
- Session storage
- Temporary file processing
- Build artifacts
## Validation Rules
### RAM Cannot Be Persistent
```yaml
# INVALID - will fail validation
storage:
- name: cache
size: 4Gi
attributes:
class: ram
persistent: true # Error!
```
### Non-Default Classes Require Persistent Flag
```yaml
# INVALID - beta2/beta3 require persistent: true
storage:
- name: data
size: 10Gi
attributes:
class: beta2
persistent: false # Error!
# VALID
storage:
- name: data
size: 10Gi
attributes:
class: beta2
persistent: true
```
## Size Specifications
Use standard Kubernetes size suffixes:
| Suffix | Meaning | Example |
|--------|---------|---------|
| `Ki` | Kibibytes (1024) | `512Ki` |
| `Mi` | Mebibytes (1024²) | `512Mi` |
| `Gi` | Gibibytes (1024³) | `100Gi` |
| `Ti` | Tebibytes (1024⁴) | `1Ti` |
## Storage Guidelines
### Database Storage
```yaml
# PostgreSQL with data and WAL separation
profiles:
compute:
postgres:
resources:
cpu:
units: 2
memory:
size: 4Gi
storage:
- name: default
size: 1Gi
- name: pgdata
size: 100Gi
attributes:
persistent: true
class: beta2
- name: pgwal
size: 20Gi
attributes:
persistent: true
class: beta3 # Fast NVMe for WAL
```
### Web Application with Uploads
```yaml
profiles:
compute:
webapp:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
- name: default
size: 2Gi
- name: uploads
size: 50Gi
attributes:
persistent: true
class: beta2
```
### Cache-Heavy Application
```yaml
profiles:
compute:
cache-app:
resources:
cpu:
units: 2
memory:
size: 8Gi
storage:
- name: default
size: 1Gi
- name: cache
size: 4Gi
attributes:
class: ram
```
## Provider Availability
Not all providers support all storage classes. Use provider attributes to filter:
```yaml
profiles:
placement:
dcloud:
attributes:
capabilities/storage/2/class: beta3 # Require beta3 support
pricing:
web:
denom: uact
amount: 2000
```
## Cost Considerations
Storage costs are included in the overall bid price. Higher-performance storage classes (beta3) may result in:
- Higher bid prices from providers
- Fewer available providers
- Better I/O performance
Balance performance needs against cost and availability.
rules/sdk/go/client-setup.md
# Go SDK Client Setup
Complete client configuration for Akash Go SDK.
## Full Client Setup
```go
package akash
import (
"context"
"fmt"
"os"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
// Codec registration is per-module in pkg.akt.dev/go (there is no
// aggregate "node/codec" package). Each module exposes its own
// RegisterInterfaces(registry). Verify exports at:
// https://pkg.go.dev/pkg.akt.dev/go
certv1 "pkg.akt.dev/go/node/cert/v1"
deploymentv1beta4 "pkg.akt.dev/go/node/deployment/v1beta4"
marketv1beta5 "pkg.akt.dev/go/node/market/v1beta5"
)
type AkashClient struct {
clientCtx client.Context
txFactory tx.Factory
keyring keyring.Keyring
address sdk.AccAddress
}
func NewAkashClient(
nodeURI string,
chainID string,
keyringBackend string,
keyringDir string,
keyName string,
) (*AkashClient, error) {
// Configure SDK
config := sdk.GetConfig()
config.SetBech32PrefixForAccount("akash", "akashpub")
config.Seal()
// Create codec — register each Akash module's interfaces individually
// (no single aggregate registration helper exists).
interfaceRegistry := codectypes.NewInterfaceRegistry()
deploymentv1beta4.RegisterInterfaces(interfaceRegistry)
marketv1beta5.RegisterInterfaces(interfaceRegistry)
certv1.RegisterInterfaces(interfaceRegistry)
cdc := codec.NewProtoCodec(interfaceRegistry)
// Setup keyring
kr, err := keyring.New(
"akash",
keyringBackend,
keyringDir,
os.Stdin,
cdc,
)
if err != nil {
return nil, fmt.Errorf("failed to create keyring: %w", err)
}
// Get key
keyInfo, err := kr.Key(keyName)
if err != nil {
return nil, fmt.Errorf("failed to get key: %w", err)
}
address, err := keyInfo.GetAddress()
if err != nil {
return nil, fmt.Errorf("failed to get address: %w", err)
}
// Create gRPC connection
grpcConn, err := grpc.NewClient(
nodeURI,
// Public mainnet gRPC is TLS on :443 — use TLS creds (system roots), not insecure.
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
)
if err != nil {
return nil, fmt.Errorf("failed to connect to node: %w", err)
}
// Create client context
clientCtx := client.Context{}.
WithChainID(chainID).
WithCodec(cdc).
WithInterfaceRegistry(interfaceRegistry).
WithTxConfig(tx.NewTxConfig(cdc, tx.DefaultSignModes)).
WithAccountRetriever(authtypes.AccountRetriever{}).
WithBroadcastMode(flags.BroadcastSync).
WithKeyring(kr).
WithFromName(keyName).
WithFromAddress(address).
WithGRPCClient(grpcConn)
// Create tx factory
txFactory := tx.Factory{}.
WithChainID(chainID).
WithKeybase(kr).
WithGas(200000).
WithGasAdjustment(1.5).
WithGasPrices("0.025uakt").
WithSignMode(signing.SignMode_SIGN_MODE_DIRECT)
return &AkashClient{
clientCtx: clientCtx,
txFactory: txFactory,
keyring: kr,
address: address,
}, nil
}
func (c *AkashClient) Address() string {
return c.address.String()
}
func (c *AkashClient) Close() error {
if c.clientCtx.GRPCClient != nil {
return c.clientCtx.GRPCClient.Close()
}
return nil
}
```
## Deployment Operations
In `pkg.akt.dev/go` v0.2.14 the message/ID types span several packages: the
deployment messages live in `deployment/v1beta4`, but `DeploymentID` (and the
`Deployment`/`DeploymentReclamation` types) live in `deployment/v1`, and the
`Deposit` value type lives in `types/deposit/v1`. Verify every symbol below at
<https://pkg.go.dev/pkg.akt.dev/go>.
```go
import (
deploymentv1 "pkg.akt.dev/go/node/deployment/v1"
deploymentv1beta4 "pkg.akt.dev/go/node/deployment/v1beta4"
depositv1 "pkg.akt.dev/go/node/types/deposit/v1"
)
func (c *AkashClient) CreateDeployment(
ctx context.Context,
dseq uint64,
groups deploymentv1beta4.GroupSpecs,
hash []byte,
amount sdk.Coin,
) (*sdk.TxResponse, error) {
// MsgCreateDeployment v1beta4 fields are ID/Groups/Hash/Deposit/Reclamation.
// There is no Version or Depositor field, and Deposit is a deposit/v1.Deposit
// (which wraps the sdk.Coin), not a bare sdk.Coin.
msg := &deploymentv1beta4.MsgCreateDeployment{
ID: deploymentv1.DeploymentID{
Owner: c.address.String(),
DSeq: dseq,
},
Groups: groups,
Hash: hash,
Deposit: depositv1.Deposit{
Amount: amount,
},
}
return c.broadcastTx(ctx, msg)
}
func (c *AkashClient) CloseDeployment(
ctx context.Context,
dseq uint64,
) (*sdk.TxResponse, error) {
msg := &deploymentv1beta4.MsgCloseDeployment{
ID: deploymentv1.DeploymentID{
Owner: c.address.String(),
DSeq: dseq,
},
}
return c.broadcastTx(ctx, msg)
}
// NOTE: MsgDepositDeployment was removed in deployment/v1beta4 (it only exists
// in the older v1beta3). To add funds to an existing deployment's escrow
// account on current chains, deposit via the escrow module / `provider-services
// tx deployment deposit` rather than a deployment Msg. Check the current
// message set at https://pkg.go.dev/pkg.akt.dev/go before relying on this.
func (c *AkashClient) QueryDeployment(
ctx context.Context,
dseq uint64,
) (*deploymentv1beta4.QueryDeploymentResponse, error) {
queryClient := deploymentv1beta4.NewQueryClient(c.clientCtx.GRPCClient)
return queryClient.Deployment(ctx, &deploymentv1beta4.QueryDeploymentRequest{
ID: deploymentv1.DeploymentID{
Owner: c.address.String(),
DSeq: dseq,
},
})
}
```
### Resource reclamation field (v2.1.0+)
The `Reclamation` field on `MsgCreateDeployment` (a `deploymentv1.DeploymentReclamation`)
is the AEP-82 minimum grace window — see **@../../sdl/reclamation.md** for what it
means. Leave it nil to keep pre-2.1 behavior; populate it (`MinWindow`, a
`time.Duration` within the governance bounds 1h–720h) to require a reclamation
window. The field is ignored by pre-2.1.0 nodes, so gate on discovery
(`node_version >= 2.1.0`) before relying on it — see below.
## Market Operations
As with deployments, the market `Msg`/query types live in `market/v1beta5` while
the `BidID` identifier lives in `market/v1`. Verify at
<https://pkg.go.dev/pkg.akt.dev/go>.
`MsgLeaseStartReclaim` (provider-signed; starts reclamation on an `Active` lease)
also lives in `pkg.akt.dev/go/node/market/v1beta5`, with `LeaseID` in
`pkg.akt.dev/go/node/market/v1`. Tenants do not send it; resume a paused group with
`MsgStartGroup` from `pkg.akt.dev/go/node/deployment/v1beta4`.
```go
import (
marketv1 "pkg.akt.dev/go/node/market/v1"
marketv1beta5 "pkg.akt.dev/go/node/market/v1beta5"
)
func (c *AkashClient) CreateLease(
ctx context.Context,
dseq uint64,
gseq uint32,
oseq uint32,
provider string,
) (*sdk.TxResponse, error) {
msg := &marketv1beta5.MsgCreateLease{
BidID: marketv1.BidID{
Owner: c.address.String(),
DSeq: dseq,
GSeq: gseq,
OSeq: oseq,
Provider: provider,
},
}
return c.broadcastTx(ctx, msg)
}
func (c *AkashClient) QueryBids(
ctx context.Context,
dseq uint64,
) (*marketv1beta5.QueryBidsResponse, error) {
queryClient := marketv1beta5.NewQueryClient(c.clientCtx.GRPCClient)
return queryClient.Bids(ctx, &marketv1beta5.QueryBidsRequest{
Filters: marketv1beta5.BidFilters{
Owner: c.address.String(),
DSeq: dseq,
},
})
}
func (c *AkashClient) QueryLeases(
ctx context.Context,
) (*marketv1beta5.QueryLeasesResponse, error) {
queryClient := marketv1beta5.NewQueryClient(c.clientCtx.GRPCClient)
return queryClient.Leases(ctx, &marketv1beta5.QueryLeasesRequest{
Filters: marketv1.LeaseFilters{
Owner: c.address.String(),
},
})
}
```
## Transaction Broadcasting
```go
func (c *AkashClient) broadcastTx(
ctx context.Context,
msgs ...sdk.Msg,
) (*sdk.TxResponse, error) {
// Build unsigned tx
txBuilder, err := tx.BuildUnsignedTx(c.txFactory, msgs...)
if err != nil {
return nil, fmt.Errorf("failed to build tx: %w", err)
}
// Sign tx
err = tx.Sign(c.txFactory, c.clientCtx.FromName, txBuilder, true)
if err != nil {
return nil, fmt.Errorf("failed to sign tx: %w", err)
}
// Encode tx
txBytes, err := c.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
return nil, fmt.Errorf("failed to encode tx: %w", err)
}
// Broadcast
res, err := c.clientCtx.BroadcastTx(txBytes)
if err != nil {
return nil, fmt.Errorf("failed to broadcast tx: %w", err)
}
if res.Code != 0 {
return res, fmt.Errorf("tx failed: %s", res.RawLog)
}
return res, nil
}
```
## Usage Example
```go
func main() {
client, err := NewAkashClient(
"akash-grpc.publicnode.com:443",
"akashnet-2",
"file",
os.ExpandEnv("$HOME/.akash"),
"wallet",
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
// Create deployment
dseq := uint64(time.Now().Unix())
deposit := sdk.NewCoin("uact", sdk.NewInt(5000000))
// groups is a deploymentv1beta4.GroupSpecs built from your SDL; hash is the
// manifest version hash (sha256 of the manifest).
txRes, err := client.CreateDeployment(ctx, dseq, groups, hash, deposit)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Deployment created: %s\n", txRes.TxHash)
}
```
rules/sdk/go/installation.md
# Go SDK Installation
Install and configure the Akash Go SDK for backend services.
## Packages
```go
// Akash API types and clients
import "pkg.akt.dev/go/node/deployment/v1beta4"
import "pkg.akt.dev/go/node/market/v1beta5"
// Cosmos SDK for client operations
import "github.com/cosmos/cosmos-sdk/client"
import "github.com/cosmos/cosmos-sdk/crypto/keyring"
```
For v2.1.0 (resource reclamation, Oracle V2, discovery), pull the version-matched
`pkg.akt.dev/go` and verify symbols at <https://pkg.go.dev/pkg.akt.dev/go>. Relevant
packages: `node/market/v1beta5` holds `MsgLeaseStartReclaim`; `node/deployment/v1`
holds `DeploymentReclamation`; the discovery `GetInfo` query client lives under
`node/discovery/v1` (confirm the exact path for your pinned version). See
**@client-setup.md** for usage.
## Installation
```bash
go get pkg.akt.dev/go@latest
go get github.com/cosmos/cosmos-sdk@v0.47.x
```
## Basic Setup
```go
package main
import (
"context"
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
deploymentv1beta4 "pkg.akt.dev/go/node/deployment/v1beta4"
)
func main() {
// Configure SDK
config := sdk.GetConfig()
config.SetBech32PrefixForAccount("akash", "akashpub")
config.Seal()
// Create client context
clientCtx := client.Context{}.
WithChainID("akashnet-2").
WithNodeURI("https://rpc.akashnet.net:443")
fmt.Println("Akash Go SDK ready")
}
```
## Configuration
### Client Context
```go
import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func createClientContext() (client.Context, error) {
// Create codec
interfaceRegistry := codectypes.NewInterfaceRegistry()
cdc := codec.NewProtoCodec(interfaceRegistry)
// Create gRPC connection
grpcConn, err := grpc.NewClient(
"akash-grpc.publicnode.com:443",
// Public mainnet gRPC is TLS on :443 — use TLS creds (system roots), not insecure.
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
)
if err != nil {
return client.Context{}, err
}
clientCtx := client.Context{}.
WithChainID("akashnet-2").
WithCodec(cdc).
WithInterfaceRegistry(interfaceRegistry).
WithGRPCClient(grpcConn)
return clientCtx, nil
}
```
### Keyring Setup
```go
import (
"github.com/cosmos/cosmos-sdk/crypto/keyring"
)
func setupKeyring(homeDir string) (keyring.Keyring, error) {
kr, err := keyring.New(
"akash",
keyring.BackendFile,
homeDir,
nil,
cdc,
)
if err != nil {
return nil, err
}
return kr, nil
}
```
## Query Examples
### Query Deployment
```go
import (
"context"
deploymentv1 "pkg.akt.dev/go/node/deployment/v1"
deploymentv1beta4 "pkg.akt.dev/go/node/deployment/v1beta4"
)
func queryDeployment(
ctx context.Context,
clientCtx client.Context,
owner string,
dseq uint64,
) (*deploymentv1beta4.QueryDeploymentResponse, error) {
queryClient := deploymentv1beta4.NewQueryClient(clientCtx.GRPCClient)
// DeploymentID lives in the deployment/v1 package; the query request
// field is `ID` (capital D).
return queryClient.Deployment(ctx, &deploymentv1beta4.QueryDeploymentRequest{
ID: deploymentv1.DeploymentID{
Owner: owner,
DSeq: dseq,
},
})
}
```
### Query Leases
```go
import (
marketv1 "pkg.akt.dev/go/node/market/v1" // LeaseFilters lives here
marketv1beta5 "pkg.akt.dev/go/node/market/v1beta5" // Query client + request/response types
)
func queryLeases(
ctx context.Context,
clientCtx client.Context,
owner string,
) (*marketv1beta5.QueryLeasesResponse, error) {
queryClient := marketv1beta5.NewQueryClient(clientCtx.GRPCClient)
return queryClient.Leases(ctx, &marketv1beta5.QueryLeasesRequest{
Filters: marketv1.LeaseFilters{
Owner: owner,
State: "active",
},
})
}
```
## Transaction Examples
### Create Deployment
```go
import (
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
deploymentv1 "pkg.akt.dev/go/node/deployment/v1"
deploymentv1beta4 "pkg.akt.dev/go/node/deployment/v1beta4"
depositv1 "pkg.akt.dev/go/node/types/deposit/v1"
)
func createDeployment(
clientCtx client.Context,
owner string,
dseq uint64,
deposit sdk.Coin,
) error {
// In v1beta4 MsgCreateDeployment has no Version or Depositor fields, and
// Deposit is a deposit/v1.Deposit (which wraps the coin), not a bare sdk.Coin.
// DeploymentID lives in the deployment/v1 package.
msg := &deploymentv1beta4.MsgCreateDeployment{
ID: deploymentv1.DeploymentID{
Owner: owner,
DSeq: dseq,
},
Groups: deploymentv1beta4.GroupSpecs{}, // Add your groups
Deposit: depositv1.Deposit{Amount: deposit},
}
txf := tx.Factory{}.
WithChainID(clientCtx.ChainID).
WithGas(200000).
WithGasAdjustment(1.5).
WithGasPrices("0.025uakt")
return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg)
}
```
## Error Handling
```go
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func handleQueryError(err error) error {
if err == nil {
return nil
}
st, ok := status.FromError(err)
if !ok {
return fmt.Errorf("unknown error: %w", err)
}
switch st.Code() {
case codes.NotFound:
return fmt.Errorf("resource not found")
case codes.PermissionDenied:
return fmt.Errorf("permission denied")
case codes.Unavailable:
return fmt.Errorf("service unavailable")
default:
return fmt.Errorf("grpc error: %s", st.Message())
}
}
```
## Next Steps
- **@client-setup.md** - Complete client configuration
- **@examples/** - Full working examples
rules/sdk/overview.md
# SDK Overview
Akash provides official SDKs for programmatic integration with the network.
> **Chain message versions** (verify against your installed package — these change with chain upgrades):
> - `akash.deployment.v1beta4.*` — `MsgCreateDeployment` (carries an optional `reclamation` field, AEP-82, on v2.1.0+), `MsgUpdateDeployment`, `MsgCloseDeployment`, `MsgDepositDeployment`, `MsgStartGroup`
> - `akash.market.v1beta5.*` — `MsgCreateBid`, `MsgCreateLease`, `MsgCloseBid`, `MsgCloseLease`, `MsgLeaseStartReclaim` (provider-signed, AEP-82, v2.1.0+)
> - `akash.cert.v1.*` — `MsgCreateCertificate`, `MsgRevokeCertificate`
>
> Older code targeting `v1beta3` deployment or `v1beta4` market will fail with a chain-side parse error. When porting examples, double-check the version segment of any `typeUrl` and SDK proxy path.
## TypeScript: `@akashnetwork/chain-sdk`
The current TypeScript SDK. **Do not use the older `@akashnetwork/akashjs`** — it is deprecated and no longer maintained.
- **Package:** [`@akashnetwork/chain-sdk`](https://www.npmjs.com/package/@akashnetwork/chain-sdk)
- **Source:** https://github.com/akash-network/chain-sdk (TypeScript lives under `ts/`)
- **Install:** `npm install @akashnetwork/chain-sdk@alpha` — you must use the `@alpha` tag; the `latest` npm tag points at an older alpha. Pin an exact version (avoid `^` / `~`).
- **Node engine:** `>=22.14.0 <25`.
- **Module type:** Dual ESM/CJS.
**Three entry points:**
| Subpath | Use |
|---|---|
| `@akashnetwork/chain-sdk` | Node / server (gRPC over HTTP/2) |
| `@akashnetwork/chain-sdk/web` | Browser (gRPC-Gateway over HTTPS — for wallet-extension flows) |
| `@akashnetwork/chain-sdk/private-types/*` | Raw generated proto types — e.g. `.../private-types/akash.v1beta4` for `MsgCreateDeployment`, `Source`, `DeploymentID` |
**Top-level exports:**
- `createChainNodeSDK`, `createChainNodeWebSDK` — chain client factories
- `createProviderSDK` — provider client (only `getStatus` / `streamStatus` today)
- `createStargateClient` — signer factory (wraps a mnemonic or `DirectSecp256k1HdWallet`)
- `transaction(sdk, messages, options?)`, `msg(method, data)` — multi-message tx batching
- `JwtTokenManager` — AEP-64 JWT signing for self-custody provider auth
- `certificateManager` — mTLS cert management (legacy; prefer JWT for new code)
- `generateManifest`, `generateManifestVersion`, `yaml` (tagged template), type `SDLInput` — SDL parsing & manifest helpers
- `SDKError`, `TxError` — error classes
Messages are accessed as methods on the SDK proxy at runtime:
```typescript
sdk.akash.deployment.v1beta4.createDeployment({ ... })
sdk.akash.market.v1beta5.createLease({ ... })
sdk.akash.market.v1beta5.getBids({ ... })
sdk.akash.cert.v1.createCertificate({ ... })
```
There is no manual type registry; you don't import `MsgCreateDeployment` directly unless you want raw types from `private-types`.
**What chain-sdk does NOT do** (so you don't go looking for it):
- Provider HTTP helpers like `sendManifest`, `queryLeaseLogs`, `leaseShell` — these were in the old akashjs and were **not ported**. For logs / events / shell, mint a JWT with `JwtTokenManager` and call the provider's HTTPS/WSS endpoints directly via `fetch` / `WebSocket` / `undici`. See **@../deploy/console-api/operations.md** for the URL templates.
- `getAkashTypeRegistry` — gone. `createStargateClient` returns a ready-to-use signer; the SDK proxy handles serialization.
- `getRpc` — gone. Just pass `baseUrl` strings to `createChainNodeSDK` and `createStargateClient`.
## Go: `pkg.akt.dev/go`
The current Go bindings live in a single module, `pkg.akt.dev/go` (repo `akash-network/chain-sdk`, `go/` subdir):
- [`pkg.akt.dev/go`](https://pkg.go.dev/pkg.akt.dev/go) — generated protobuf message types and codecs (`pkg.akt.dev/go/node/deployment/v1beta4`, `pkg.akt.dev/go/node/market/v1beta5`, `pkg.akt.dev/go/node/cert/v1`).
Use the Cosmos SDK (`github.com/cosmos/cosmos-sdk`) for transaction building, signing, and broadcasting.
## SDK Comparison
### TypeScript (`chain-sdk`)
Best for:
- Web applications (use the `/web` entry)
- Node.js services (use the default entry)
- Browser-based dApps with Keplr
Features:
- Chain transactions and queries via a flat proxy
- AEP-64 JWT minting for self-custody (`JwtTokenManager`)
- SDL manifest generation
- mTLS cert helpers (legacy)
### Go SDK
Best for:
- Backend services
- Custom tooling
- High-performance applications
- Provider development
Features:
- Full Cosmos SDK integration
- Direct gRPC access
- Idiomatic protobuf message types
- Provider integration code shared with the provider daemon
## Quick Start — TypeScript
```typescript
import { createChainNodeSDK, createStargateClient } from "@akashnetwork/chain-sdk";
const signer = createStargateClient({
baseUrl: "https://rpc.akashnet.net:443",
signerMnemonic: process.env.MNEMONIC!,
});
const sdk = createChainNodeSDK({
query: { baseUrl: "https://akash-grpc.publicnode.com:443" },
tx: { signer },
});
// Query
const deployments = await sdk.akash.deployment.v1beta4.getDeployments({
pagination: { limit: 10 },
});
// Tx
const result = await sdk.akash.deployment.v1beta4.createDeployment({
// ... see @typescript/chain-node-sdk.md for full message shape
});
```
## Quick Start — Go
```go
import (
dv1 "pkg.akt.dev/go/node/deployment/v1" // DeploymentID lives here, NOT in v1beta4
"pkg.akt.dev/go/node/deployment/v1beta4" // MsgCreateDeployment, GroupSpec(s)
deposit "pkg.akt.dev/go/node/types/deposit/v1" // Deposit type
"github.com/cosmos/cosmos-sdk/client"
)
// Prefer the constructor — in v1beta4 the message has fields
// ID / Groups / Hash / Deposit (there is no Version or Depositor field):
// func NewMsgCreateDeployment(id dv1.DeploymentID, groups []v1beta4.GroupSpec,
// hash []byte, dep deposit.Deposit) *v1beta4.MsgCreateDeployment
msg := v1beta4.NewMsgCreateDeployment(
dv1.DeploymentID{Owner: address, DSeq: dseq}, // owner bech32 + dseq
groups, // []v1beta4.GroupSpec
manifestHash, // []byte — the deployment Hash
dep, // deposit.Deposit
)
// broadcast msg using a configured client.Context
// Confirm field/symbol names for your pinned version at
// https://pkg.go.dev/pkg.akt.dev/go/node/deployment/v1beta4
```
## Discovery & capability detection (v2.1.0)
Before using v2.1.0 features (resource reclamation, Oracle V2), detect whether the
node supports them. The discovery endpoint reports the node's version and module
versions:
- **Typed:** `sdk.akash.discovery.v1.getInfo()`
- **REST:** `GET /akash/discovery/v1/info`
The response includes `chain_id`, `node_version`, `min_client_version`, and
`supported_versions[]` (each entry has an `api_version` and a `modules` map).
(`client_info.api_version` is deprecated — read `node_version` / `supported_versions`
instead.)
```typescript
const info = await sdk.akash.discovery.v1.getInfo();
import semver from "semver";
// reclamation (AEP-82) requires node v2.1.0+
const supportsReclamation = semver.gte(semver.coerce(info.nodeVersion)!, "2.1.0");
// Oracle V2 advertised via the module map
const supportsOracleV2 = info.supportedVersions
?.some(v => v.modules?.oracle === "v2") ?? false;
```
**Fallback:** pre-2.1 nodes do not implement discovery — the call returns HTTP `501`
(REST) or gRPC `UNIMPLEMENTED`. Treat that as "no v2.1.0 features" and fall back to
pre-2.1 behavior. `semver.coerce` handles `-rcN` pre-release tags.
## When to use an SDK vs. the Console API
- Use the **TypeScript SDK** when you need self-custody (Keplr or hardware) and want JS/TS code.
- Use the **Go SDK** for self-custody backends.
- Use the **Console API** (not this chapter) if you want HTTP + an API key and don't mind that Console manages the wallet. See **@../deploy/console-api/**.
The SDK paths and the Console API path are not interchangeable — pick one and stay there. See the SKILL.md "Choosing a Deployment Method" section.
## See also
- **@typescript/installation.md** — Install `chain-sdk` and wire it up
- **@typescript/chain-node-sdk.md** — Node-side flows (full deploy + bids + leases)
- **@typescript/chain-web-sdk.md** — Browser flows with wallet adapters
- **@typescript/provider-sdk.md** — Provider client + JWT auth + raw provider HTTPS calls
- **@go/installation.md** — Install Go modules
- **@go/client-setup.md** — Configure cosmos client + akash-api
- **@../deploy/console-api/operations.md** — For provider URL templates referenced by the SDK examples
rules/sdk/typescript/chain-node-sdk.md
# Chain SDK — Node.js
End-to-end deploy flows using `@akashnetwork/chain-sdk` on the server side.
If you haven't installed yet, see **@installation.md**.
## Setup
```typescript
import { createChainNodeSDK, createStargateClient } from "@akashnetwork/chain-sdk";
const signer = createStargateClient({
baseUrl: "https://rpc.akashnet.net:443",
signerMnemonic: process.env.MNEMONIC!, // 12/24-word phrase
});
const sdk = createChainNodeSDK({
query: { baseUrl: "https://akash-grpc.publicnode.com:443" },
tx: { signer },
});
const address = await signer.address();
```
## Query — list your deployments
```typescript
const result = await sdk.akash.deployment.v1beta4.getDeployments({
filters: { owner: address, state: "active" },
pagination: { limit: 50 },
});
for (const d of result.deployments) {
console.log(d.deployment.deploymentId.dseq, d.deployment.state);
}
```
## Create a deployment
The full flow has four logical steps:
1. Build the manifest + version from your SDL.
2. Send `MsgCreateDeployment`.
3. Wait for bids and pick one.
4. Send `MsgCreateLease` and push the manifest to the chosen provider.
### Step 1 — Generate manifest from SDL
```typescript
import { generateManifest, generateManifestVersion } from "@akashnetwork/chain-sdk";
import fs from "node:fs";
const sdlText = fs.readFileSync("./deploy.yaml", "utf-8");
const manifest = generateManifest(sdlText);
const version = generateManifestVersion(manifest); // bytes used as on-chain hash
```
Alternatively, use the `yaml` tagged template for inline SDL:
```typescript
import { yaml, generateManifest } from "@akashnetwork/chain-sdk";
const sdlText = yaml`
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu: { units: 0.5 }
memory: { size: 512Mi }
storage: { size: 1Gi }
placement:
dcloud:
pricing:
web: { denom: uact, amount: 1000 }
deployment:
web:
dcloud:
profile: web
count: 1
`;
const manifest = generateManifest(sdlText);
```
### Step 2 — Send `MsgCreateDeployment`
```typescript
import { Source } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4";
const dseq = BigInt(Math.floor(Date.now() / 1000));
const createResult = await sdk.akash.deployment.v1beta4.createDeployment({
id: { owner: address, dseq },
groups: manifest.groups,
version: generateManifestVersion(manifest),
deposit: { denom: "uact", amount: "5000000" },
depositor: address,
// Or fund from balance via `sources`:
// deposit: { sources: [Source.balance] },
});
console.log("Created", createResult.transactionHash, "dseq:", dseq);
```
### Step 3 — Wait for and list bids
```typescript
// Poll until we have at least one bid
async function waitForBids(dseq: bigint) {
for (let i = 0; i < 12; i++) {
const bids = await sdk.akash.market.v1beta5.getBids({
filters: { owner: address, dseq, state: "open" },
});
if (bids.bids.length > 0) return bids.bids;
await new Promise((r) => setTimeout(r, 5000));
}
throw new Error("No bids received");
}
const bids = await waitForBids(dseq);
const cheapest = bids.sort((a, b) =>
Number(BigInt(a.bid.price.amount) - BigInt(b.bid.price.amount))
)[0];
```
Each bid exposes `bid.bidId.{owner,dseq,gseq,oseq,provider}` and `bid.price.{denom,amount}`. The `resourcesOffer` array tells you what the provider committed to.
### Step 4 — Accept the bid (create lease)
```typescript
const leaseResult = await sdk.akash.market.v1beta5.createLease({
bidId: cheapest.bid.bidId,
});
console.log("Lease created", leaseResult.transactionHash);
```
After `createLease`, you also need to push the manifest to the provider so it actually starts the workload. The provider call is **not** part of the chain SDK — see **@provider-sdk.md** for the JWT + provider URL pattern.
## Update a deployment (push new SDL)
```typescript
const newManifest = generateManifest(fs.readFileSync("./deploy.v2.yaml", "utf-8"));
const newVersion = generateManifestVersion(newManifest);
await sdk.akash.deployment.v1beta4.updateDeployment({
id: { owner: address, dseq },
version: newVersion,
});
// then push newManifest to the provider — see provider-sdk.md
```
## Close a deployment
```typescript
await sdk.akash.deployment.v1beta4.closeDeployment({
id: { owner: address, dseq },
});
```
This closes the deployment and all its leases on-chain.
## Deposit additional funds
```typescript
await sdk.akash.deployment.v1beta4.depositDeployment({
id: { owner: address, dseq },
amount: { denom: "uact", amount: "5000000" },
depositor: address,
});
```
## Capability detection (v2.1.0)
Before relying on v2.1.0 features, check the node via discovery:
```typescript
import semver from "semver";
const info = await sdk.akash.discovery.v1.getInfo();
const supportsReclamation = semver.gte(semver.coerce(info.nodeVersion)!, "2.1.0");
// pre-2.1 nodes throw UNIMPLEMENTED / return HTTP 501 — treat as unsupported
```
See **@../overview.md** ("Discovery & capability detection") for the full response
shape and fallback handling.
## Resource reclamation (v2.1.0)
A tenant opts in to reclamation by including a `reclamation` block in the SDL (see
**@../../sdl/reclamation.md**) — `createDeployment` carries it through automatically
from the generated manifest; no extra SDK argument is needed.
`MsgLeaseStartReclaim` is **provider-signed** and starts reclamation on an `Active`
lease. Tenants do not send it; it is documented here for surface completeness:
```typescript
// provider-side — reason is a LeaseClosedReason in the provider range 10000–19999
await sdk.akash.market.v1beta5.leaseStartReclaim({
id: { owner, dseq, gseq, oseq, provider }, // LeaseID
reason: 10001,
});
```
After the reclamation deadline the provider may close the lease, leaving the group
paused; the tenant resumes with `MsgStartGroup`
(`sdk.akash.deployment.v1beta4.startGroup({ id })`). See
**@../../deploy/cli/deployment-lifecycle.md** for the full lifecycle.
## Multi-message transactions
If you want to batch (e.g. accept a lease and create a cert in one tx):
```typescript
import { transaction, msg } from "@akashnetwork/chain-sdk";
const result = await transaction(sdk, [
msg("akash.market.v1beta5.createLease", { bidId: cheapest.bid.bidId }),
msg("akash.cert.v1.createCertificate", { cert: certPem, pubkey: pubKeyPem }),
], { memo: "deploy run #42" });
```
## Error handling
```typescript
import { SDKError, TxError } from "@akashnetwork/chain-sdk";
try {
await sdk.akash.deployment.v1beta4.createDeployment({ ... });
} catch (err) {
if (err instanceof TxError) {
// chain-side error: insufficient gas, invalid msg, etc.
console.error("Tx failed:", err.code, err.rawLog);
} else if (err instanceof SDKError) {
// network / transport issue
console.error("SDK error:", err.code, err.message);
} else {
throw err;
}
}
```
## Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| `MsgCreateDeployment` parse error | Wrong message version | Use `v1beta4`, not `v1beta3` |
| `MsgCreateLease` parse error | Wrong message version | Use `v1beta5`, not `v1beta4` |
| Insufficient gas | Default gas too low for SDL with many services | Pass `gas: "auto"` or set a higher limit in tx options |
| No bids | Pricing too low, or SDL resources don't match providers | Use `POST /v1/bid-screening` on the Console API; for in-skill diagnosis see `rules/bid-matching/` in the `akash` skill |
| Manifest push fails | Sent to wrong provider host | Resolve `hostUri` from `sdk.akash.audit.v1` queries or the Console API's `GET /v1/providers/{address}` |
## See also
- **@installation.md** — Setup and dependencies
- **@chain-web-sdk.md** — Browser flows
- **@provider-sdk.md** — Provider client, JWT minting, manifest push, logs
- **@../../deploy/console-api/operations.md** — Provider URL templates and JWT pattern
rules/sdk/typescript/chain-web-sdk.md
# Chain SDK — Browser
Using `@akashnetwork/chain-sdk/web` from a browser application, signing transactions with a wallet extension (Keplr).
If you haven't installed yet, see **@installation.md**.
## Why a separate entry?
The default `chain-sdk` entry uses gRPC over HTTP/2 via `@connectrpc/connect-node`, which doesn't work in browsers. The `/web` entry uses gRPC-Gateway (regular HTTPS) and works wherever `fetch` is available — browsers, Workers, edge runtimes.
```typescript
// Node
import { createChainNodeSDK } from "@akashnetwork/chain-sdk";
// Browser / edge
import { createChainNodeWebSDK } from "@akashnetwork/chain-sdk/web";
```
## Wallet integration (Keplr)
```typescript
import { createChainNodeWebSDK } from "@akashnetwork/chain-sdk/web";
import { SigningStargateClient } from "@cosmjs/stargate";
// Wait for Keplr to be injected
async function getKeplrSigner() {
if (!window.keplr) throw new Error("Keplr not installed");
await window.keplr.enable("akashnet-2");
return window.keplr.getOfflineSigner("akashnet-2");
}
const offlineSigner = await getKeplrSigner();
const accounts = await offlineSigner.getAccounts();
const address = accounts[0].address;
const stargate = await SigningStargateClient.connectWithSigner(
"https://rpc.akashnet.net:443",
offlineSigner,
);
const sdk = createChainNodeWebSDK({
query: { baseUrl: "https://api.akashnet.net" }, // gRPC-Gateway base
tx: { signer: stargate },
});
```
## Wallet integration (WalletConnect / hardware)
WalletConnect's offline-signer adapter and Ledger's `LedgerSigner` from `@cosmjs/ledger-amino` both work in the same shape — they expose `signDirect` / `signAmino` and `getAccounts`. Wire them into `SigningStargateClient.connectWithSigner` and the rest of the flow is unchanged.
## Create a deployment from the browser
Same proxy paths as the Node example — but the user approves each tx in their wallet popup.
```typescript
import { generateManifest, generateManifestVersion } from "@akashnetwork/chain-sdk/web";
const sdlText = document.getElementById("sdl").value;
const manifest = generateManifest(sdlText);
const version = generateManifestVersion(manifest);
const dseq = BigInt(Math.floor(Date.now() / 1000));
const result = await sdk.akash.deployment.v1beta4.createDeployment({
id: { owner: address, dseq },
groups: manifest.groups,
version,
deposit: { denom: "uact", amount: "5000000" },
depositor: address,
});
// User approved the popup; tx is broadcast.
```
## Querying from the browser
Pure-query usage doesn't need a signer:
```typescript
const sdkQuery = createChainNodeWebSDK({
query: { baseUrl: "https://api.akashnet.net" },
});
const providers = await sdkQuery.akash.provider.v1beta4.providers({});
```
## Provider proxy considerations
The browser **cannot** call providers directly for logs/events because the provider's TLS cert is self-signed (not signed by a public CA). Browsers will reject the connection.
Workarounds:
1. **Run your own provider-proxy** — the Console monorepo's `apps/provider-proxy/` is a Node service that validates the provider connection and forwards requests. Host it on infrastructure you control.
2. **Use Console's public provider-proxy** — if one is published in your Console deployment configuration. Hostname is environment-specific; don't hardcode.
For SSR / Node code embedded in a Next.js app, you can avoid the proxy entirely by calling the provider from a server route with the chain-sdk's auth helpers. See **@provider-sdk.md**.
## Bundle considerations
`chain-sdk/web` and its `@cosmjs/*` dependencies are large (~300+ KB minified+gzipped combined). For client-only bundles, tree-shake aggressively:
```typescript
// Good — import only what you need
import { createChainNodeWebSDK, generateManifest } from "@akashnetwork/chain-sdk/web";
// Avoid — pulls everything
import * as chainSdk from "@akashnetwork/chain-sdk/web";
```
Most bundlers (Vite, Webpack 5, esbuild) tree-shake properly when you use named imports.
## Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| `window.keplr is undefined` | Loaded before extension is injected | Listen for the `keplr_keystorechange` event or poll briefly on load |
| Tx rejected silently | User dismissed the popup | Check for `userRejected` in the error code |
| Deposit/pricing denom rejected | Wrong denom for the context | `uakt` (AKT) is the gas/staking denom; `uact` (ACT) is the deployment-payment denom for SDL pricing/deposits — both valid in their roles |
| `Failed to fetch` on provider call | Browser CORS / cert pinning | Route through a provider-proxy server |
| Wrong chain id | Hardcoded mainnet on testnet | Use `"akashnet-2"` for mainnet, `"sandbox-01"` for sandbox |
## See also
- **@installation.md** — Setup, subpaths, exports
- **@chain-node-sdk.md** — Node-side equivalent flows
- **@provider-sdk.md** — Provider client, JWT, manifest push
- **@../../deploy/console-api/operations.md** — Provider URL templates (useful when running your own provider-proxy)
rules/sdk/typescript/installation.md
# TypeScript SDK Installation
Install and wire up `@akashnetwork/chain-sdk` — the current Akash TypeScript SDK.
> The older `@akashnetwork/akashjs` is deprecated and is not used by the Akash team or these docs. If you find AkashJS in your code, migrate to `chain-sdk`.
## Package
```bash
npm install @akashnetwork/chain-sdk@alpha
# or
yarn add @akashnetwork/chain-sdk@alpha
# or
pnpm add @akashnetwork/chain-sdk@alpha
```
The package is currently published under the `@alpha` dist-tag. The `latest` tag points at an older alpha — **don't install without specifying `@alpha`**, and pin an exact version once you've chosen one (avoid `^` / `~` ranges).
## Requirements
- **Node:** `>=22.14.0 <25`
- **TypeScript:** 5.x (the package ships its own type declarations)
- **Module type:** Dual ESM/CJS — works in both `"type": "module"` and CommonJS projects
## Subpaths
The package exposes three subpaths:
| Subpath | Use |
|---|---|
| `@akashnetwork/chain-sdk` | Default — Node / server entry, gRPC over HTTP/2 |
| `@akashnetwork/chain-sdk/web` | Browser entry, gRPC-Gateway over HTTPS |
| `@akashnetwork/chain-sdk/private-types/<package>` | Raw generated proto types — e.g. `private-types/akash.v1beta4` for `MsgCreateDeployment`, `Source`, `DeploymentID`. Marked `private` because the wire format is generated and may evolve. |
## Top-level exports
From the default entry:
```typescript
import {
// SDK factories
createChainNodeSDK,
createStargateClient,
createProviderSDK,
// Tx batching
transaction,
msg,
// Provider auth
JwtTokenManager, // AEP-64 JWTs (preferred)
certificateManager, // mTLS (legacy)
// SDL
generateManifest,
generateManifestVersion,
yaml, // tagged template
// Errors
SDKError,
TxError,
} from "@akashnetwork/chain-sdk";
```
From the web entry:
```typescript
import {
createChainNodeWebSDK,
createProviderSDK,
// ... same auth/SDL/error exports
} from "@akashnetwork/chain-sdk/web";
```
Raw proto types when you need them:
```typescript
import {
MsgCreateDeployment,
Source,
DeploymentID,
} from "@akashnetwork/chain-sdk/private-types/akash.v1beta4";
import { MsgCreateLease } from "@akashnetwork/chain-sdk/private-types/akash.v1beta5";
import { MsgCreateCertificate } from "@akashnetwork/chain-sdk/private-types/akash.v1";
```
## Minimal Node setup
```typescript
import { createChainNodeSDK, createStargateClient } from "@akashnetwork/chain-sdk";
const signer = createStargateClient({
baseUrl: "https://rpc.akashnet.net:443",
signerMnemonic: process.env.MNEMONIC!,
});
const sdk = createChainNodeSDK({
query: { baseUrl: "https://akash-grpc.publicnode.com:443" },
tx: { signer },
});
// Use the SDK
const deployments = await sdk.akash.deployment.v1beta4.getDeployments({
pagination: { limit: 10 },
});
console.log(deployments);
```
## Minimal web setup (Keplr)
The web entry expects you to supply a `TxClient` (an object with a `signAndBroadcast` method) — typically wired to a browser wallet extension.
```typescript
import { createChainNodeWebSDK } from "@akashnetwork/chain-sdk/web";
import { SigningStargateClient } from "@cosmjs/stargate";
const offlineSigner = window.keplr.getOfflineSigner("akashnet-2");
const stargate = await SigningStargateClient.connectWithSigner(
"https://rpc.akashnet.net:443",
offlineSigner,
);
const sdk = createChainNodeWebSDK({
query: { baseUrl: "https://api.akashnet.net" }, // gRPC-Gateway
tx: { signer: stargate },
});
```
## Transport retries
Retries are off by default. To enable on queries (never on tx):
```typescript
const sdk = createChainNodeSDK({
query: { baseUrl: "https://akash-grpc.publicnode.com:443" },
tx: { signer },
transportOptions: {
retry: { maxAttempts: 3, maxDelayMs: 2000 },
},
});
```
Retries only fire on gRPC codes `Unavailable`, `DeadlineExceeded`, `Internal`, `Unknown`. Use [`cockatiel`](https://www.npmjs.com/package/cockatiel) under the hood.
## Troubleshooting
### Wrong tag installed
```bash
npm ls @akashnetwork/chain-sdk
# If you see 1.0.0-alpha.0 (old), reinstall:
npm install @akashnetwork/chain-sdk@alpha
```
### Node version error
```
Engines incompatibility: requires node >=22.14.0 <25
```
Upgrade Node, or use `nvm use 22`.
### CJS / ESM interop
If you're in a CJS project requiring an ESM-only build:
```javascript
// CommonJS — use the require export
const { createChainNodeSDK } = require("@akashnetwork/chain-sdk");
```
The dual-build package.json `exports` map handles both.
### `private-types/*` not resolving
You're probably on the old `1.0.0-alpha.0` — the subpath map changed in the `@alpha` channel. Reinstall.
## See also
- **@chain-node-sdk.md** — Node-side flows: create deployment, list bids, accept lease, batch txs
- **@chain-web-sdk.md** — Browser flows with wallet extensions
- **@provider-sdk.md** — Provider client and the JWT-auth pattern for direct provider calls
rules/sdk/typescript/provider-sdk.md
# Provider SDK & Provider-Direct Calls
Communicating with Akash providers using `@akashnetwork/chain-sdk`.
The provider surface in `chain-sdk` is intentionally small — `getStatus` / `streamStatus` only. Everything else (logs, events, manifest push, shell) is done by:
1. Minting an AEP-64 JWT with `JwtTokenManager`.
2. Calling the provider's HTTPS / WSS endpoints directly with `fetch` / `WebSocket` / `undici`.
## What `createProviderSDK` gives you
```typescript
import { createProviderSDK } from "@akashnetwork/chain-sdk";
const provider = createProviderSDK({
baseUrl: hostUri, // from sdk.akash.provider.v1beta4 query or Console API
// auth: { type: "jwt", token } | { type: "mtls", certPem, keyPem }
});
// One-shot status
const status = await provider.getStatus();
// Streaming status (resilient)
for await (const status of provider.streamStatus()) {
console.log(status);
}
```
For anything beyond status, see the patterns below.
## Mint a JWT
### Self-custody (recommended for chain-sdk users)
```typescript
import { JwtTokenManager } from "@akashnetwork/chain-sdk";
const jwtMgr = new JwtTokenManager(signer); // signer from createStargateClient
const jwt = await jwtMgr.generateToken({
iss: address,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 1800,
version: "v1",
leases: {
access: "scoped",
scope: ["status", "logs", "events", "shell"],
},
});
```
The JWT payload follows [AEP-64](https://akash.network/roadmap/aep-64/); `generateToken` validates it and throws on anything else. The `leases` claim has three forms:
```typescript
// Full: every action on every lease. Takes no scope.
{ access: "full" }
// Scoped: the listed actions on every lease
{ access: "scoped", scope: ["logs"] }
// Granular: per provider, either full, scoped, or per deployment
{
access: "granular",
permissions: [
{ provider: "akash1provA...", access: "scoped", scope: ["logs"] },
{
provider: "akash1provB...",
access: "granular",
deployments: [{ dseq: 12345678, gseq: 1, services: ["web"], scope: ["logs"] }],
},
],
}
```
In a per-deployment entry, `dseq` is a number and `services` lists at least one service. Valid scopes: `send-manifest`, `get-manifest`, `logs`, `shell`, `events`, `status`, `restart`, `hostname-migrate`, `ip-migrate`, `attestation`. Grant the minimum.
### Managed wallet (Console API users)
You don't have the key — Console signs the JWT for you:
```bash
curl -X POST https://console-api.akash.network/v1/create-jwt-token \
-H "x-api-key: $AKASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data":{"ttl":1800,"leases":{"access":"scoped","scope":["logs"]}}}'
```
See **@../../deploy/console-api/operations.md**. A key limited to some projects (bound to a project, or held by a team `member` or `viewer`) gets 403 `project_forbidden` for this `scoped` claim and must name its deployments with the `granular` form.
## Resolve the provider's `hostUri`
The deployment object gives you the provider's address, not the URL:
```typescript
// From the SDK
const lease = await sdk.akash.market.v1beta5.getLeases({
filters: { owner: address, dseq, state: "active" },
});
const providerAddress = lease.leases[0].lease.leaseId.provider;
const prov = await sdk.akash.provider.v1beta4.provider({ owner: providerAddress });
const hostUri = prov.provider.hostUri;
```
```bash
# Or via the Console API (public; the provider object is not wrapped in `data`)
curl https://console-api.akash.network/v1/providers/$PROVIDER \
| jq -r .hostUri
```
## Provider TLS — skip verification for now
Provider TLS certs are self-signed (not issued by a public CA), so standard HTTPS clients reject them. Authentication is the JWT — there's no client certificate to manage. For direct, server-side provider calls, disable TLS verification:
```typescript
import https from "node:https";
import { fetch, Agent } from "undici";
const agent = new Agent({
connect: { rejectUnauthorized: false },
});
// Then pass `dispatcher: agent` to undici fetch.
```
For Node's built-in `https` module:
```typescript
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
```
Don't hand-roll CN-matching against the provider's on-chain address — first-class provider-certificate verification is coming to `@akashnetwork/chain-sdk`. In the browser, route through a provider proxy instead (see **@chain-web-sdk.md**).
## Stream logs
Logs are served via WebSocket on the provider:
```
wss://<hostUri-without-https>/lease/<dseq>/<gseq>/<oseq>/logs
```
Query params (optional): `follow=true`, `tail=<n>`, `service=<svc-name>`.
```typescript
import WebSocket from "ws";
const wss = hostUri.replace(/^https?:\/\//, "");
const ws = new WebSocket(
`wss://${wss}/lease/${dseq}/${gseq}/${oseq}/logs?follow=true&tail=200`,
{
headers: { Authorization: `Bearer ${jwt}` },
agent: httpsAgent, // TLS-verification-disabled agent from above
}
);
ws.on("message", (chunk) => process.stdout.write(chunk));
ws.on("close", () => console.log("Logs stream closed"));
ws.on("error", (err) => console.error("Logs error:", err));
```
## Stream events
The provider path is `/kubeevents`, not `/events` (the Console UI aliases the latter to the former — that alias is client-side only):
```typescript
const ws = new WebSocket(
`wss://${wss}/lease/${dseq}/${gseq}/${oseq}/kubeevents`,
{
headers: { Authorization: `Bearer ${jwt}` },
agent: httpsAgent,
}
);
```
## Read lease status
This is what `createProviderSDK().getStatus()` does under the hood; you can also call it manually:
```typescript
const res = await fetch(
`${hostUri}/lease/${dseq}/${gseq}/${oseq}/status`,
{
headers: { Authorization: `Bearer ${jwt}` },
dispatcher: agent,
}
);
const status = await res.json();
console.log(status.services, status.forwardedPorts, status.ips);
```
## Push a manifest
After `createLease`, push the manifest so the provider starts the workload:
```typescript
const res = await fetch(
`${hostUri}/deployment/${dseq}/manifest`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
},
body: JSON.stringify(manifest), // from generateManifest()
dispatcher: agent,
}
);
```
The JWT needs `send-manifest` scope for this.
## Shell into a running pod
```
wss://<hostUri>/lease/<dseq>/<gseq>/<oseq>/shell?service=<svc>&podIndex=<n>&cmd=<base64-cmd>
```
The protocol is a binary multiplexed stream (stdin/stdout/stderr/resize). Use a library like `xterm-headless` or implement the framing yourself; the Console web app uses `xterm.js`. The JWT needs `shell` scope.
## Restart a service
The `restart` scope authorizes:
```
POST {hostUri}/lease/{dseq}/{gseq}/{oseq}/services/{service}/restart
```
## Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| 401 from provider | Wrong JWT or scope missing | Re-mint with the right scope, set `Authorization: Bearer` |
| 404 on `/events` | Used the alias | Use `/kubeevents` |
| Cert rejected | Provider cert is self-signed | Set `rejectUnauthorized: false` (server-side) or use a provider proxy (browser) |
| Logs hang then close | `follow=true` plus an idle service | Normal; reconnect on close |
| Stream stops mid-session | JWT expired | Increase `ttl` or refresh by minting a new JWT |
| 403 on `send-manifest` | JWT lacks scope | Re-mint with `scope: ["send-manifest", ...]` |
| Calling Console API for logs | Wrong service | Logs come from the provider; Console API only mints the JWT |
## See also
- **@installation.md** — Install + dependencies
- **@chain-node-sdk.md** — Full deploy flow ending with `createLease`
- **@chain-web-sdk.md** — Browser flow; provider calls usually need a proxy
- **@../../deploy/console-api/operations.md** — Same patterns in curl form
- **AEP-64** — https://akash.network/roadmap/aep-64/
rules/sdl/compute-resources.md
# Compute Resources Configuration
The `profiles.compute` section defines resource requirements for each service profile.
## Basic Structure
```yaml
profiles:
compute:
<profile-name>:
resources:
cpu:
units: <cpu-units>
memory:
size: <memory-size>
storage:
size: <storage-size>
```
## CPU Configuration
CPU units can be specified in three formats:
| Format | Example | Description |
|--------|---------|-------------|
| Decimal | `0.5` | Half a CPU core |
| Integer | `2` | Two CPU cores |
| Millicores | `500m` | 500 millicores (= 0.5 cores) |
```yaml
profiles:
compute:
web:
resources:
cpu:
units: 0.5 # Half a core
api:
resources:
cpu:
units: 2 # Two cores
worker:
resources:
cpu:
units: 1500m # 1.5 cores
```
### CPU Architecture
`cpu.attributes.arch` requests a CPU architecture. It is the only key accepted under `cpu.attributes`.
| Value | Nodes |
|-------|-------|
| `amd64` | x86-64 |
| `arm64` | 64-bit ARM |
```yaml
profiles:
compute:
api:
resources:
cpu:
units: 2
attributes:
arch: arm64
memory:
size: 2Gi
storage:
size: 1Gi
```
### CPU Constraints
1. **Exactly `amd64` or `arm64`.** `x86_64`, `x86-64`, `aarch64`, `arm-64` and any other spelling or case fail SDL validation with `unsupported cpu architecture`.
2. **`arch` is the only recognised key.** Any other key under `cpu.attributes` fails with `unsupported cpu attribute`.
3. **Omitting it writes nothing.** A `cpu` block without `attributes` produces zero CPU attributes in the group spec. There is no default and no client fills one in, because every deployment created before `arch` existed has an empty CPU attribute set and an implicit `amd64` would change all of them. Providers place a group with no `arch` on amd64 nodes, so only add `arch: amd64` when the workload has to be pinned there.
4. **It is a hard constraint.** A provider bids only when it has nodes of the requested architecture, and the attribute is never relaxed to attract bids. See `rules/bid-matching/adaptation-rules.md`.
Set `arch: arm64` when the image is arm64-only or the workload needs ARM hardware. For a multi-arch image with no preference, leave the attribute out.
## Memory Configuration
Memory size with standard suffixes:
| Suffix | Meaning | Example |
|--------|---------|---------|
| `Ki` | Kibibytes (1024) | `512Ki` |
| `Mi` | Mebibytes (1024²) | `512Mi` |
| `Gi` | Gibibytes (1024³) | `2Gi` |
| `Ti` | Tebibytes (1024⁴) | `1Ti` |
| `k` | Kilobytes (1000) | `512k` |
| `M` | Megabytes (1000²) | `512M` |
| `G` | Gigabytes (1000³) | `2G` |
| `T` | Terabytes (1000⁴) | `1T` |
```yaml
profiles:
compute:
web:
resources:
memory:
size: 512Mi # 512 mebibytes
ml:
resources:
memory:
size: 16Gi # 16 gibibytes
```
## Storage Configuration
### Simple Storage
Single ephemeral storage volume:
```yaml
profiles:
compute:
web:
resources:
storage:
size: 1Gi
```
### Named Storage Volumes
Array of named volumes for more control:
```yaml
profiles:
compute:
db:
resources:
storage:
- name: default
size: 1Gi
- name: data
size: 10Gi
attributes:
persistent: true
class: beta2
```
### Storage Attributes
| Attribute | Values | Description |
|-----------|--------|-------------|
| `persistent` | `true`/`false` | Data survives restarts |
| `class` | `default`, `beta1`, `beta2`, `beta3`, `ram` | Storage class type |
```yaml
storage:
- name: data
size: 10Gi
attributes:
persistent: true
class: beta2 # SSD storage
- name: cache
size: 1Gi
attributes:
persistent: false
class: ram # RAM-based (fast, volatile)
```
### Storage Constraints
1. **RAM class cannot be persistent**: `class: ram` with `persistent: true` is invalid
2. **Non-RAM with attributes requires persistent**: If using `class: beta2` or `beta3`, must set `persistent: true`
## GPU Configuration
For GPU workloads:
```yaml
profiles:
compute:
ml:
resources:
cpu:
units: 4
memory:
size: 16Gi
storage:
size: 50Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
```
### GPU Attributes
```yaml
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: a100 # Specific model
ram: 80Gi # GPU memory
interface: sxm # Interface type: pcie or sxm
```
### Available GPU Models
Common NVIDIA models:
- `a100` - NVIDIA A100
- `rtxa6000` - NVIDIA RTX A6000
- `rtx3080` - NVIDIA RTX 3080
- `rtx3090` - NVIDIA RTX 3090
- `t4` - NVIDIA T4
- `v100` - NVIDIA V100
### GPU Constraints
1. **GPU with units > 0 must have attributes with vendor**
2. **GPU with units = 0 cannot have attributes**
```yaml
# Valid: GPU with vendor
gpu:
units: 1
attributes:
vendor:
nvidia:
# Valid: No GPU
gpu:
units: 0
# Invalid: GPU without vendor
gpu:
units: 1 # Error: must have vendor attribute
```
## Complete Example
```yaml
profiles:
compute:
frontend:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
backend:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
- name: default
size: 2Gi
- name: uploads
size: 10Gi
attributes:
persistent: true
class: beta2
ml-worker:
resources:
cpu:
units: 8
memory:
size: 32Gi
storage:
size: 100Gi
gpu:
units: 2
attributes:
vendor:
nvidia:
- model: a100
ram: 80Gi
```
rules/sdl/confidential-compute.md
# SDL Confidential Compute / TEE (`params.tee`)
A tenant requests a **Confidential Compute (TEE)** workload by setting a single
field — `tee` — under a service's `params`. This is the SDL surface of AEP-83
(Confidential Compute via Kata Containers).
Confidential compute is **opt-in** and needs **no SDL version bump**: `params.tee`
works with `version: "2.0"`, and a deployment that omits the field behaves exactly
as before.
## The field
```yaml
services:
web:
image: nginx:1.25.3
expose:
- port: 80
to:
- global: true
params:
tee: cpu
```
`tee` is a plain string that lives under `services.<name>.params`, alongside the
existing `storage` mounts. It takes one of two values:
| Value | Meaning |
|-------|---------|
| `cpu` | CPU-only confidential compute. The provider selects the underlying TEE platform (AMD SEV-SNP or Intel TDX). |
| `cpu-gpu` | CPU **+** GPU confidential compute. Requires GPU resources in the compute profile (see below). |
## What the tenant does *not* set
TEE deployments are deliberately simple on the tenant side:
- **No attestation toggle.** The provider injects an attestation sidecar into every
confidential workload automatically — there is no SDL field to enable or disable it.
- **No hardware / platform choice.** You request a *capability* (`cpu` or
`cpu-gpu`), not a specific chip. The provider picks the confidential-compute
hardware (AMD SEV-SNP vs Intel TDX) and the matching runtime at deploy time.
There is exactly one control — the `tee` string. Everything else (runtime class,
platform detection, attestation) is handled provider-side.
## Verifying a running deployment
The sidecar answers attestation requests. Fetch hardware-signed evidence from the
provider with a fresh nonce (`POST {hostUri}/lease/{dseq}/{gseq}/{oseq}/attestation/quote`,
JWT scope `attestation`), then check it against AMD, Intel and NVIDIA with the Console
API's `POST /v1/confidential-compute/attestation/validate` (it takes a Console API key),
or verify the reports against the vendors' roots yourself. The steps are in
[../deploy/console-api/operations.md](../deploy/console-api/operations.md#attestation-confidential-compute).
## `cpu-gpu` requires GPU resources
The `cpu-gpu` value requires the service's compute profile to declare `gpu`
resources. An SDL that sets `tee: cpu-gpu` without a GPU in the profile is
**rejected** at validation time. See the GPU section of
[compute-resources.md](compute-resources.md) for GPU profile syntax.
## Same value across a deployment group
All services in the same deployment group must use the **same** `tee` value, or
none of them may set it. Mixing TEE values (or mixing TEE and non-TEE services)
within a single group is rejected. See [validation-rules.md](validation-rules.md).
## Examples
### CPU-only confidential compute
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
to:
- global: true
params:
tee: cpu
profiles:
compute:
web:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
size: 10Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
### CPU + GPU confidential compute
```yaml
version: "2.0"
services:
inference:
image: nvidia/cuda:12.0-runtime-ubuntu22.04
expose:
- port: 8080
to:
- global: true
params:
tee: cpu-gpu
profiles:
compute:
inference:
resources:
cpu:
units: 8
memory:
size: 32Gi
storage:
size: 100Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: h100
placement:
dcloud:
pricing:
inference:
denom: uact
amount: 5000
deployment:
inference:
dcloud:
profile: inference
count: 1
```
See [examples/confidential-compute.md](examples/confidential-compute.md) for the
full annotated examples.
## Marketplace matching
Setting `tee` requires no extra placement configuration from the tenant. The SDL
builder automatically projects a `tee/type = <value>` placement attribute, and the
marketplace uses it to match your order with providers that advertise the requested
confidential-compute capability. Providers without TEE support (or without the
requested `cpu` / `cpu-gpu` capability) simply will not bid.
## Validation
- `tee` must be `cpu` or `cpu-gpu`.
- `cpu-gpu` requires GPU resources in the compute profile.
- All services in a deployment group must use the same value or none.
See [validation-rules.md](validation-rules.md) for the constraint reference.
rules/sdl/deployment.md
# Deployment Configuration
The `deployment` section maps services to placement profiles, specifying where and how many instances to run.
## Basic Structure
```yaml
deployment:
<service-name>:
<placement-name>:
profile: <compute-profile-name>
count: <instance-count>
```
## Deployment Properties
### profile (required)
References a compute profile defined in `profiles.compute`:
```yaml
profiles:
compute:
web-profile:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
deployment:
web:
dcloud:
profile: web-profile # References 'web-profile' from profiles.compute
count: 1
```
### count (required)
Number of instances to deploy (minimum 1):
```yaml
deployment:
web:
dcloud:
profile: web
count: 3 # Deploy 3 instances
```
## Matching Rules
1. **Service name** must exist in `services` section
2. **Placement name** must exist in `profiles.placement` section
3. **Profile name** must exist in `profiles.compute` section
4. **Pricing** must be defined in the placement for the referenced profile
## Single Service Example
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web: # Matches 'web' in services
dcloud: # Matches 'dcloud' in profiles.placement
profile: web # Matches 'web' in profiles.compute
count: 1
```
## Multi-Service Example
```yaml
version: "2.0"
services:
frontend:
image: myapp/frontend:v1.0.0
expose:
- port: 80
to:
- global: true
backend:
image: myapp/backend:v1.0.0
expose:
- port: 8080
to:
- service: frontend
database:
image: postgres:15
expose:
- port: 5432
to:
- service: backend
profiles:
compute:
frontend-profile:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
backend-profile:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
db-profile:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
- name: default
size: 1Gi
- name: data
size: 20Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
frontend-profile:
denom: uact
amount: 500
backend-profile:
denom: uact
amount: 1000
db-profile:
denom: uact
amount: 2000
deployment:
frontend:
dcloud:
profile: frontend-profile
count: 2 # 2 frontend instances
backend:
dcloud:
profile: backend-profile
count: 3 # 3 backend instances
database:
dcloud:
profile: db-profile
count: 1 # 1 database instance
```
## Multiple Placements
Deploy to different placements:
```yaml
profiles:
placement:
us-region:
attributes:
region: us-west
pricing:
web:
denom: uact
amount: 1000
eu-region:
attributes:
region: eu-central
pricing:
web:
denom: uact
amount: 1200
deployment:
web:
us-region:
profile: web
count: 2
eu-region:
profile: web
count: 1
```
## Scaling Considerations
- Each placement can have different instance counts
- Higher counts require more resources and higher pricing budget
- Database services typically use `count: 1` unless using replication
- Stateless services (web, API) can scale horizontally with higher counts
rules/sdl/endpoints.md
# IP Endpoints Configuration
IP endpoints allow you to lease dedicated IP addresses for your deployments. This feature requires SDL version 2.1.
## Version Requirement
```yaml
version: "2.1" # Required for IP endpoints
```
## Basic Structure
### 1. Declare the endpoint
```yaml
endpoints:
<endpoint-name>:
kind: ip
```
### 2. Reference in service expose
```yaml
services:
web:
expose:
- port: 80
to:
- ip: <endpoint-name>
```
## Complete Example
```yaml
version: "2.1"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- ip: myip
- port: 443
as: 443
to:
- ip: myip
endpoints:
myip:
kind: ip
profiles:
compute:
web:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
## Endpoint Rules
| Rule | Description |
|------|-------------|
| Must be global scope | IP endpoints expose services to the internet |
| Must be used | Declared endpoints must be referenced by at least one service |
| Single kind | Currently only `kind: ip` is supported |
| Version 2.1 | IP endpoints require SDL version "2.1" |
## Multiple Ports on Same IP
You can expose multiple ports through the same IP endpoint:
```yaml
services:
app:
image: myapp:v1.0.0
expose:
- port: 80
as: 80
to:
- ip: public-ip
- port: 443
as: 443
to:
- ip: public-ip
- port: 22
as: 22
proto: tcp
to:
- ip: public-ip
endpoints:
public-ip:
kind: ip
```
## Multiple IP Endpoints
You can declare multiple IP endpoints for different services:
```yaml
version: "2.1"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
to:
- ip: web-ip
api:
image: myapi:v1.0.0
expose:
- port: 8080
as: 80
to:
- ip: api-ip
endpoints:
web-ip:
kind: ip
api-ip:
kind: ip
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
api:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 500
api:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
api:
dcloud:
profile: api
count: 1
```
## Combining Global and IP Exposure
You can expose a service both via the default Akash hostname and a dedicated IP:
```yaml
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true # Akash-provided hostname
- port: 80
as: 8080
to:
- ip: dedicated # Dedicated IP on port 8080
endpoints:
dedicated:
kind: ip
```
## When to Use IP Endpoints
| Use Case | Recommendation |
|----------|----------------|
| Standard web apps | Use `global: true` (default hostnames) |
| Custom domains | Use IP endpoint with DNS A record |
| Non-HTTP protocols | Use IP endpoint |
| Multiple ports | Use IP endpoint |
| Gaming servers | Use IP endpoint (UDP support) |
| Email servers | Use IP endpoint |
## Cost Considerations
IP endpoints have additional costs on top of compute resources. The IP lease is billed separately from the deployment pricing. Factor this into your budget when designing deployments.
rules/sdl/examples/confidential-compute.md
# Example: Confidential Compute (TEE)
SDL configurations for Confidential Compute (TEE) workloads (AEP-83). A tenant
requests confidential compute with a single service param — `params.tee` — set to
`cpu` (CPU-only) or `cpu-gpu` (CPU + GPU).
## Use Case
- Workloads processing sensitive data (keys, PII, proprietary models) that must stay
encrypted in use, not just at rest and in transit
- Confidential AI/ML inference where both the model weights and the input data are
protected inside a TEE
- Any deployment that needs hardware-backed attestation of the runtime environment
## No Version Bump
`params.tee` works with SDL version `"2.0"` — confidential compute is opt-in and does
not require version `"2.1"`. A deployment that omits `tee` behaves exactly as before.
## CPU-Only Confidential Compute
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
params:
tee: cpu
profiles:
compute:
web:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
size: 10Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
The provider selects the underlying TEE platform (AMD SEV-SNP or Intel TDX) and the
matching Kata runtime automatically — there is nothing else to configure.
## CPU + GPU Confidential Compute
The `cpu-gpu` value **requires** GPU resources in the compute profile. Without a
`gpu` block the SDL is rejected.
```yaml
version: "2.0"
services:
inference:
image: nvidia/cuda:12.0-runtime-ubuntu22.04
command:
- "python"
- "serve.py"
expose:
- port: 8080
as: 80
to:
- global: true
params:
tee: cpu-gpu
profiles:
compute:
inference:
resources:
cpu:
units: 8
memory:
size: 32Gi
storage:
size: 100Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: h100
placement:
dcloud:
pricing:
inference:
denom: uact
amount: 5000
deployment:
inference:
dcloud:
profile: inference
count: 1
```
## Notes
1. **Two values only:** `tee` must be `cpu` or `cpu-gpu`. No other values are valid.
2. **`cpu-gpu` needs a GPU:** the compute profile must declare `gpu` resources, or the
SDL is rejected. See [../compute-resources.md](../compute-resources.md).
3. **Same value per group:** all services in a deployment group must use the same
`tee` value, or none of them. Mixed TEE values in one group are rejected.
4. **Nothing else to set:** attestation is injected by the provider automatically and
the hardware platform (SEV-SNP vs TDX) is chosen by the provider — there is no
tenant-facing attestation toggle or platform selector.
5. **Matching:** a `tee/type = <value>` placement attribute is projected
automatically; providers without the requested capability will not bid.
See [../confidential-compute.md](../confidential-compute.md) for the full reference.
rules/sdl/examples/gpu-workload.md
# Example: GPU Workload
SDL configurations for GPU-accelerated workloads including machine learning, AI inference, and rendering.
> **If you only need to *call* an open-source LLM (not host it), AkashML is a lower-friction alternative** — managed inference behind OpenAI- and Anthropic-compatible APIs, no SDL required. See [@rules/deploy/akashml/overview.md](../../deploy/akashml/overview.md). Self-deploying via the SDLs below is the right answer when you need custom weights, fine-tunes, persistent state, or a non-LLM GPU workload.
## Use Case
- Machine learning training
- AI inference
- Video encoding/transcoding
- 3D rendering
- Scientific computing
## Basic GPU SDL
```yaml
version: "2.0"
services:
ml:
image: nvidia/cuda:12.0-runtime-ubuntu22.04
command:
- "nvidia-smi"
- "-l"
- "60"
expose:
- port: 8080
as: 80
to:
- global: true
profiles:
compute:
ml:
resources:
cpu:
units: 4
memory:
size: 16Gi
storage:
size: 50Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
placement:
dcloud:
pricing:
ml:
denom: uact
amount: 10000
deployment:
ml:
dcloud:
profile: ml
count: 1
```
## GPU Model Selection
### NVIDIA A100 (High-end Training)
```yaml
version: "2.0"
services:
training:
image: pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
command:
- "python"
- "train.py"
env:
- "CUDA_VISIBLE_DEVICES=0"
expose:
- port: 6006
as: 6006
to:
- global: true
profiles:
compute:
training:
resources:
cpu:
units: 8
memory:
size: 64Gi
storage:
- name: default
size: 10Gi
- name: models
size: 200Gi
attributes:
persistent: true
class: beta2
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: a100
ram: 80Gi
placement:
dcloud:
pricing:
training:
denom: uact
amount: 50000
deployment:
training:
dcloud:
profile: training
count: 1
```
### NVIDIA RTX 3090 (Cost-effective Training)
```yaml
version: "2.0"
services:
training:
image: tensorflow/tensorflow:2.13.0-gpu
expose:
- port: 8888
as: 8888
to:
- global: true
profiles:
compute:
training:
resources:
cpu:
units: 8
memory:
size: 32Gi
storage:
size: 100Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: rtx3090
placement:
dcloud:
pricing:
training:
denom: uact
amount: 20000
deployment:
training:
dcloud:
profile: training
count: 1
```
### NVIDIA T4 (Inference)
```yaml
version: "2.0"
services:
inference:
image: nvcr.io/nvidia/tritonserver:23.08-py3
command:
- "tritonserver"
- "--model-repository=/models"
expose:
- port: 8000
as: 8000
to:
- global: true
- port: 8001
as: 8001
to:
- global: true
profiles:
compute:
inference:
resources:
cpu:
units: 4
memory:
size: 16Gi
storage:
size: 50Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: t4
placement:
dcloud:
pricing:
inference:
denom: uact
amount: 15000
deployment:
inference:
dcloud:
profile: inference
count: 1
```
## Multi-GPU Configuration
```yaml
version: "2.0"
services:
distributed-training:
image: pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
env:
- "WORLD_SIZE=4"
- "MASTER_ADDR=localhost"
- "MASTER_PORT=29500"
expose:
- port: 29500
to:
- global: true
profiles:
compute:
distributed-training:
resources:
cpu:
units: 32
memory:
size: 128Gi
storage:
size: 500Gi
gpu:
units: 4
attributes:
vendor:
nvidia:
- model: a100
interface: sxm
placement:
dcloud:
pricing:
distributed-training:
denom: uact
amount: 200000
deployment:
distributed-training:
dcloud:
profile: distributed-training
count: 1
```
## LLM Inference Example
```yaml
version: "2.0"
services:
llm:
image: ghcr.io/huggingface/text-generation-inference:1.1.0
command:
- "--model-id"
- "meta-llama/Llama-2-7b-chat-hf"
- "--port"
- "8080"
env:
- "HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}"
expose:
- port: 8080
as: 80
to:
- global: true
http_options:
read_timeout: 60000
send_timeout: 60000
profiles:
compute:
llm:
resources:
cpu:
units: 8
memory:
size: 32Gi
storage:
size: 100Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: a100
ram: 40Gi
placement:
dcloud:
pricing:
llm:
denom: uact
amount: 40000
deployment:
llm:
dcloud:
profile: llm
count: 1
```
## Stable Diffusion Example
```yaml
version: "2.0"
services:
sd:
image: stability-ai/stablediffusion:v2.1
expose:
- port: 7860
as: 7860
to:
- global: true
profiles:
compute:
sd:
resources:
cpu:
units: 4
memory:
size: 16Gi
storage:
size: 50Gi
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: rtx3090
placement:
dcloud:
pricing:
sd:
denom: uact
amount: 20000
deployment:
sd:
dcloud:
profile: sd
count: 1
```
## GPU Resource Guidelines
| GPU Model | VRAM | Typical Use Case | Price Range (uact) |
|-----------|------|------------------|-------------------|
| T4 | 16GB | Inference | 10000-20000 |
| RTX 3080 | 10GB | Light training, inference | 15000-25000 |
| RTX 3090 | 24GB | Training, inference | 18000-30000 |
| RTX A6000 | 48GB | Heavy training | 35000-50000 |
| A100 40GB | 40GB | Large models | 40000-60000 |
| A100 80GB | 80GB | Very large models | 60000-100000 |
## Best Practices
1. **Match GPU to workload**: Use T4 for inference, A100 for training
2. **Consider VRAM**: Large models need GPUs with sufficient memory
3. **Use persistent storage**: Store models and checkpoints persistently
4. **Set appropriate timeouts**: ML inference can be slow; increase HTTP timeouts
5. **Environment variables**: Pass model configs and tokens via env vars
rules/sdl/examples/ip-lease.md
# Example: IP Endpoint Lease
SDL configurations using dedicated IP addresses for deployments.
## Use Case
- Custom domain mapping with DNS A records
- Non-HTTP services (SSH, gaming, mail)
- Services requiring fixed IP addresses
- Multi-port applications
## Version Requirement
IP endpoints require SDL version 2.1:
```yaml
version: "2.1"
```
## Basic IP Lease
```yaml
version: "2.1"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- ip: public-ip
- port: 443
as: 443
to:
- ip: public-ip
endpoints:
public-ip:
kind: ip
profiles:
compute:
web:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
## Game Server Example
```yaml
version: "2.1"
services:
gameserver:
image: itzg/minecraft-server:java21
env:
- "EULA=TRUE"
- "TYPE=PAPER"
- "MEMORY=4G"
expose:
- port: 25565
as: 25565
proto: tcp
to:
- ip: game-ip
- port: 25565
as: 25565
proto: udp
to:
- ip: game-ip
params:
storage:
world-data:
mount: /data
endpoints:
game-ip:
kind: ip
profiles:
compute:
gameserver:
resources:
cpu:
units: 4
memory:
size: 6Gi
storage:
- name: default
size: 1Gi
- name: world-data
size: 20Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
gameserver:
denom: uact
amount: 5000
deployment:
gameserver:
dcloud:
profile: gameserver
count: 1
```
## SSH Access Example
```yaml
version: "2.1"
services:
dev:
image: lscr.io/linuxserver/openssh-server:9.6_p1
env:
- "PUID=1000"
- "PGID=1000"
- "TZ=UTC"
- "PUBLIC_KEY=ssh-rsa AAAA..."
- "SUDO_ACCESS=true"
expose:
- port: 2222
as: 22
proto: tcp
to:
- ip: dev-ip
endpoints:
dev-ip:
kind: ip
profiles:
compute:
dev:
resources:
cpu:
units: 2
memory:
size: 4Gi
storage:
- name: default
size: 1Gi
- name: home
size: 50Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
dev:
denom: uact
amount: 3000
deployment:
dev:
dcloud:
profile: dev
count: 1
```
## Multi-Service with Separate IPs
```yaml
version: "2.1"
services:
frontend:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- ip: frontend-ip
- port: 443
as: 443
to:
- ip: frontend-ip
api:
image: myapi:v1.0.0
expose:
- port: 8080
as: 80
to:
- ip: api-ip
- port: 8443
as: 443
to:
- ip: api-ip
endpoints:
frontend-ip:
kind: ip
api-ip:
kind: ip
profiles:
compute:
frontend:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
api:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
size: 10Gi
placement:
dcloud:
pricing:
frontend:
denom: uact
amount: 500
api:
denom: uact
amount: 2000
deployment:
frontend:
dcloud:
profile: frontend
count: 1
api:
dcloud:
profile: api
count: 1
```
## Mail Server Example
```yaml
version: "2.1"
services:
mail:
image: mailserver/docker-mailserver:13.3.1
env:
- "OVERRIDE_HOSTNAME=mail.example.com"
- "ENABLE_SPAMASSASSIN=1"
- "ENABLE_CLAMAV=1"
expose:
- port: 25
as: 25
proto: tcp
to:
- ip: mail-ip
- port: 587
as: 587
proto: tcp
to:
- ip: mail-ip
- port: 993
as: 993
proto: tcp
to:
- ip: mail-ip
params:
storage:
mail-data:
mount: /var/mail
mail-state:
mount: /var/mail-state
endpoints:
mail-ip:
kind: ip
profiles:
compute:
mail:
resources:
cpu:
units: 2
memory:
size: 4Gi
storage:
- name: default
size: 1Gi
- name: mail-data
size: 50Gi
attributes:
persistent: true
class: beta2
- name: mail-state
size: 10Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
mail:
denom: uact
amount: 4000
deployment:
mail:
dcloud:
profile: mail
count: 1
```
## Hybrid: Global + IP Endpoint
```yaml
version: "2.1"
services:
web:
image: nginx:1.25.3
expose:
# Default Akash hostname (HTTPS with auto-cert)
- port: 80
as: 80
to:
- global: true
# Dedicated IP for custom domain
- port: 80
as: 8080
to:
- ip: custom-ip
- port: 443
as: 8443
to:
- ip: custom-ip
endpoints:
custom-ip:
kind: ip
profiles:
compute:
web:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1500
deployment:
web:
dcloud:
profile: web
count: 1
```
## DNS Configuration
After deployment, configure your DNS:
1. Get the leased IP from deployment status
2. Create DNS A record:
```
example.com. A <leased-ip>
www.example.com. A <leased-ip>
```
3. For mail servers, add MX and SPF records:
```
example.com. MX 10 mail.example.com.
mail.example.com. A <leased-ip>
example.com. TXT "v=spf1 ip4:<leased-ip> -all"
```
## Best Practices
1. **Use v2.1**: IP endpoints require version "2.1"
2. **Declare before use**: Define endpoints before referencing them
3. **Use all declared endpoints**: Unused endpoints cause validation errors
4. **Consider costs**: IP leases have additional fees beyond compute
5. **Protocol awareness**: Specify `proto: udp` for UDP services
6. **Multiple ports**: Same IP can expose multiple ports
rules/sdl/examples/reclamation.md
# Example: Resource Reclamation
SDL configuration declaring a minimum reclamation grace window (AEP-82).
## Use Case
- Stateful workloads that need lead time to drain or snapshot before a provider reclaims resources
- Migrations where you must move to another lease before the current one is closed
- Any deployment that should not be reclaimed without a guaranteed notice period
## Version Requirement
The `reclamation` block requires SDL version 2.1:
```yaml
version: "2.1"
```
SDL "2.0" still works unchanged — reclamation is opt-in.
## Basic Reclamation Window
```yaml
version: "2.1"
reclamation:
min_window: "24h"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
## Stateful Service with a Longer Window
A database that needs time to flush and snapshot before reclamation requests a
multi-day window (still within the 720h governance maximum):
```yaml
version: "2.1"
reclamation:
min_window: "72h"
services:
db:
image: postgres:16.2
env:
- "POSTGRES_PASSWORD=changeme"
expose:
- port: 5432
to:
- global: false
params:
storage:
data:
mount: /var/lib/postgresql/data
profiles:
compute:
db:
resources:
cpu:
units: 2
memory:
size: 4Gi
storage:
- name: default
size: 1Gi
- name: data
size: 100Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
db:
denom: uact
amount: 5000
deployment:
db:
dcloud:
profile: db
count: 1
```
## Notes
1. **Bounds:** `min_window` must be a positive Go duration within `[1h, 720h]`.
2. **Bidding:** providers that cannot honor a window at least as long as your
`min_window` will not bid — a very long window may reduce available bids.
3. **Lifecycle:** see [../../deploy/cli/deployment-lifecycle.md](../../deploy/cli/deployment-lifecycle.md)
for the `Active → Reclaiming → paused group` flow.
4. **Compatibility:** requires node v2.1.0 / provider-services v0.13.0+. Omitting the
block keeps full SDL 2.0 behavior.
rules/sdl/examples/web-app.md
# Example: Simple Web Application
A minimal SDL for deploying a web application on Akash.
## Use Case
- Static websites
- Single-page applications
- Simple API servers
- Development/testing environments
## SDL Configuration
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
## Variations
### Node.js Application
```yaml
version: "2.0"
services:
app:
image: node:18-alpine
command:
- "node"
- "server.js"
env:
- "NODE_ENV=production"
- "PORT=3000"
expose:
- port: 3000
as: 80
to:
- global: true
profiles:
compute:
app:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 2Gi
placement:
dcloud:
pricing:
app:
denom: uact
amount: 1500
deployment:
app:
dcloud:
profile: app
count: 1
```
### Python Flask Application
```yaml
version: "2.0"
services:
api:
image: python:3.11-slim
command:
- "python"
- "-m"
- "flask"
- "run"
- "--host=0.0.0.0"
env:
- "FLASK_APP=app.py"
- "FLASK_ENV=production"
expose:
- port: 5000
as: 80
to:
- global: true
profiles:
compute:
api:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
api:
denom: uact
amount: 1000
deployment:
api:
dcloud:
profile: api
count: 1
```
### Custom Docker Image
```yaml
version: "2.0"
services:
myapp:
image: myregistry/myapp:v1.0.0
credentials:
host: myregistry.io
username: ${REGISTRY_USER}
password: ${REGISTRY_PASS}
env:
- "DATABASE_URL=postgres://..."
- "API_KEY=${API_KEY}"
expose:
- port: 8080
as: 80
to:
- global: true
http_options:
max_body_size: 10485760
read_timeout: 30000
profiles:
compute:
myapp:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
placement:
dcloud:
pricing:
myapp:
denom: uact
amount: 2000
deployment:
myapp:
dcloud:
profile: myapp
count: 2
```
## Resource Guidelines
| Application Type | CPU | Memory | Storage |
|-----------------|-----|--------|---------|
| Static site | 0.25-0.5 | 256-512Mi | 512Mi-1Gi |
| Node.js app | 0.5-2 | 512Mi-2Gi | 1-5Gi |
| Python app | 0.5-2 | 512Mi-2Gi | 1-5Gi |
| Go app | 0.25-1 | 128-512Mi | 512Mi-2Gi |
## Tips
1. **Start small**: Begin with minimal resources and scale up as needed
2. **Use specific tags**: Avoid `latest` in production; use specific version tags
3. **Environment variables**: Use env vars for configuration, never hardcode secrets
4. **HTTP options**: Configure timeouts based on your application's needs
5. **Multiple instances**: Set `count: 2` or higher for high availability
rules/sdl/examples/wordpress-db.md
# Example: WordPress with Database
A multi-service deployment with WordPress and MariaDB, featuring persistent storage.
## Use Case
- Content management systems
- Multi-tier applications
- Applications requiring databases
- Persistent data storage
## SDL Configuration
```yaml
version: "2.0"
services:
wordpress:
image: wordpress:6.4
env:
- "WORDPRESS_DB_HOST=db"
- "WORDPRESS_DB_USER=wordpress"
- "WORDPRESS_DB_PASSWORD=wordpress_password"
- "WORDPRESS_DB_NAME=wordpress"
expose:
- port: 80
as: 80
to:
- global: true
params:
storage:
wordpress-data:
mount: /var/www/html
readOnly: false
db:
image: mariadb:10.11
env:
- "MYSQL_ROOT_PASSWORD=root_password"
- "MYSQL_DATABASE=wordpress"
- "MYSQL_USER=wordpress"
- "MYSQL_PASSWORD=wordpress_password"
expose:
- port: 3306
to:
- service: wordpress
params:
storage:
db-data:
mount: /var/lib/mysql
readOnly: false
profiles:
compute:
wordpress:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
- name: default
size: 1Gi
- name: wordpress-data
size: 10Gi
attributes:
persistent: true
class: beta2
db:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
- name: default
size: 1Gi
- name: db-data
size: 20Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
wordpress:
denom: uact
amount: 2000
db:
denom: uact
amount: 3000
deployment:
wordpress:
dcloud:
profile: wordpress
count: 1
db:
dcloud:
profile: db
count: 1
```
## Key Concepts
### Service Communication
Services communicate using service names as hostnames:
- WordPress connects to `db:3306`
- The `db` service is only exposed to `wordpress` service
### Persistent Storage
Both services use persistent storage:
- WordPress content: `/var/www/html`
- Database files: `/var/lib/mysql`
Storage configuration:
```yaml
storage:
- name: db-data
size: 20Gi
attributes:
persistent: true # Data survives restarts
class: beta2 # SSD storage
```
### Storage Mounting
Connect named storage to container paths:
```yaml
params:
storage:
db-data:
mount: /var/lib/mysql
readOnly: false
```
## Variations
### With Redis Cache
```yaml
version: "2.0"
services:
wordpress:
image: wordpress:6.4
env:
- "WORDPRESS_DB_HOST=db"
- "WORDPRESS_DB_USER=wordpress"
- "WORDPRESS_DB_PASSWORD=wordpress_password"
- "WORDPRESS_DB_NAME=wordpress"
expose:
- port: 80
as: 80
to:
- global: true
params:
storage:
wordpress-data:
mount: /var/www/html
db:
image: mariadb:10.11
env:
- "MYSQL_ROOT_PASSWORD=root_password"
- "MYSQL_DATABASE=wordpress"
- "MYSQL_USER=wordpress"
- "MYSQL_PASSWORD=wordpress_password"
expose:
- port: 3306
to:
- service: wordpress
params:
storage:
db-data:
mount: /var/lib/mysql
redis:
image: redis:7-alpine
expose:
- port: 6379
to:
- service: wordpress
profiles:
compute:
wordpress:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
- name: default
size: 1Gi
- name: wordpress-data
size: 10Gi
attributes:
persistent: true
class: beta2
db:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
- name: default
size: 1Gi
- name: db-data
size: 20Gi
attributes:
persistent: true
class: beta2
redis:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
wordpress:
denom: uact
amount: 2000
db:
denom: uact
amount: 3000
redis:
denom: uact
amount: 1000
deployment:
wordpress:
dcloud:
profile: wordpress
count: 1
db:
dcloud:
profile: db
count: 1
redis:
dcloud:
profile: redis
count: 1
```
### PostgreSQL Variant
```yaml
version: "2.0"
services:
app:
image: myapp:v1.0.0
env:
- "DATABASE_URL=postgres://appuser:apppassword@db:5432/appdb"
expose:
- port: 3000
as: 80
to:
- global: true
db:
image: postgres:15
env:
- "POSTGRES_USER=appuser"
- "POSTGRES_PASSWORD=apppassword"
- "POSTGRES_DB=appdb"
expose:
- port: 5432
to:
- service: app
params:
storage:
pgdata:
mount: /var/lib/postgresql/data
profiles:
compute:
app:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 2Gi
db:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
- name: default
size: 1Gi
- name: pgdata
size: 50Gi
attributes:
persistent: true
class: beta2
placement:
dcloud:
pricing:
app:
denom: uact
amount: 2000
db:
denom: uact
amount: 4000
deployment:
app:
dcloud:
profile: app
count: 1
db:
dcloud:
profile: db
count: 1
```
## Best Practices
1. **Never expose databases globally**: Use `to: - service: <name>` pattern
2. **Use strong passwords**: Even in examples, use secure password patterns
3. **Size storage appropriately**: Databases grow; allocate sufficient space
4. **Use SSD storage**: `class: beta2` for database workloads
5. **Separate compute profiles**: Different resources for app vs database tiers
rules/sdl/placement-pricing.md
# Placement and Pricing Configuration
The `profiles.placement` section defines where deployments run and their pricing.
## Basic Structure
```yaml
profiles:
placement:
<placement-name>:
pricing:
<profile-name>:
denom: uact
amount: 1000
```
## Placement Properties
### pricing (required)
Defines the maximum price you're willing to pay per block for each compute profile:
```yaml
profiles:
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
api:
denom: uact
amount: 2000
```
### attributes (optional)
Filter providers by attributes:
```yaml
profiles:
placement:
dcloud:
attributes:
region: us-west
host: akash
pricing:
web:
denom: uact
amount: 1000
```
Common attributes:
- `region` - Geographic region
- `host` - Hosting provider name
- `tier` - Service tier level
### signedBy (optional)
Require providers to be audited:
```yaml
profiles:
placement:
dcloud:
signedBy:
anyOf:
- akash1365yvmc4s7awdyj3n2sav7xfx76adc6dnmlx63
- akash18qa2a2ltfyvkyj0ggj3hkvuj6twzyumuaru9s4
pricing:
web:
denom: uact
amount: 1000
```
#### signedBy Options
| Field | Description |
|-------|-------------|
| `anyOf` | Provider signed by at least one of these auditors |
| `allOf` | Provider must be signed by all of these auditors |
```yaml
signedBy:
allOf:
- akash1auditor1...
- akash1auditor2...
anyOf:
- akash1auditor3...
```
## Payment Denominations
### uact (ACT — the deployment-payment token)
ACT is Akash's deployment-payment token (distinct from AKT, the chain token used for gas/staking). `uact` is its micro-denomination — `1 ACT = 1,000,000 uact`. `uact` is the SDL pricing, deposit, and lease-payment denom; `uakt` is only accepted via a rare burn-mint (BME) fallback.
```yaml
pricing:
web:
denom: uact
amount: 1000 # 0.001 ACT per block
```
### Price Calculation
- Prices are set per block, and blocks are produced at a roughly fixed cadence, so the `amount` you set translates into an ongoing per-block spend that accumulates over the lease.
- The block-based `amount` is a bid ceiling, not the final cost — the actual lease price is whatever a provider bids at or below it.
- Any monthly estimate derived from the per-block `amount` is illustrative only — rely on live price discovery (the bids you actually receive) and compare against real pricing in the [awesome-akash](https://github.com/akash-network/awesome-akash) example templates rather than a fixed multiplier.
## Multiple Placements
You can define multiple placement options:
```yaml
profiles:
placement:
us-east:
attributes:
region: us-east
signedBy:
anyOf:
- akash1auditor...
pricing:
web:
denom: uact
amount: 1000
eu-west:
attributes:
region: eu-west
pricing:
web:
denom: uact
amount: 1200
```
## Complete Example
```yaml
profiles:
compute:
web:
resources:
cpu:
units: 1
memory:
size: 1Gi
storage:
size: 5Gi
api:
resources:
cpu:
units: 2
memory:
size: 2Gi
storage:
size: 10Gi
placement:
primary:
attributes:
region: us-west
signedBy:
anyOf:
- akash1365yvmc4s7awdyj3n2sav7xfx76adc6dnmlx63
pricing:
web:
denom: uact
amount: 1000
api:
denom: uact
amount: 2000
backup:
attributes:
region: eu-central
pricing:
web:
denom: uact
amount: 1200
api:
denom: uact
amount: 2200
```
## Pricing Guidelines
The ranges below are rough, illustrative starting points for the per-block `amount` (in `uact`) — not exact prices. Treat them as a place to begin, then let live bids and current market conditions guide the real value.
| Workload Type | Illustrative uact/block | Notes |
|---------------|---------------------|-------|
| Small web app | 500-1000 | 0.5 CPU, 512Mi RAM |
| API server | 1000-2000 | 1-2 CPU, 1-2Gi RAM |
| Database | 2000-5000 | Depends on storage |
| GPU workload | 10000-50000+ | Varies by GPU model |
Higher pricing increases the likelihood of provider acceptance. Start within these illustrative ranges and adjust based on the bids you receive and current market conditions.
rules/sdl/reclamation.md
# SDL Resource Reclamation (`reclamation`)
The optional top-level `reclamation` block lets a tenant declare the **minimum grace
window** a provider must honor before it may reclaim the lease's resources. This is
the SDL surface of AEP-82 Resource Reclamation.
Requires SDL version `"2.1"`. SDL `"2.0"` continues to work unchanged — reclamation
is **opt-in**; omit the block and leases behave exactly as before.
## The block
```yaml
version: "2.1"
reclamation:
min_window: "24h"
```
| Field | Type | Required | Meaning |
|-------|------|----------|---------|
| `min_window` | Go duration string (e.g. `"1h"`, `"24h"`, `"720h"`) | Yes, when the block is present | Minimum time a provider must wait after starting reclamation before it may close the lease. Must be positive. |
The value maps to `DeploymentReclamation` (field 5 on `MsgCreateDeployment`). When the
block is omitted the field is nil and the deployment carries no reclamation
requirement.
## Governance bounds
`min_window` must fall within the market's governance parameters:
- **Minimum:** `1h` (param `min_reclamation_window`, default 1h)
- **Maximum:** `720h` / 30 days (param `max_reclamation_window`, default 720h)
A `min_window` outside `[1h, 720h]` is rejected at validation time. See
[validation-rules.md](validation-rules.md).
## When to use
Set a `reclamation` requirement when your workload needs guaranteed lead time to
react before a provider can reclaim its resources — for example to drain
connections, snapshot state, or migrate to another lease. Providers that cannot
honor a window at least as long as your `min_window` will not bid.
## What happens at runtime
Declaring `min_window` does not change normal operation. It only constrains
provider-initiated reclamation: once a provider starts reclamation, it cannot close
the lease until the window elapses. See the full lifecycle —
[deployment-lifecycle.md](../deploy/cli/deployment-lifecycle.md) — for the
`Active → Reclaiming → paused group` flow and recovery via `MsgStartGroup`.
## Client requirements
Resource reclamation requires the v2.1.0-compatible toolchain:
- **Node:** v2.1.0 (mainnet upgrade)
- **Provider:** provider-services v0.13.0 or later (the build paired with node v2.1.0)
- **chain-sdk:** `@akashnetwork/chain-sdk@alpha` (TS) / `pkg.akt.dev/go` (Go)
Pre-2.1 nodes do not understand the field; detect support via the discovery endpoint
(`node_version >= 2.1.0`) before submitting — see
[sdk/overview.md](../sdk/overview.md).
rules/sdl/schema-overview.md
# SDL Schema Overview
## Version Requirement
The SDL version must be specified as the first field:
```yaml
version: "2.0"
```
Use version `"2.1"` when you need IP endpoints **or** the `reclamation` block (resource reclamation, see below):
```yaml
version: "2.1"
```
SDL `"2.0"` continues to work unchanged — version `"2.1"` is only required when you use one of the 2.1-gated sections.
## Required Sections
Every valid SDL must contain these four sections:
| Section | Purpose |
|---------|---------|
| `version` | SDL version string |
| `services` | Container and workload definitions |
| `profiles` | Compute resources and placement configuration |
| `deployment` | Maps services to placement profiles |
## Optional Sections
| Section | Purpose | Requires Version |
|---------|---------|------------------|
| `endpoints` | IP lease endpoint declarations | 2.1 |
| `reclamation` | Minimum grace window a provider must honor before reclaiming the lease's resources (AEP-82). See [reclamation.md](reclamation.md). | 2.1 |
## Basic Structure
```yaml
version: "2.0"
services:
<service-name>:
image: <container-image>
expose:
- port: <port>
to:
- global: true
profiles:
compute:
<profile-name>:
resources:
cpu:
units: <cpu-units>
memory:
size: <memory-size>
storage:
size: <storage-size>
placement:
<placement-name>:
pricing:
<profile-name>:
denom: uact
amount: <price>
deployment:
<service-name>:
<placement-name>:
profile: <profile-name>
count: <instance-count>
```
## Naming Rules
- Service names, profile names, and placement names must be valid identifiers
- Use lowercase letters, numbers, and hyphens
- Names must start with a letter
- Names are case-sensitive
## Section Dependencies
1. **services** - Defines what containers to run
2. **profiles.compute** - Defines resource requirements for each service
3. **profiles.placement** - Defines where to deploy and pricing per compute profile
4. **deployment** - Links services to placements using compute profiles
All service names referenced in `deployment` must exist in `services`.
All profile names referenced in `deployment` must exist in `profiles.compute`.
All placement names used in `deployment` must exist in `profiles.placement`.
rules/sdl/services.md
# Services Configuration
The `services` section defines the containers and workloads to deploy.
## Basic Service Definition
```yaml
services:
<service-name>:
image: <container-image>
expose:
- port: <port>
to:
- global: true
```
## Service Properties
### image (required)
The container image to deploy. **Must include an explicit tag** - the `latest` tag is not allowed.
```yaml
services:
web:
image: nginx:1.25.3 # Explicit version tag
app:
image: node:18-alpine # Version with variant
api:
image: myapp:v1.2.0 # Semantic version
```
**Important:** Do not use `:latest` or omit the tag. Akash requires explicit tags for reproducible deployments.
### command (optional)
Override the container's default command:
```yaml
services:
app:
image: node:18
command:
- "node"
- "server.js"
```
### args (optional)
Arguments passed to the command:
```yaml
services:
app:
image: myapp:v1.0.0
args:
- "--config"
- "/etc/config.yaml"
```
### env (optional)
Environment variables as an array of strings:
```yaml
services:
app:
image: myapp:v1.0.0
env:
- "NODE_ENV=production"
- "DATABASE_URL=postgres://user:pass@db:5432/mydb"
- "SECRET_KEY=mysecretkey"
```
### expose (required for accessible services)
Port exposure configuration:
```yaml
services:
web:
image: nginx:1.25.3
expose:
- port: 80 # Container port (1-65535)
as: 80 # External port (optional, defaults to port)
proto: tcp # Protocol: tcp (default) or udp
to:
- global: true # Expose to internet
```
#### Expose Options
| Field | Type | Description |
|-------|------|-------------|
| `port` | integer | Container port (1-65535, required) |
| `as` | integer | External port (1-65535, optional) |
| `proto` | string | Protocol: `tcp` (default) or `udp` |
| `to` | array | Exposure targets |
#### Exposure Targets
```yaml
expose:
- port: 80
to:
- global: true # Internet accessible
- port: 5432
to:
- service: web # Only accessible by 'web' service
- port: 8080
to:
- ip: myendpoint # Expose via IP endpoint (requires v2.1)
```
#### HTTP Options
For HTTP services, additional options are available:
```yaml
expose:
- port: 80
as: 80
to:
- global: true
http_options:
max_body_size: 1048576 # Max request body (bytes, 0-104857600)
read_timeout: 60000 # Read timeout (ms, or a duration string such as "60s")
send_timeout: 60000 # Send timeout (ms, or a duration string such as "60s")
next_tries: 3 # Retry attempts
next_timeout: 0 # Retry timeout (ms)
next_cases: # Retry conditions (all values must be strings)
- error
- timeout
- "500" # HTTP status codes must be quoted
- "502"
- "503"
```
##### Proxy Tuning
`http_options.proxy` tunes the provider's nginx reverse proxy for one route. Every field is optional, and so is the block itself:
```yaml
expose:
- port: 80
as: 80
to:
- global: true
http_options:
proxy:
buffering_disable: true # Stream the response instead of buffering it
buffer_size: 12288 # Bytes for the first part of the response
buffers_number: 6 # Buffer count; must be set with buffers_size
buffers_size: 24576 # Bytes per buffer; must be set with buffers_number
busy_buffers_size: 49152 # Bytes that may be busy sending to the client
connect_timeout: 7000 # Connect timeout in milliseconds
```
| Field | Type | nginx directive |
|-------|------|-----------------|
| `buffering_disable` | boolean | `proxy_buffering off` when `true` |
| `buffer_size` | integer (bytes) | `proxy_buffer_size` |
| `buffers_number` | integer | count half of `proxy_buffers`; requires `buffers_size` |
| `buffers_size` | integer (bytes) | size half of `proxy_buffers`; requires `buffers_number` |
| `busy_buffers_size` | integer (bytes) | `proxy_busy_buffers_size` |
| `connect_timeout` | integer (ms) | `proxy_connect_timeout` |
Set `buffering_disable: true` on a route that streams, such as server-sent events or a long poll, where buffering holds the payload back until the response finishes. Raise `buffer_size` when clients get a `502` on responses with large headers, usually an oversized `Set-Cookie` or a token echoed back in a header.
Requires chain-sdk `1.0.0-alpha.44` or newer. Earlier parsers reject the `proxy` key outright. The provider gateway support that turns these into nginx directives is not merged yet ([provider#430](https://github.com/akash-network/provider/pull/430)), so the block validates and reaches the manifest but no provider acts on it. Do not offer proxy tuning to a user as a fix that works today.
### credentials (optional)
For private container registries:
```yaml
services:
app:
image: registry.example.com/myapp:v1.0.0
credentials:
host: registry.example.com
username: myuser
password: mypassword
```
### params (optional)
Service parameters — storage mounts and Confidential Compute (TEE).
Storage mount configuration:
```yaml
services:
db:
image: postgres:15
params:
storage:
data:
mount: /var/lib/postgresql/data
readOnly: false
```
The storage name (`data` in the example) must match a named storage volume in the compute profile.
#### tee (Confidential Compute)
Request a Confidential Compute (TEE) workload with `params.tee`, a plain string set to `cpu` (CPU-only) or `cpu-gpu` (CPU + GPU):
```yaml
services:
web:
image: nginx:1.25.3
params:
tee: cpu # or: cpu-gpu (requires GPU resources in the compute profile)
```
The provider selects the TEE hardware and injects attestation automatically. See [confidential-compute.md](confidential-compute.md) for the full reference.
### dependencies (optional)
Declare service startup dependencies. The service will wait for dependent services to be ready before starting:
```yaml
services:
api:
image: myapp:v1.0.0
dependencies:
- service: database
- service: cache
expose:
- port: 3000
to:
- global: true
database:
image: postgres:15
expose:
- port: 5432
to:
- service: api
cache:
image: redis:7
expose:
- port: 6379
to:
- service: api
```
**Note:** Dependencies control startup order. For network access, you must also expose ports using the `to: - service: <name>` directive.
## Service-to-Service Communication
Services can communicate using their service names as hostnames:
```yaml
services:
web:
image: nginx:1.25.3
env:
- "API_URL=http://api:3000"
expose:
- port: 80
to:
- global: true
api:
image: myapi:v1.0.0
expose:
- port: 3000
to:
- service: web # Only web can access
```
## Complete Example
```yaml
services:
frontend:
image: myapp/frontend:v1.0
env:
- "API_URL=http://backend:8080"
expose:
- port: 3000
as: 80
to:
- global: true
http_options:
max_body_size: 5242880
backend:
image: myapp/backend:v1.0
command:
- "node"
- "server.js"
env:
- "DATABASE_URL=postgres://postgres:password@db:5432/myapp"
- "NODE_ENV=production"
expose:
- port: 8080
to:
- service: frontend
db:
image: postgres:15
env:
- "POSTGRES_PASSWORD=password"
- "POSTGRES_DB=myapp"
expose:
- port: 5432
to:
- service: backend
params:
storage:
pgdata:
mount: /var/lib/postgresql/data
```
rules/sdl/validation-rules.md
# SDL Validation Rules
This document lists all constraints and validation rules for Akash SDL configurations.
## Image Tag Constraints
**Docker images must specify an explicit tag.** The `latest` tag is not allowed.
```yaml
# Valid - explicit version tag
services:
web:
image: nginx:1.25.3
# Valid - semantic version
services:
app:
image: node:18-alpine
# Valid - SHA digest
services:
api:
image: myapp@sha256:abc123...
# Invalid - latest tag
services:
web:
image: nginx:latest # Error: 'latest' tag not allowed
# Invalid - no tag (implies latest)
services:
web:
image: nginx # Error: must specify explicit tag
```
**Why?** The `latest` tag is mutable and can change unexpectedly, causing deployment inconsistencies. Explicit tags ensure reproducible deployments.
## Port Constraints
| Field | Valid Range | Notes |
|-------|-------------|-------|
| `port` | 1-65535 | Container port |
| `as` | 1-65535 | External port (optional) |
```yaml
# Valid
expose:
- port: 80
as: 8080
# Invalid - port out of range
expose:
- port: 0 # Error: must be >= 1
- port: 70000 # Error: must be <= 65535
```
## HTTP Options Constraints
| Field | Valid Range | Default |
|-------|-------------|---------|
| `max_body_size` | 0-104857600 (100MB) | 1048576 (1MB) |
| `read_timeout` | 0-4294967295 ms, or a duration string | 60000 |
| `send_timeout` | 0-4294967295 ms, or a duration string | 60000 |
| `next_tries` | Not range-validated | 3 |
| `next_timeout` | Not range-validated | 0 |
| `next_cases` | Array of strings | `["error", "timeout"]` |
| `proxy` | Optional object, see below | Unset |
> `read_timeout` and `send_timeout` take either a plain integer of milliseconds or a duration string ending in `ms`, `s`, `m` or `h`. Either form normalises to a uint32 millisecond count, so 4294967295 ms is the ceiling. `next_timeout` and `next_tries` are not range-validated.
```yaml
# Valid
http_options:
max_body_size: 52428800 # 50MB
read_timeout: 30000 # 30 seconds as a millisecond count
send_timeout: 2m # the same value can be written as a duration string
next_cases:
- error
- timeout
- "502" # HTTP status codes must be quoted strings
- "503"
- "504"
# Invalid
http_options:
max_body_size: 200000000 # Error: exceeds 100MB limit
read_timeout: 30sec # Error: the unit must be ms, s, m or h
next_cases:
- 502 # Error: numbers must be quoted as strings
- 503
```
### Proxy Options
`http_options.proxy` is an optional object with a closed key set:
| Field | Valid Range |
|-------|-------------|
| `buffering_disable` | boolean |
| `buffer_size` | 0-4294967295 (bytes) |
| `buffers_number` | 1-4294967295 |
| `buffers_size` | 1-4294967295 (bytes) |
| `busy_buffers_size` | 0-4294967295 (bytes) |
| `connect_timeout` | 0-4294967295 (ms) |
```yaml
# Valid - every field set
http_options:
proxy:
buffering_disable: true
buffer_size: 4096
buffers_number: 8
buffers_size: 4096
busy_buffers_size: 8192
connect_timeout: 5000
# Valid - one field is enough, the rest stay unset
http_options:
proxy:
buffering_disable: true
# Invalid - the buffers are an nginx pair
http_options:
proxy:
buffers_number: 8 # Error: buffers_number and buffers_size must be set together
# Invalid - the pair does not accept 0
http_options:
proxy:
buffers_number: 0 # Error: minimum is 1
buffers_size: 4096
# Invalid - unknown key
http_options:
proxy:
buffering: false # Error: the field is buffering_disable, and false is its default
```
### Unset Proxy Blocks Are Dropped
A `proxy: {}` block, or one whose fields are all zero with `buffering_disable: false`, counts as unset. It is dropped from the manifest, so the deployment version hash matches a manifest written with no proxy options. The provider gateway applies its own defaults when it serves the route, and clients must not write those defaults into the SDL to make them explicit.
## Pricing Constraints
| Field | Valid Values |
|-------|--------------|
| `denom` | `uact` (the SDL pricing denom) |
| `amount` | Positive integer |
```yaml
# Valid - ACT (deployment-payment token)
pricing:
web:
denom: uact
amount: 1000
# Invalid
pricing:
web:
denom: usdc # Error: must be 'uact'
amount: -100 # Error: must be positive
```
## Storage Constraints
### RAM Storage
RAM-based storage cannot be persistent:
```yaml
# Valid
storage:
- name: cache
size: 1Gi
attributes:
class: ram
persistent: false
# Invalid
storage:
- name: cache
size: 1Gi
attributes:
class: ram
persistent: true # Error: RAM cannot be persistent
```
### Non-RAM Storage with Attributes
Non-RAM storage classes with attributes must be persistent:
```yaml
# Valid
storage:
- name: data
size: 10Gi
attributes:
class: beta2
persistent: true
# Invalid
storage:
- name: data
size: 10Gi
attributes:
class: beta2
persistent: false # Error: non-RAM with class must be persistent
```
### Simple Storage (No Constraints)
Storage without attributes has no special constraints:
```yaml
# Valid - ephemeral storage
storage:
size: 5Gi
# Valid - named ephemeral
storage:
- name: default
size: 5Gi
```
## CPU Constraints
### Architecture Values
`cpu.attributes.arch` accepts exactly `amd64` or `arm64`:
```yaml
# Valid - arm64
cpu:
units: 1
attributes:
arch: arm64
# Valid - amd64
cpu:
units: 1
attributes:
arch: amd64
# Valid - no architecture requested (writes no CPU attributes)
cpu:
units: 1
# Invalid - uname and Go spellings are not accepted
cpu:
units: 1
attributes:
arch: x86_64 # Error: unsupported cpu architecture "x86_64"
cpu:
units: 1
attributes:
arch: aarch64 # Error: unsupported cpu architecture "aarch64"
```
### Only `arch` Is Recognised
```yaml
# Invalid - unknown CPU attribute key
cpu:
units: 1
attributes:
vendor: amd # Error: unsupported cpu attribute "vendor"
```
### No Default
Omitting `attributes` is not shorthand for `arch: amd64`. The group spec gets zero CPU attributes, which is what every deployment created before `arch` existed has. SDL builders and SDK wrappers must not fill one in.
## GPU Constraints
### GPU with units > 0
Must have attributes with vendor specified:
```yaml
# Valid
gpu:
units: 1
attributes:
vendor:
nvidia:
# Valid - with model
gpu:
units: 2
attributes:
vendor:
nvidia:
- model: a100
# Invalid - missing attributes
gpu:
units: 1 # Error: must have attributes with vendor
# Invalid - missing vendor
gpu:
units: 1
attributes: {} # Error: must specify vendor
```
### GPU with units = 0
Cannot have attributes:
```yaml
# Valid
gpu:
units: 0
# Invalid
gpu:
units: 0
attributes: # Error: cannot have attributes when units=0
vendor:
nvidia:
```
## Confidential Compute (TEE) Constraints
The `services.<name>.params.tee` field (AEP-83) has three constraints.
### Allowed Values
`tee` must be `cpu` or `cpu-gpu`:
```yaml
# Valid - CPU-only confidential compute
services:
web:
params:
tee: cpu
# Valid - CPU + GPU confidential compute
services:
inference:
params:
tee: cpu-gpu
# Invalid - unknown value
services:
web:
params:
tee: sev # Error: must be 'cpu' or 'cpu-gpu'
```
### cpu-gpu Requires GPU Resources
`tee: cpu-gpu` requires the compute profile to declare GPU resources:
```yaml
# Valid - cpu-gpu with a GPU in the profile
services:
inference:
params:
tee: cpu-gpu
profiles:
compute:
inference:
resources:
gpu:
units: 1
attributes:
vendor:
nvidia:
# Invalid - cpu-gpu without GPU resources
services:
inference:
params:
tee: cpu-gpu # Error: cpu-gpu requires GPU resources in the profile
profiles:
compute:
inference:
resources:
cpu:
units: 4
```
### Same Value Per Deployment Group
All services in a deployment group must use the same `tee` value, or none:
```yaml
# Valid - both services request the same TEE value
services:
web:
params:
tee: cpu
api:
params:
tee: cpu
# Invalid - mixed TEE values in one group
services:
web:
params:
tee: cpu
api:
params:
tee: cpu-gpu # Error: all services in a group must use the same value or none
```
See [confidential-compute.md](confidential-compute.md) for the full field reference.
## Endpoint Constraints
### Must Be Global
IP endpoints implicitly expose services globally:
```yaml
# Valid
expose:
- port: 80
to:
- ip: myendpoint
# The endpoint exposure is global by nature
```
### Must Be Used
Declared endpoints must be referenced:
```yaml
# Invalid - unused endpoint
endpoints:
myip:
kind: ip # Error: endpoint declared but not used
services:
web:
expose:
- port: 80
to:
- global: true # Does not reference 'myip'
```
### Version Requirement
IP endpoints require version 2.1:
```yaml
# Valid
version: "2.1"
endpoints:
myip:
kind: ip
# Invalid
version: "2.0" # Error: endpoints require version 2.1
endpoints:
myip:
kind: ip
```
## Reclamation Constraints
The optional `reclamation` block (AEP-82) requires version 2.1 and a valid window.
### Window Bounds
`min_window` must be a positive Go duration within the governance bounds
`[min_reclamation_window, max_reclamation_window]` — defaults **1h–720h**:
```yaml
# Valid
version: "2.1"
reclamation:
min_window: "24h"
# Invalid - below minimum
reclamation:
min_window: "10m" # Error: below min_reclamation_window (1h)
# Invalid - above maximum
reclamation:
min_window: "1000h" # Error: above max_reclamation_window (720h)
# Invalid - not a positive duration
reclamation:
min_window: "0s" # Error: must be positive
```
### Version Requirement
The `reclamation` block requires version 2.1:
```yaml
# Invalid
version: "2.0" # Error: reclamation requires version 2.1
reclamation:
min_window: "24h"
```
See [reclamation.md](reclamation.md) for the full field reference.
## Reference Constraints
### Service Names
Services in `deployment` must exist in `services`:
```yaml
services:
web:
image: nginx:1.25.3
deployment:
web: # Valid: matches 'web' in services
dcloud:
profile: web
count: 1
api: # Error: 'api' not defined in services
dcloud:
profile: api
count: 1
```
### Dependencies
Service dependencies must reference existing services:
```yaml
services:
api:
image: myapp:v1.0.0
dependencies:
- service: database # Valid: 'database' exists
- service: cache # Error if 'cache' not defined
database:
image: postgres:15
```
Circular dependencies should be avoided to prevent startup deadlocks.
### Profile Names
Profiles in `deployment` must exist in `profiles.compute`:
```yaml
profiles:
compute:
web:
resources:
cpu:
units: 1
deployment:
web:
dcloud:
profile: web # Valid: matches 'web' in profiles.compute
count: 1
api:
dcloud:
profile: api # Error: 'api' not in profiles.compute
count: 1
```
### Placement Names
Placements in `deployment` must exist in `profiles.placement`:
```yaml
profiles:
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud: # Valid: matches 'dcloud' in profiles.placement
profile: web
count: 1
other: # Error: 'other' not in profiles.placement
profile: web
count: 1
```
### Pricing Coverage
Placements must have pricing for all profiles used:
```yaml
profiles:
compute:
web:
resources: ...
api:
resources: ...
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
# Missing: api pricing
deployment:
web:
dcloud:
profile: web
count: 1
api:
dcloud:
profile: api # Error: no pricing for 'api' in 'dcloud'
count: 1
```
## Deployment Constraints
| Field | Constraint |
|-------|------------|
| `count` | Must be >= 1 |
```yaml
# Valid
deployment:
web:
dcloud:
profile: web
count: 1
# Invalid
deployment:
web:
dcloud:
profile: web
count: 0 # Error: count must be at least 1
```
## Summary Table
| Constraint | Rule |
|------------|------|
| Image tag | Must be explicit, `latest` not allowed |
| Port range | 1-65535 |
| Timeout range | 0-60000ms |
| Body size | 0-104857600 bytes (100MB) |
| next_cases | All values must be strings (quote HTTP status codes) |
| Price denom | `uact` (the SDL pricing denom) |
| Price amount | Positive integer |
| RAM storage | Cannot be persistent |
| Non-RAM + attributes | Requires persistent=true |
| CPU `arch` value | `amd64` or `arm64` exactly; `x86_64`, `aarch64` rejected |
| CPU attributes | `arch` is the only recognised key |
| CPU `arch` omitted | Writes no CPU attributes; no default |
| `http_options` timeouts | Milliseconds or a duration string in `ms`, `s`, `m`, `h`; ceiling 4294967295 ms |
| `http_options.proxy` keys | Closed set: `buffering_disable`, `buffer_size`, `buffers_number`, `buffers_size`, `busy_buffers_size`, `connect_timeout` |
| Proxy buffers pair | `buffers_number` and `buffers_size` set together, both >= 1 |
| Unset proxy block | Dropped from the manifest; no defaults injected |
| GPU with units > 0 | Must have attributes with vendor |
| GPU with units = 0 | Cannot have attributes |
| TEE value | Must be `cpu` or `cpu-gpu` |
| TEE `cpu-gpu` | Requires GPU resources in the profile |
| TEE per group | All services same value or none |
| Endpoints | Must be global, must be used, requires v2.1 |
| Reclamation `min_window` | Positive Go duration within 1h–720h, requires v2.1 |
| Deployment count | Minimum 1 |
| Dependencies | Must reference existing services |
| References | All names must match across sections |
rules/terminology.md
# Akash Network Terminology
Key terms and concepts used throughout the Akash Network.
## Deployment Identifiers
### DSEQ (Deployment Sequence)
A unique identifier for a deployment on the Akash blockchain.
```
dseq: 12345678
```
- Assigned when deployment is created on-chain
- Used to reference the deployment in all operations
- Immutable once assigned
### GSEQ (Group Sequence)
Identifies a group within a deployment. Most deployments have a single group (gseq: 1).
```
gseq: 1
```
- Groups allow deploying to multiple providers
- Each group can have different placement requirements
- Starts at 1 for each deployment
### OSEQ (Order Sequence)
Identifies an order within a group. Increments when a lease is closed and reopened.
```
oseq: 1
```
- Starts at 1 for new deployments
- Increments on lease migration or restart
- Used with dseq and gseq to identify a specific lease
### Lease ID
The combination of owner address, dseq, gseq, oseq, and provider address.
```
owner/dseq/gseq/oseq/provider
akash1abc.../12345678/1/1/akash1xyz...
```
## Core Concepts
### SDL (Stack Definition Language)
YAML configuration file defining a deployment:
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
### Manifest
The SDL converted to a format sent to the provider. Contains deployment instructions without pricing information.
### Deployment
An on-chain record representing a tenant's intent to deploy workloads. Contains:
- Owner address
- SDL hash
- Escrow balance
- State (open, closed)
### Order
A request for bids on a deployment group. Providers monitor for orders matching their capabilities.
### Bid
A provider's offer to fulfill an order at a specific price.
### Lease
A binding agreement between tenant and provider:
- Created when bid is accepted
- Defines price per block
- Allows provider to deploy workload
- Continues until closed or funds depleted
## Actors
### Tenant (Deployer)
The user deploying workloads:
- Creates deployments
- Deposits escrow
- Accepts bids
- Manages leases
### Provider
Entity running compute infrastructure:
- Operates Kubernetes cluster
- Runs provider software
- Submits bids on orders
- Deploys and manages workloads
### Auditor
Trusted entity that verifies provider attributes:
- Signs provider attestations
- Enables attribute filtering
- Builds trust in provider claims
### Validator
Secures the Akash blockchain:
- Runs consensus node
- Validates transactions
- Earns staking rewards
## Payment Terms
### Two tokens — AKT vs ACT
Akash has two separate tokens. Don't conflate them.
| Token | Denom | Purpose |
|---|---|---|
| **AKT** | `uakt` | Chain token — gas, staking, validator rewards. `1 AKT = 1,000,000 uakt`. |
| **ACT** | `uact` | Deployment-payment token — SDL pricing, bid *prices*, deployment escrow/deposit, lease payments. `1 ACT = 1,000,000 uact`. The provider-side bid *deposit* (`MsgCreateBid.deposit`, the anti-spam collateral) defaults to `uakt` — the provider posts 500000 uakt (0.5 AKT) from its balance; `uact` only via a rare burn-mint fallback. |
Self-custody deployers must mint ACT by burning AKT before depositing to a deployment. Console API users skip this — Console funds the managed wallet with ACT directly.
### uact
Micro-ACT, the smallest denomination of the deployment-payment token:
```
1 ACT = 1,000,000 uact
```
### Escrow
Funds deposited for a deployment:
- Required before deployment starts
- Drawn down per block
- Returned on close (minus spent)
### Bid Price
Amount provider charges per block:
```yaml
pricing:
web:
denom: uact
amount: 1000 # per block (~6 seconds)
```
Monthly cost is roughly `amount × blocks_per_month` (a block is ~6 seconds, so on the order of hundreds of thousands of blocks per month). These figures are illustrative only — do not treat them as a quote. For real costs, rely on live price discovery (the bids returned for your deployment) and the pricing in current awesome-akash templates.
## Provider Terms
### Attributes
Key-value pairs describing provider capabilities:
```yaml
attributes:
region: us-west
host: akash
tier: community
```
### Signed By
Auditor signatures that verify provider attributes:
```yaml
signedBy:
anyOf:
- akash1auditor...
```
### Bid Engine
Provider component that:
- Monitors for matching orders
- Calculates bid prices
- Submits bids automatically
## Infrastructure Terms
### Kubernetes
Container orchestration platform used by providers:
- Runs workloads as pods
- Manages networking and storage
- Provides service discovery
### Ingress
Kubernetes component exposing services:
- Routes external traffic
- Handles TLS termination
- Provides hostnames for deployments
### Persistent Volume
Storage that survives container restarts:
- Mounted to containers
- Backed by provider storage class
- beta2, beta3, or ram classes
## State Machine
### Deployment States
```
Active: Deployment is running
Closed: Deployment is terminated
```
### Order States
```
Open: Accepting bids
Matched: Bid accepted, lease created
Closed: No longer accepting bids
```
### Lease States
```
Active: Workload running
Reclaiming: Provider has started resource reclamation (AEP-82); deadline = block_time + reclamation window
Closed: Lease terminated
Insufficient Funds: Escrow depleted
```
`Reclaiming` and the paused-group state below only appear on v2.1.0+ leases; pre-2.1
leases never enter them.
### Group States
```
Open: Group active, orders may be created
Paused: Group has no active lease and will not auto-create new orders (e.g. after a reclamation window expires); resume with MsgStartGroup
```
## Resource Reclamation (AEP-82)
**Resource Reclamation** is the v2.1.0 protocol mechanism by which a **provider**
reclaims the resources of an *active* lease after a tenant-declared grace window. It
is distinct from the provider-side Kubernetes cleanup that runs *after* a lease
closes (see the akash-provider skill's "Resource Cleanup on Lease Close").
| Term | Meaning |
|------|---------|
| `MsgLeaseStartReclaim` | Provider-signed message that starts reclamation on an `Active` lease, moving it to `Reclaiming`. Carries `{ id: LeaseID, reason: LeaseClosedReason }`. |
| Reclamation deadline | `block_time + reclamation window`. The provider may close the lease **only after** this deadline; closing before it is rejected. |
| `MsgStartGroup` | Resumes a **paused** group, creating new orders (which inherit the deployment's reclamation requirement). |
| `MsgCloseLease` | Closes a **single lease**. Tenant-initiated close auto-re-orders (the new order inherits the reclamation requirement). |
| `MsgCloseDeployment` | Closes the **whole deployment**; this escrow-level close bypasses the reclamation window. |
See [deploy/cli/deployment-lifecycle.md](deploy/cli/deployment-lifecycle.md) for the
full flow and [sdl/reclamation.md](sdl/reclamation.md) for the SDL field.
## Common Abbreviations
| Abbreviation | Meaning |
|--------------|---------|
| SDL | Stack Definition Language |
| AKT | Akash Token |
| IBC | Inter-Blockchain Communication |
| RPC | Remote Procedure Call |
| gRPC | Google Remote Procedure Call |
| API | Application Programming Interface |
| CLI | Command Line Interface |
| JWT | JSON Web Token |
| mTLS | Mutual TLS |
scripts/match_providers.py
#!/usr/bin/env python3
"""
Match an Akash SDL against the live provider set.
Fetches providers from https://console-api.akash.network/v1/providers,
filters for online + audited, then checks how many can satisfy each profile
in the SDL. Outputs a JSON report with a filter funnel, matching providers,
and the single biggest constraint.
Usage:
python match_providers.py <sdl.yaml> [--json] [--top N] [--api URL]
--api also accepts file:///abs/path/providers.json to run against a saved response.
"""
from __future__ import annotations
import argparse
import fnmatch
import json
import re
import sys
import urllib.request
from dataclasses import dataclass, field
from typing import Any
try:
import yaml
except ImportError:
sys.stderr.write("Missing dependency: PyYAML. Install with: pip install pyyaml\n")
sys.exit(2)
DEFAULT_API = "https://console-api.akash.network/v1/providers"
# ---------- Unit parsing ----------
_CPU_SUFFIX = {"m": 1} # millicores
_MEM_SUFFIX_BIN = {"Ki": 1024, "Mi": 1024**2, "Gi": 1024**3, "Ti": 1024**4, "Pi": 1024**5}
_MEM_SUFFIX_DEC = {"k": 1000, "M": 1000**2, "G": 1000**3, "T": 1000**4, "P": 1000**5}
def parse_cpu_millis(v: Any) -> int:
"""SDL cpu.units accepts 0.5, 2, '500m'. Returns millicores."""
if v is None:
return 0
if isinstance(v, (int, float)):
return int(round(float(v) * 1000))
s = str(v).strip()
if s.endswith("m"):
return int(s[:-1])
return int(round(float(s) * 1000))
def parse_bytes(v: Any) -> int:
"""Parse memory/storage size like '512Mi', '2Gi', '100M', or a raw integer of bytes."""
if v is None:
return 0
if isinstance(v, (int, float)):
return int(v)
s = str(v).strip()
for suf, mult in {**_MEM_SUFFIX_BIN, **_MEM_SUFFIX_DEC}.items():
if s.endswith(suf):
return int(float(s[: -len(suf)]) * mult)
return int(float(s))
def fmt_bytes(n: int) -> str:
if n <= 0:
return "0"
for suf, mult in [("Ti", 1024**4), ("Gi", 1024**3), ("Mi", 1024**2), ("Ki", 1024)]:
if n >= mult:
return f"{n / mult:.1f}{suf}"
return f"{n}B"
# ---------- SDL extraction ----------
@dataclass
class GPUAlternative:
"""One acceptable GPU spec from the SDL's vendor.<v>[*] list — matches if any one accepts."""
vendor: str
model: str | None = None
ram: str | None = None # raw form like "80Gi" — provider's gpuModels[].ram is reported the same way
interface: str | None = None # "pcie" or "sxm"
@dataclass
class ProfileRequirement:
profile: str
services: list[str]
count: int
cpu_millis: int
memory_bytes: int
storage_ephemeral_bytes: int
storage_persistent_bytes: int
storage_classes: list[str]
gpu_units: int
gpu_alternatives: list[GPUAlternative] # any one being satisfied is enough
cpu_arch: str | None # resources.cpu.attributes.arch: amd64 | arm64 | None (no attribute written)
needs_ip_endpoint: bool
denom: str | None
price_amount: int | None
placement_attributes: dict[str, str] # required attrs from profiles.placement.<used>.attributes
def extract_requirements(sdl: dict) -> list[ProfileRequirement]:
compute = (sdl.get("profiles") or {}).get("compute") or {}
placement = (sdl.get("profiles") or {}).get("placement") or {}
deployment = sdl.get("deployment") or {}
services = sdl.get("services") or {}
endpoints = sdl.get("endpoints") or {}
# services that use an IP endpoint (v2.1)
services_using_ip: set[str] = set()
for svc_name, svc in services.items():
for exp in svc.get("expose", []) or []:
for to in exp.get("to", []) or []:
if "ip" in to:
services_using_ip.add(svc_name)
ip_endpoint_declared = bool(endpoints)
# profile -> services using it; deployment maps service -> placement -> {profile, count}
profile_to_services: dict[str, list[tuple[str, int]]] = {}
denom_by_profile: dict[str, tuple[str | None, int | None]] = {}
placement_attrs_by_profile: dict[str, dict[str, str]] = {}
for svc_name, placements in deployment.items():
for place_name, spec in (placements or {}).items():
prof = spec.get("profile")
count = int(spec.get("count", 1))
if not prof:
continue
profile_to_services.setdefault(prof, []).append((svc_name, count))
place_block = placement.get(place_name) or {}
price = (place_block.get("pricing") or {}).get(prof)
if price:
denom_by_profile[prof] = (price.get("denom"), price.get("amount"))
# Placement-level attribute requirements (e.g. region: us-west) are required of any
# provider bidding on this profile. Provider matches via subset semantics.
place_attrs = place_block.get("attributes") or {}
if place_attrs:
merged = placement_attrs_by_profile.setdefault(prof, {})
for k, v in place_attrs.items():
merged[str(k)] = str(v)
reqs: list[ProfileRequirement] = []
for prof_name, prof in compute.items():
resources = prof.get("resources") or {}
cpu_raw = resources.get("cpu") or {}
cpu_millis = parse_cpu_millis(cpu_raw.get("units"))
cpu_arch_raw = (cpu_raw.get("attributes") or {}).get("arch")
cpu_arch = str(cpu_arch_raw).strip() if cpu_arch_raw is not None else None
memory_bytes = parse_bytes((resources.get("memory") or {}).get("size"))
storage_raw = resources.get("storage")
ephemeral = 0
persistent = 0
classes: list[str] = []
if isinstance(storage_raw, dict):
ephemeral = parse_bytes(storage_raw.get("size"))
elif isinstance(storage_raw, list):
for entry in storage_raw:
size = parse_bytes(entry.get("size"))
attrs = entry.get("attributes") or {}
cls = attrs.get("class")
is_persistent = bool(attrs.get("persistent"))
if is_persistent:
persistent += size
else:
ephemeral += size
if cls and cls != "ram" and is_persistent:
classes.append(cls)
gpu_raw = resources.get("gpu") or {}
gpu_units = int(gpu_raw.get("units", 0) or 0)
gpu_alts: list[GPUAlternative] = []
if gpu_units > 0:
vendor_attr = (gpu_raw.get("attributes") or {}).get("vendor") or {}
for v, models in vendor_attr.items():
if not models:
# `vendor: { nvidia: }` with no list — vendor-only constraint
gpu_alts.append(GPUAlternative(vendor=str(v).lower()))
continue
for m in models:
if not isinstance(m, dict):
continue
gpu_alts.append(
GPUAlternative(
vendor=str(v).lower(),
model=str(m["model"]).lower() if m.get("model") else None,
ram=str(m["ram"]) if m.get("ram") else None,
interface=str(m["interface"]).lower() if m.get("interface") else None,
)
)
svc_list = profile_to_services.get(prof_name, [])
total_count = sum(c for _, c in svc_list) or 1
needs_ip = any(s in services_using_ip for s, _ in svc_list) or ip_endpoint_declared
denom, amount = denom_by_profile.get(prof_name, (None, None))
reqs.append(
ProfileRequirement(
profile=prof_name,
services=[s for s, _ in svc_list],
count=total_count,
cpu_millis=cpu_millis * total_count,
memory_bytes=memory_bytes * total_count,
storage_ephemeral_bytes=ephemeral * total_count,
storage_persistent_bytes=persistent * total_count,
storage_classes=sorted(set(classes)),
gpu_units=gpu_units * total_count,
gpu_alternatives=gpu_alts,
cpu_arch=cpu_arch,
needs_ip_endpoint=needs_ip,
denom=denom,
price_amount=amount,
placement_attributes=placement_attrs_by_profile.get(prof_name, {}),
)
)
return reqs
# ---------- Provider matching ----------
_STORAGE_CLASS_ATTR_RE = re.compile(r"^capabilities/storage/(\d+)/class$")
_STORAGE_PERSIST_ATTR_RE = re.compile(r"^capabilities/storage/(\d+)/persistent$")
_CPU_ARCH_ATTR_KEY = "capabilities/cpu/arch"
# Declared vocabulary is x86-64 / arm-64; amd64, x86_64, arm64 show up in the wild. Fold to SDL values.
# 32-bit x86 / arm have no alias and never match.
_CPU_ARCH_ALIASES = {
"amd64": "amd64",
"x86-64": "amd64",
"x86_64": "amd64",
"x64": "amd64",
"arm64": "arm64",
"arm-64": "arm64",
"aarch64": "arm64",
}
SDL_CPU_ARCHS = {"amd64", "arm64"} # chain-sdk go/sdl/cpu.go cpuArchitectures
def provider_cpu_archs(provider: dict) -> set[str]:
"""Normalised CPU architectures a provider declares; empty when unknown.
Sources: Console-derived `hardwareCpuArch` (comma-joined when the attribute repeats) and the
raw on-chain attribute `capabilities/cpu/arch`. Self-declared; the bid engine matches on live
node inventory, which the providers endpoint does not expose."""
raw: list[str] = []
if provider.get("hardwareCpuArch"):
raw.extend(str(provider["hardwareCpuArch"]).split(","))
for attr in provider.get("attributes") or []:
if (attr.get("key") or "").strip() == _CPU_ARCH_ATTR_KEY:
raw.append(str(attr.get("value") or ""))
out: set[str] = set()
for v in raw:
norm = _CPU_ARCH_ALIASES.get(v.strip().lower())
if norm:
out.add(norm)
return out
def provider_cpu_arch_satisfies(provider: dict, arch: str | None) -> bool:
"""Hard constraint. No arch requested: not applicable. Arch requested: the provider must
declare it. An undeclared architecture fails, same as the empty-gpuModels rule."""
if not arch:
return True
return arch.lower() in provider_cpu_archs(provider)
def provider_persistent_classes(provider: dict) -> set[str]:
"""Return the set of persistent storage classes a provider supports, based on its attributes.
Providers declare class support via attribute pairs:
capabilities/storage/<N>/class = beta2 | beta3 | ram | ...
capabilities/storage/<N>/persistent = true | false
A class counts as persistent-capable only when the matching slot's persistent=true.
Also honors the alternate `feat-persistent-storage-type=<class>` attribute form.
"""
by_slot_class: dict[str, str] = {}
by_slot_persist: dict[str, bool] = {}
extra_persistent: set[str] = set()
for attr in provider.get("attributes") or []:
key = (attr.get("key") or "").strip()
val = str(attr.get("value") or "").strip().lower()
m = _STORAGE_CLASS_ATTR_RE.match(key)
if m:
by_slot_class[m.group(1)] = val
continue
m = _STORAGE_PERSIST_ATTR_RE.match(key)
if m:
by_slot_persist[m.group(1)] = val == "true"
continue
if key == "feat-persistent-storage-type" and val:
extra_persistent.add(val)
out: set[str] = set(extra_persistent)
for slot, cls in by_slot_class.items():
if by_slot_persist.get(slot) and cls not in ("ram",):
out.add(cls)
return out
def provider_supports_storage_class(provider: dict, cls: str) -> bool:
return cls.lower() in provider_persistent_classes(provider)
def provider_gpu_satisfies(provider: dict, alts: list[GPUAlternative]) -> bool:
"""Mirror provider/cluster/.../inventory.go tryAdjustGPU: any one of the SDL's vendor[*]
alternatives is satisfied if at least one entry in the provider's gpuModels[] matches all
specified fields (vendor required, model/ram/interface filtered when provided)."""
if not alts:
return True
pmodels = provider.get("gpuModels") or []
if not pmodels:
return False
for alt in alts:
for g in pmodels:
if alt.vendor and (g.get("vendor") or "").lower() != alt.vendor:
continue
if alt.model and (g.get("model") or "").lower() != alt.model:
continue
if alt.ram and str(g.get("ram") or "") != alt.ram:
continue
if alt.interface and (g.get("interface") or "").lower() != alt.interface:
continue
return True
return False
def provider_satisfies_attributes(provider: dict, required: dict[str, str]) -> bool:
"""Mirror chain-sdk Attributes.SubsetOf: every required key=value must appear in the
provider's attributes[], with glob-aware value match (filepath.Match semantics).
Provider-side wildcards work — e.g. provider declares `region=*` and SDL requires
`region=us-west`, that's a match."""
if not required:
return True
pattrs = {a.get("key"): str(a.get("value") or "") for a in (provider.get("attributes") or [])}
for k, want in required.items():
have = pattrs.get(k)
if have is None:
return False
# Glob match in either direction (provider-side wildcard OR SDL-side wildcard)
if not (fnmatch.fnmatchcase(want, have) or fnmatch.fnmatchcase(have, want) or have == want):
return False
return True
def check_provider(provider: dict, req: ProfileRequirement) -> dict[str, bool]:
stats = provider.get("stats") or {}
cpu_avail = int((stats.get("cpu") or {}).get("available", 0) or 0)
mem_avail = int((stats.get("memory") or {}).get("available", 0) or 0)
storage = stats.get("storage") or {}
eph_avail = int((storage.get("ephemeral") or {}).get("available", 0) or 0)
pers_avail = int((storage.get("persistent") or {}).get("available", 0) or 0)
gpu_avail = int((stats.get("gpu") or {}).get("available", 0) or 0)
checks = {
"placement_attributes": provider_satisfies_attributes(provider, req.placement_attributes),
"cpu": cpu_avail >= req.cpu_millis,
"cpu_arch": provider_cpu_arch_satisfies(provider, req.cpu_arch),
"memory": mem_avail >= req.memory_bytes,
"storage_ephemeral": eph_avail >= req.storage_ephemeral_bytes,
"storage_persistent": (
pers_avail >= req.storage_persistent_bytes if req.storage_persistent_bytes > 0 else True
),
"storage_classes": (
all(provider_supports_storage_class(provider, c) for c in req.storage_classes)
if req.storage_classes
else True
),
"gpu_count": gpu_avail >= req.gpu_units if req.gpu_units > 0 else True,
"gpu_model": (
provider_gpu_satisfies(provider, req.gpu_alternatives) if req.gpu_units > 0 else True
),
"ip_endpoint": bool(provider.get("featEndpointIp")) if req.needs_ip_endpoint else True,
}
return checks
# ---------- Reporting ----------
def fetch_providers(api_url: str) -> list[dict]:
req = urllib.request.Request(
api_url,
headers={
"Accept": "application/json",
"User-Agent": "akash-bid-matcher/0.1 (+https://github.com/akash-network/console)",
},
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read())
if isinstance(data, dict) and "providers" in data:
data = data["providers"]
return data
def summarize(providers: list[dict], reqs: list[ProfileRequirement], top_n: int = 5) -> dict:
online = [p for p in providers if p.get("isOnline")]
online_audited = [p for p in online if p.get("isAudited")]
per_profile = []
for req in reqs:
funnel_stages: list[tuple[str, int]] = [
("total_providers", len(providers)),
("online", len(online)),
("online_audited", len(online_audited)),
]
check_names = [
"placement_attributes",
"cpu",
"cpu_arch",
"memory",
"storage_ephemeral",
"storage_persistent",
"storage_classes",
"gpu_count",
"gpu_model",
"ip_endpoint",
]
matches: list[dict] = []
per_check_pass = {name: 0 for name in check_names}
pool = online_audited
for p in pool:
checks = check_provider(p, req)
for k, v in checks.items():
if v:
per_check_pass[k] += 1
if all(checks.values()):
matches.append(
{
"owner": p.get("owner"),
"organization": p.get("organization") or p.get("host"),
"hostUri": p.get("hostUri"),
"region": p.get("locationRegion"),
"country": p.get("country"),
"tier": p.get("tier"),
"gpuModels": [
f"{g.get('vendor')}/{g.get('model')}" for g in p.get("gpuModels") or []
],
"cpuArch": sorted(provider_cpu_archs(p)),
}
)
# Biggest filter = applicable check with lowest pass count (among checks that matter)
applicable = [n for n in check_names if _check_applies(n, req)]
biggest_filter = None
if applicable:
biggest_filter = min(applicable, key=lambda n: per_check_pass[n])
per_profile.append(
{
"profile": req.profile,
"services": req.services,
"count": req.count,
"requirements": {
"cpu_millis": req.cpu_millis,
"cpu_arch": req.cpu_arch,
"memory": fmt_bytes(req.memory_bytes),
"storage_ephemeral": fmt_bytes(req.storage_ephemeral_bytes),
"storage_persistent": fmt_bytes(req.storage_persistent_bytes),
"storage_classes": req.storage_classes,
"gpu_units": req.gpu_units,
"gpu_alternatives": [
{k: v for k, v in vars(a).items() if v} for a in req.gpu_alternatives
],
"needs_ip_endpoint": req.needs_ip_endpoint,
"placement_attributes": req.placement_attributes,
"denom": req.denom,
"price_amount": req.price_amount,
},
"funnel": [{"stage": s, "count": c} for s, c in funnel_stages]
+ [
{"stage": f"passes_{n}", "count": per_check_pass[n], "applicable": _check_applies(n, req)}
for n in check_names
],
"biggest_filter": biggest_filter,
"match_count": len(matches),
"top_matches": matches[:top_n],
"feasible": len(matches) > 0,
"denom_note": _denom_note(req.denom),
"cpu_arch_note": _cpu_arch_note(req.cpu_arch),
}
)
return {
"api": "console-api.akash.network/v1/providers",
"total_providers": len(providers),
"online_providers": len(online),
"online_audited_providers": len(online_audited),
"profiles": per_profile,
}
def _check_applies(name: str, req: ProfileRequirement) -> bool:
if name == "storage_persistent":
return req.storage_persistent_bytes > 0
if name == "storage_classes":
return bool(req.storage_classes)
if name in ("gpu_count", "gpu_model"):
return req.gpu_units > 0
if name == "ip_endpoint":
return req.needs_ip_endpoint
if name == "placement_attributes":
return bool(req.placement_attributes)
if name == "cpu_arch":
return bool(req.cpu_arch)
return True
KNOWN_DENOMS = {"uact"}
def _denom_note(denom: str | None) -> str | None:
if denom is None:
return None
if denom in KNOWN_DENOMS:
return None
return (
f"denom '{denom}' is not the native Akash denom (uact) — "
"providers reject non-uact denoms for lease payment."
)
def _cpu_arch_note(arch: str | None) -> str | None:
if arch is None or arch in SDL_CPU_ARCHS:
return None
return (
f"cpu.attributes.arch '{arch}' is not a valid SDL value (amd64 or arm64); "
"the SDL is rejected at validation before any bid window opens."
)
# ---------- CLI ----------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Match an SDL against live Akash providers.")
parser.add_argument("sdl", help="Path to SDL YAML file")
parser.add_argument("--api", default=DEFAULT_API, help="Providers API URL")
parser.add_argument("--top", type=int, default=5, help="Top matching providers to include")
parser.add_argument("--json", action="store_true", help="Emit JSON only (no pretty header)")
args = parser.parse_args(argv)
with open(args.sdl, "r") as f:
sdl = yaml.safe_load(f)
reqs = extract_requirements(sdl)
providers = fetch_providers(args.api)
report = summarize(providers, reqs, top_n=args.top)
print(json.dumps(report, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())
SKILL.md
---
name: akash
description: >
Build, validate, and deploy workloads to the Akash Network — the decentralized cloud
marketplace. Covers SDL syntax & examples, choosing a deployment method (Console API,
the akt CLI, provider-services, TypeScript/Go SDKs), authentication (API key, JWT, self-custody wallets),
Console teams and projects (team and project API keys, team billing),
deployment lifecycle, fetching logs/events via the provider proxy, and fee grants/authz.
Also covers AkashML — the managed inference surface for calling open-source LLMs on
Akash compute via OpenAI/Anthropic-compatible APIs. Use for "deploy to Akash", "Akash SDL",
"Akash Console API", "Akash CLI deploy", "akt deploy", "akt console", "Akash API key", "x-api-key", "Akash deploy logs",
"stream Akash logs", "integrate Akash into my app", "@akashnetwork/chain-sdk",
"@akashnetwork", "AkashML", "managed inference on Akash", "call an LLM on Akash",
"playground.akashml.com", "api.akashml.com".
license: MIT
metadata:
author: Akash Network
version: "3.0.0"
argument-hint: <task-description>
---
# Akash — Deploy Workloads
This skill covers everything needed to deploy and operate a workload on the Akash Network: writing SDL, picking a deployment method, authenticating, creating the deployment, accepting bids, and reading logs/events from the live deployment.
For running an Akash **provider**, use the `akash-network:akash-provider` skill. For running a **full node or validator**, use `akash-network:akash-node`.
## Critical Rules
**NEVER use `:latest` or omit image tags.** Always specify explicit version tags so deployments are reproducible.
```yaml
# CORRECT
image: nginx:1.25.3
image: postgres:16
# WRONG
image: nginx:latest
image: nginx # implies :latest
```
**Akash has two tokens — don't conflate them.**
- **AKT** (`uakt`, 1 AKT = 1,000,000 uakt) is the **chain token**. Used for gas fees (`--gas-prices`, `minimum-gas-prices`, SDK `WithGasPrices`), staking (self-delegation, validator bonds, slashing), and validator rewards. Self-custody wallets hold AKT.
- **ACT** (`uact`, 1 ACT = 1,000,000 uact) is the **deployment-payment token**. Used for SDL pricing (`denom: uact`), deployment escrow, **bid prices** (the rate the provider offers), and lease payments. Note: the **provider-side bid deposit** (anti-spam collateral on `MsgCreateBid`, the `bidMinDeposit` config) is `uakt`, not `uact` — providers operate exclusively in AKT for chain operations and earn ACT from leases. Deployers don't deal with this directly.
**For self-custody deployers: you must mint ACT by burning AKT before you can fund a deployment.** Acquire AKT first (exchange / faucet for testnet), then convert a portion to ACT via the burn/mint mechanism. Console API users skip this entirely — you add USD credits and Console funds each deployment from them automatically (Stripe → USD → ACT server-side).
When you see `--gas-prices 0.025uakt`, that's correct (gas is paid in AKT). When you see `denom: uact, amount: 1000` in an SDL, that's also correct (lease pricing is in ACT). Both denoms coexist in normal commands.
## Choosing a Deployment Method
This is the most important decision in this skill — and the one most easily gotten wrong.
**Default behavior: recommend + proceed, don't interrogate.** If the user gave any reasonable cue (see the cue table below), commit to that path silently and start producing code. If you must ask, ask **one short question**, give a recommendation, and offer to switch — don't build a multi-step decision tree before the user has seen any commands. The goal is to get them running code fast, not to make them answer a quiz.
Only **stop and ask** when the prompt is genuinely silent about everything (no language signal, no wallet signal, no CI signal). Even then, recommend + offer to switch rather than presenting an open menu of equals.
Once a path is chosen, **stay on it** for the rest of the conversation. Do not silently switch. If the user explicitly asks to switch later, do so cleanly.
There are **five deployment paths** (you host the workload) plus one **consumption path** (you call a hosted model):
| # | Method | Wallet model | Auth | When to use |
|---|---|---|---|---|
| 1 | **Console API** | Managed (Console holds the wallet of the key's personal account or team) | `x-api-key` header | CI/CD, server-to-server, any backend that wants HTTP + an API key. No private-key handling. |
| 2 | **akt CLI** | Managed through a Console API key, or self-custody through a local keyring | `AKT_CONSOLE_API_KEY` or a context credential; or a local key | Terminal and agent sessions: one `akt deploy` goes from SDL to a running service, and `akt console` reads logs, status and shell. |
| 3 | **Akash CLI (`provider-services`)** | Self-custody (you hold the keys) | Local key + signature | Existing `provider-services` scripts and manual chain workflows. The official docs now point new CLI users to akt. |
| 4 | **TypeScript SDK** | Self-custody (browser wallet, hardware, or local key) | SDK signs locally | dApps, Node.js services, anywhere you want JS/TS code to deploy. |
| 5 | **Go SDK** | Self-custody | SDK signs locally | Backend Go services, custom tooling. |
| 6 | **AkashML** *(consumption, not deployment)* | Managed (AkashML account, USD credits) | `Authorization: Bearer` | You want to **call** an LLM, not host one. OpenAI/Anthropic-compatible REST. No SDL, no wallet, no ACT. |
Rows 1–5 are paths to **host your own workload** on Akash. Row 6 is the answer when the user wants to **consume** inference instead — different problem, different surface. See [@rules/deploy/akashml/overview.md](rules/deploy/akashml/overview.md) for the full split.
### The concept that bites everyone
**Console holds the wallet, and a key acts in one place.** Every Console user has a personal account and can belong to teams; each of them has its own managed wallet and credits. An API key is created in one of them and acts only there, with its creator's role, and it may be limited to one project. Deployments spend from that wallet. There is no separate "Console API + bring your own wallet" mode.
So a key does not always carry full account rights: a team `viewer` or `billing` member's key can't deploy, a project key only reaches its project (anything else answers 403 `project_forbidden`), and a key never spends outside the one personal account or team it was created in. Read the key's reach from `GET /v1/organizations` and `GET /v1/projects` instead of assuming it; see `rules/deploy/console-api/teams-and-projects.md`.
If a user has a self-custody wallet (Keplr or Ledger) and wants to deploy from *that* wallet, they need the **CLI** or **an SDK**. The Console API will not sign with an external wallet.
### Console has two flavors — disambiguate
- **Standard Console** at `console.akash.network` — managed wallet. This is what the Console API in this skill operates on.
- **Console Air** ([github.com/akash-network/console-air](https://github.com/akash-network/console-air)) — self-custody UI for Keplr or hardware wallets. **Self-hosted** — there is no official hosted URL at `console-air.akash.network`; users clone the repo and run it locally or on their own infrastructure. **Out of scope for this skill** (it is a UI, not an API). If a user wants a UI with self-custody, point them at the repo and stop; programmatic self-custody users go CLI or SDK.
### Recognize cues — and use them aggressively
If you see **any** of these signals, commit to the matching path silently and start producing code. Don't ask for confirmation; the cue *is* the confirmation.
| If the user mentions… | Commit to… |
|---|---|
| `"akt"`, `"akt deploy"`, `"akt redeploy"`, `"akt console"`, `"akt context"`, `AKT_CONSOLE_API_KEY`, `"akash-network/tap/akt"`, or the user wants it done from this terminal and `command -v akt` finds it | akt CLI (its context decides Console or self-custody) |
| `"I have an API key"`, a key starting `ac.sk.`, `"$AKASH_API_KEY"`, `"x-api-key"`, `"x-project-id"`, `"x-organization-id"`, a Console team or project, `"curl"`, `"CI/CD"`, `"GitHub Actions"`, `"backend"`, `"server-to-server"`, `"deploy from CI"`, `"automate"` | Console API |
| `"Keplr"`, `"Ledger"`, `"hardware wallet"`, `"my wallet"`, `"self-custody"`, `"provider-services keys add"`, `"my mnemonic"` | CLI or SDK (ask only if they didn't also signal a language) |
| `"React app"`, `"Next.js"`, `"akashjs"` (legacy), `"@akashnetwork/chain-sdk"`, `"my dApp"`, `"in the browser"` | TypeScript SDK |
| `"Go service"`, `"golang"`, `"cosmos-sdk Go"`, `"my Go backend"` | Go SDK |
| `"AkashML"`, `"akml-..."`, `"managed inference"`, `"call an LLM"`, `"hosted LLM on Akash"`, `"OpenAI-compatible"`, `"Anthropic-compatible"`, `"playground.akashml.com"`, `"api.akashml.com"`, `ANTHROPIC_BASE_URL` pointing at AkashML | AkashML (consumption path) |
| `"Console Air"`, `"web UI for my Keplr wallet"`, `"GUI"` + `"self-custody"` | Console Air (out-of-scope; one-line point to repo, then stop) |
| `"easiest"`, `"simplest"`, `"first time"`, `"just want to try"` + no wallet/keys signal | Console (UI or API — recommend the UI for a one-off, the API for a script) |
When cues point two ways, the specific one wins: a CI job or an API key plus a mention of akt stays on akt.
**Ambiguity on LLMs — ask one short question.** If the user says *"run an LLM on Akash"* without saying *"my own"* or *"call"*, that cue is genuinely split between **AkashML** (call a hosted model) and **self-deploy with GPU SDL** (host the model yourself). One line: *"Do you want to call a hosted LLM, or run your own instance?"* — then commit. The two answers go to completely different files (AkashML vs `rules/sdl/examples/gpu-workload.md`).
**Only ask** when the prompt is genuinely empty of signals — no language hint, no wallet hint, no CI/automation hint. When you do ask, the question is one line: *"Do you want this in a script (Console API) or a UI (Console for managed wallet, Console Air for self-custody)?"* — then proceed with their answer.
A "first small deploy" with no other signals → just recommend Console UI and write the SDL. The answer's last line is a single question, ending in a question mark, that names the alternatives: *"Would you rather script it with the akt CLI or the Console API, or deploy from your own wallet?"* Do **not** make the user pick from the method table before seeing any commands.
### Language for the Console API — infer first, ask only if silent
Once on the Console API path, pick the integration language from cues. **Ask only as a last resort.**
| Cue | Use |
|---|---|
| `"curl"`, `"bash"`, `"shell"`, `"GitHub Actions"`, `"GitLab CI"`, any `.sh` reference, no app stack mentioned | curl + Bash |
| `"Node"`, `"Next.js"`, `"Express"`, `package.json`, `.ts`/`.js` file, `"my Node app"` | TypeScript / Node `fetch` |
| `"Python"`, `requirements.txt`, `.py`, `"FastAPI"`, `"Django"`, `"Flask"` | Python `requests` or `httpx` |
| `"Go"`, `go.mod`, `.go`, `"my Go service"` | Go `net/http` |
| `"Rust"`, `Cargo.toml` | Rust `reqwest` |
**Default to curl + Bash** when the language is unstated *and* the surrounding context is automation/CI (the most common case for API-key users). Don't ask unless you have a positive reason to believe the user wants a specific other runtime — and even then, write curl first and offer to translate. Curl is the universal lingua franca for HTTP and translates trivially to any language.
**Skip the question entirely** if:
- Any cue above is present (commit to that language).
- The user already pasted code in a specific language (match it).
- The user said `"curl"`, `"shell"`, `"CI"`, or anything implying scripts — they want Bash.
**Only ask** when there are zero signals — e.g. *"deploy to Akash via the API"* with no other context. Then it's one line: *"Want this as curl/Bash, Node, Python, or Go?"* — and produce the response in their answer.
This is a separate gate from the deployment-method selection. Once both are chosen, **stay on both** for the rest of the conversation.
### API key handling — silent rules, only intervene when needed
Two non-negotiable rules, applied silently:
1. **Always reference the key via env var** in code: `$AKASH_API_KEY` (Bash), `process.env.AKASH_API_KEY` (Node), `os.environ["AKASH_API_KEY"]` (Python), `os.Getenv("AKASH_API_KEY")` (Go). akt reads `AKT_CONSOLE_API_KEY` itself, so on the akt path export that name and never pass the key as an argument.
2. **Never echo a literal key value** in code or chat, even if the user pasted one. If the user pastes a literal key, replace it with `$AKASH_API_KEY` in the response and add **one sentence** at the top: *"I've used `$AKASH_API_KEY` in the code below — export your key as that env var before running."*
**When to add extra guidance** (only one of these — pick the smallest one that applies):
- User pastes a literal key → one-sentence redirect (above). Don't lecture; just use the env var name.
- User asks where to put the key for a *specific runtime* they mentioned (GitHub Actions, GitLab CI, Docker, etc.) → answer for *that* runtime in one line. Don't enumerate all of them.
- User is **writing a Dockerfile** → flag that the key must be passed at runtime (`-e`) and not baked in via `ARG`/`ENV`. One sentence.
- User mentions a `.env` file → one-line `.gitignore` reminder. Don't assume they need a tutorial.
**Stay silent on key handling** when:
- The user already said the key is in `$AKASH_API_KEY` or `${{ secrets.AKASH_API_KEY }}` or similar — they've got it. Don't repeat the rules.
- The user is asking about something downstream of the call (status, logs, bids) — the key handling is settled.
- The conversation isn't producing code at all (architecture questions, troubleshooting on existing deployments).
A pitfall table at the end of a code response *can* mention "don't use `Authorization: Bearer` for the API key" since that's a real common error — but keep it to one row, not a discourse on secret management.
### Once committed, stay there
- On the **Console API** path: don't suggest `provider-services keys add`, don't suggest mTLS certs (deprecated for Console API — see `rules/deploy/cli/mtls-legacy.md`), don't suggest `provider-services query market bid list` — every read is an HTTP call with `x-api-key` in whatever language the user picked. Console records each deployment's SDL, so: change a running deployment with `PATCH /v1/deployments/{dseq}` (never the deprecated full-SDL `PUT`), send no `manifest` to `POST /v1/leases` and no `deposit` anywhere, and treat CPU, memory, storage, GPU or replica changes as a new deployment.
- **In a team:** deploy into the intended project (`data.projectId`; a key bound to a project needs none), list with `?projectId=`, move with `PATCH /v1/deployments/{dseq}/project`, and explain `project_forbidden`, `project_required`, `project_budget_exceeded`, `organization_forbidden`, `organization_mismatch` and `session_required` from `rules/deploy/console-api/teams-and-projects.md`. Team credits, cards and Auto recharge are set on the team's Billing page by an owner or billing member, never from a script.
- On the **akt** path: run `akt` commands, not curl and not `provider-services`. Check `akt version` first: 1.0.1 and later refuse any deposit on a Console deploy, and 0.1.x needs `--deposit 0.5` there (better: `brew upgrade akash-network/tap/akt`). Anything unattended passes `--bid-select` and `--yes` and reads `-o json` or `-o jsonl`. Without a terminal there is no setup wizard, so create the context (`akt context create <name> --deploy-via console`) before the first command. On 1.0.1, secrets are `ac-secret://NAME` references with values in `--secrets-file` (never on the command line), `akt update --patch` changes single fields, and a resource change is `akt redeploy <dseq> --sdl-file <new>`, which leaves the old deployment running. Setup and recipes: `rules/deploy/akt/`.
- On the **Akash CLI (`provider-services`)** path: don't suggest `/v1/deployments` HTTP calls. Don't suggest API keys. Stay on `provider-services tx ...` / `provider-services query ...`.
- On the **SDK** paths: don't reach for curl examples or CLI commands; the user wants code.
- On the **AkashML** path: do not write SDL, do not talk about `uact`/`uakt`/leases/bids. The user is calling a hosted inference API, not deploying. Stay on `Authorization: Bearer $AKASHML_API_KEY` and `https://api.akashml.com/{v1,anthropic}` calls. If they later say *"actually I want to host my own"*, then switch cleanly to one of the five deployment paths.
- **Always query the live API for catalog/pricing/capabilities — never trust the model IDs, prices, or feature lists baked into this skill.** Before recommending a model, quoting pricing, or claiming a feature (tool use, reasoning, streaming, context length), call the relevant endpoint:
- `GET https://api.akashml.com/v1/models` — pricing (per-million tokens), `context_length`, `supported_features`, `supported_sampling_parameters`, `max_output_length`, `quantization`
- `GET https://api.akashml.com/anthropic/v1/models` — Anthropic-shape list (returns IDs aliased with `--`)
- Any other documented endpoint when the user asks something the docs may have stale info on
- This skill's named models (DeepSeek, MiniMax, Kimi, Qwen) are examples that may have shifted; the live list is authoritative. If the user provides their own API key, hit the live endpoint with their key before producing an answer that pins a model or price.
If the user asks to **switch** ("can we do this via the API instead?"), acknowledge the switch, and from that point on apply the new path's rules.
## Quick Reference
### SDL Structure
Every SDL has four required sections (plus optional `endpoints` for IP leases and `reclamation` for resource reclamation):
```yaml
version: "2.0" # or "2.1" for IP endpoints or the reclamation block
services: # Container definitions
profiles: # Compute resources & placement
deployment: # Service-to-profile mapping
```
### Minimal SDL Template
```yaml
version: "2.0"
services:
web:
image: nginx:1.25.3
expose:
- port: 80
as: 80
to:
- global: true
profiles:
compute:
web:
resources:
cpu:
units: 0.5
memory:
size: 512Mi
storage:
size: 1Gi
placement:
dcloud:
pricing:
web:
denom: uact
amount: 1000
deployment:
web:
dcloud:
profile: web
count: 1
```
## Documentation Structure
### Core Concepts
- **@rules/overview.md** — Akash Network introduction and architecture
- **@rules/terminology.md** — Key terms (lease, bid, dseq, gseq, oseq)
- **@rules/pricing.md** — Payment with uact (deployment-payment denom)
### SDL Configuration
- **@rules/sdl/schema-overview.md** — Version requirements and SDL structure
- **@rules/sdl/services.md** — Service configuration (image, expose, `http_options`, env, credentials)
- **@rules/sdl/compute-resources.md** — CPU (units, `arch`), memory, storage, and GPU specifications
- **@rules/sdl/placement-pricing.md** — Provider selection and pricing (uact)
- **@rules/sdl/deployment.md** — Service-to-profile mapping
- **@rules/sdl/endpoints.md** — IP endpoint configuration (v2.1)
- **@rules/sdl/reclamation.md** — Resource reclamation `reclamation` block (v2.1, AEP-82)
- **@rules/sdl/confidential-compute.md** — Confidential Compute / TEE (`params.tee`, AEP-83)
- **@rules/sdl/validation-rules.md** — All constraints and validation rules
### SDL Examples
- **@rules/sdl/examples/web-app.md** — Simple web deployment
- **@rules/sdl/examples/wordpress-db.md** — Multi-service with persistent storage
- **@rules/sdl/examples/gpu-workload.md** — GPU deployment with NVIDIA
- **@rules/sdl/examples/ip-lease.md** — IP endpoint configuration
- **@rules/sdl/examples/reclamation.md** — Resource reclamation grace window (v2.1)
- **@rules/sdl/examples/confidential-compute.md** — Confidential Compute (TEE): CPU-only and CPU+GPU
### Deployment Methods
- **@rules/deploy/overview.md** — Method selection (start here)
- **@rules/deploy/console-api/** — Console API (API key path)
- `overview.md` — base URL, envelopes, error codes, deprecated and UI-internal endpoints, endpoint map
- `authentication.md` — API keys (`x-api-key`, `ac.sk.…` format, CRUD), public endpoints, CI/CD
- `deployment-endpoints.md` — create, list, read, `PATCH`, close; leases, bids, runtime limits, providers, placement, bid screening
- `secrets.md` — `ac-secret://` references, `sealedSecrets`, `inheritSecretsFrom`
- `api-key-quickstart.md` — linear walkthrough from "I have an API key" to a running deployment
- `account-and-funding.md` — account model, balance, automatic funding, 402 refusals (signup and adding credits are UI-only)
- `teams-and-projects.md`: teams and roles, keys bound to a team or project, `x-organization-id` and `x-project-id`, deploying into, listing by and moving between projects, project budgets, team billing, the team errors
- `operations.md` — provider JWT (`leases` shapes, scopes), logs/events/status/shell, TEE attestation
- **@rules/deploy/akt/** — akt CLI (Console API key or local keyring; terminal and agent sessions)
- `overview.md` — install, the 0.1.x versus 1.0 differences (deposit), contexts and credentials without a terminal, JSON/JSONL output, the MCP server, how akt maps to the Console API
- `console-workflows.md` — SDL, deploy, logs/events/status/shell, update, funding, close, capacity, recovery, CI
- **@rules/deploy/cli/** — Akash CLI (`provider-services`, self-custody path)
- `mtls-legacy.md` — Legacy mTLS auth for CLI/SDK direct provider calls (deprecated for Console API)
### Managed Inference (consumption, not deployment)
- **@rules/deploy/akashml/** — AkashML: call hosted open-source LLMs on Akash compute
- `overview.md` — base URLs, when to use AkashML vs self-deploy GPU, billing differences
- `authentication.md` — `akml-...` keys, `Authorization: Bearer`, env-var rules
- `api-reference.md` — OpenAI + Anthropic surfaces with curl + SDK examples
- `quickstart.md` — Linear walkthrough from signup to first inference call
- `account-and-billing.md` — USD credits (off-chain, distinct from `uact`), per-key RPM/TPM limits
- `claude-code-integration.md` — Point Claude Code's Anthropic client at AkashML via `ANTHROPIC_BASE_URL`
### SDK Documentation
- **@rules/sdk/overview.md** — SDK comparison and selection
- **@rules/sdk/typescript/** — TypeScript SDK for web and Node.js
- **@rules/sdk/go/** — Go SDK for backend services
### AuthZ (Delegated Permissions)
- **@rules/authz/** — Fee grants and deployment authorization
### Bid Matching (deployer-facing)
- **@rules/bid-matching/overview.md** — When to use, how to run the matcher, how to present results
- **@rules/bid-matching/adaptation-rules.md** — Priority order for SDL changes
- **@rules/bid-matching/matching-rules.md** — SDL ↔ provider field mapping
### Reference
- **@rules/reference/storage-classes.md** — beta2, beta3, ram storage
- **@rules/reference/gpu-models.md** — Supported NVIDIA GPUs
- **@rules/reference/rpc-endpoints.md** — Public RPC endpoints
## Common Patterns
### Environment Variables
```yaml
services:
app:
env:
- "DATABASE_URL=postgres://..."
- "NODE_ENV=production"
```
### Persistent Storage
```yaml
profiles:
compute:
app:
resources:
storage:
- size: 10Gi
attributes:
persistent: true
class: beta2
```
### GPU Workloads
```yaml
profiles:
compute:
ml:
resources:
gpu:
units: 1
attributes:
vendor:
nvidia:
- model: a100
```
### CPU Architecture (arm64)
```yaml
profiles:
compute:
api:
resources:
cpu:
units: 2
attributes:
arch: arm64 # or amd64; omit to write no CPU attribute (there is no default)
```
### Streaming Route (disable proxy buffering)
```yaml
services:
api:
expose:
- port: 8080
as: 80
to:
- global: true
http_options:
proxy:
buffering_disable: true # chain-sdk >= 1.0.0-alpha.44; not honored by providers yet
```
### Confidential Compute (TEE)
```yaml
services:
web:
params:
tee: cpu # or: cpu-gpu (requires GPU resources)
```
### Payment Options
- **uact** — Micro-denomination of ACT (the deployment-payment token, separate from AKT). The denom for SDL pricing (`amount: 1000`), deployment escrow, and lease payments. `uakt` is only accepted via a rare burn-mint (BME) fallback, not a normal alternative.
- Note: Console API deployments take no deposit at all. Console funds them from the USD credit balance of the key's personal account or team.
## Additional Resources
- **[awesome-akash](https://github.com/akash-network/awesome-akash)** — 100+ production-ready SDL templates
- **[Akash Network Docs](https://akash.network/docs/)** — Official documentation
- **[Console (managed wallet)](https://console.akash.network)** — Web UI; managed-wallet equivalent of this skill's Console API path
- **[Console Air (self-custody, self-hosted)](https://github.com/akash-network/console-air)** — Web UI for Keplr or hardware wallets; clone and run locally
- **[akt CLI docs](https://akash.network/docs/developers/deployment/akt/)** — The unified Akash CLI; its own agent skill, `akt-cli`, ships with each [release](https://github.com/akash-network/akt/releases) as `akt_<version>_skill.zip`
- **[Console API OpenAPI spec](https://console-api.akash.network/v1/doc)** — The live contract, with deprecations marked (this skill curates the deployment-management subset; the spec also contains Console-UI internals like Stripe, alerts, user signup)
- **[@akashnetwork/chain-sdk](https://www.npmjs.com/package/@akashnetwork/chain-sdk)** — TypeScript SDK (self-custody JWT signing, chain messages)