Agent Gateway: give your system an AI assistant, with no keys and no servers
Updated: 2026-08-04 · YaDominios
The Agent Gateway sits between your system and an AI assistant. Your app posts the conversation to yapanel.yadominios.com/agente/v1/chat with your site key, and we talk to the provider. The key point: it supports multi-turn tool use — the assistant can ask you to run one of your own functions (say `cierre_del_dia`) against YOUR database; you run it and call again with the result, as many rounds as needed. We never touch your database and never store the conversations. Billing is per use in credits: 1 credit = one thousandth of a dollar.
Install it in 5 steps
If you are in a hurry, this is the whole thing. The detail of each part is further down.
- Turn the gateway on. Dashboard → Agent Gateway in the left menu → Activate.
- Get your key.
sk_if your system has its own server;pk_if it is a web app with no backend. It is shown once. - If you use
pk_, authorize your domain on that same screen. Without it the key works from nowhere. - Describe your functions — the queries your system already knows how to run — in the
herramientasfield. - Write the loop: you call, and if the answer comes back with
parar_por: "herramienta"you run that function and call again with the result. Repeat until it says"fin".
Step 5 is the only one with any trick to it, and it is solved end to end in the floating-bubble example below: copy it and it works.
How it works, at a glance
Where the key comes from
What it is for
Your system already knows how to query its own database. What it does not know is how to understand a question in plain language and decide which query is needed. That is what the gateway adds.
What we do NOT do — the important part
- We never touch your database. When the assistant needs data it asks you. The query runs on your server, with your permissions. We do not know what is on the other side.
- We do not store conversations. Questions and business results pass through and are gone. Our log keeps only site, date, model, token counts and credits.
- No memory. You keep the full history and send it whole on every call.
The real flow: multi-turn tool use
This is not a single-pass chat:
- Your system sends the user's question.
- The gateway replies: “the assistant wants to run
cierre_del_diawith these parameters” (parar_por: "herramienta"). - Your system runs that function against your database and calls again with the result.
- The gateway returns the final text (
parar_por: "fin").
There can be several rounds. An integration that only looks for parar_por: "fin" will hang forever — step 2 is not optional.
What if my site is hosted somewhere else?
It works just the same. The gateway does not need your code or your repository: what your public key authorizes is your domain, and that one is with us. If you pay us for the service but your site lives with another provider, your dashboard shows it with the same Turn on the Agent Gateway button as anyone else.
In that case the name you send in X-Sitio is built from your domain with dots turned into dashes — my-store.com becomes my-store-com — and it is shown on the same screen where you create the key.
The two keys
| Key | Where it goes | How it is protected |
|---|---|---|
pk_… public | Can live in the browser, for web apps with no server of their own. | Works only from your site's authorized domains, plus a per-minute cap. |
sk_… secret | Server only. Never in a web page. | No domain restriction. Shown once, at creation. |
POST /agente/v1/chat
Headers: X-Sitio (your site's name in the dashboard) and X-Clave. Body fields are in Spanish because the contract is ours and versioned: modelo, sistema, herramientas, mensajes.
{
"modelo": "normal",
"sistema": "You are the assistant of a hardware store.",
"herramientas": [{
"nombre": "cierre_del_dia",
"descripcion": "Returns a branch's daily cash close for a date.",
"esquema": {"type":"object","properties":{"sucursal":{"type":"string"},"fecha":{"type":"string"}},"required":["sucursal","fecha"]}
}],
"mensajes": [{"rol":"usuario","contenido":[{"tipo":"texto","texto":"how much did I sell yesterday in Caracas?"}]}]
}
Block types
| tipo | Written by | Fields |
|---|---|---|
texto | You or the assistant | texto |
herramienta | The assistant — echo it back verbatim next round | id, nombre, entrada |
resultado | You, with what your function returned | id (same as the tool's), contenido, optional error |
GET /agente/v1/saldo
{ "ok": true, "creditos_restantes": 4971, "gastado_mes": 29, "sitio": "my-site" }
Credits expire after 90 days without use
The clock counts from the last time you used the assistant, not from when you topped up. Every question restarts it: as long as you use it, you never lose a credit. If the assistant goes three months without a single question, the balance expires and you top up again.
It never expires silently: we email you 15 days, 7 days and 1 day before, the date shows in your dashboard, and when it does expire the movement is recorded with its date and reason.
After expiry the assistant stays installed and your key stays the same — you only need to top up to use it again.
Errors, with stable codes
Every error carries "ok": false and an error field that never changes. Program against the code, not the message text.
| HTTP | error | What happened |
|---|---|---|
| 401 | clave_invalida | Key missing, revoked, or not for that site |
| 402 | sin_creditos | Out of credits (carries creditos_restantes: 0). The provider is not called. |
| 403 | origen_no_autorizado | Public key used from a domain you did not authorize |
| 403 | sitio_apagado | The site's gateway is off |
| 429 | limite_de_uso | Per-minute cap exceeded (carries reintentar_en, seconds) |
| 400 | modelo_no_disponible | Unknown alias. Valid: rapido, normal, maximo |
| 400 | peticion_invalida | Malformed body — the message names the exact block |
| 502 | falla_del_proveedor | The AI provider failed. No credits are charged. |
“auto”: let us pick
Besides the three, you can send "modelo": "auto" and we decide per round: the first one —where the assistant has to choose which of your functions to call— goes to the strong model, and the following ones —where it already has your data and only writes the answer— go to the fast one.
Measured in production on a question spanning four queries: 75 credits with a fixed model, 49 on auto. 35% less for the same answer.
Credits
1 credit = one thousandth of a dollar. A $10 top-up is 10,000 credits. Every call is charged, including the one that only asks you to run a function — that one consumed the whole model too. Nothing is charged when the provider fails or the key is invalid.
Limits and fine print (measured, not assumed)
| What | How much |
|---|---|
Requests per minute (pk_ key) | 20 by default, adjustable per site. It counts EVERY request, tool rounds included: one question can spend 5. If your system loops often, ask us to raise it. |
Requests per minute (sk_ key) | No cap. It lives on your server, not in someone else's browser. |
| Body size | Tested at 2 MB with no trouble. A 50 KB result is nothing; what costs is the tokens it takes. |
| Response length | Capped at 8,192 tokens. You can ask for less with max_tokens, not more. |
| Time per call | We do not cut it off. A call that thinks and crosses data takes 2 to 40 seconds depending on the model; set your own timeout. |
| Instruction size | No cap of ours. The model's context window is the limit, and it is one million tokens. |
| Number of tools | No cap of ours. Ten is comfortable; with many more the assistant struggles to choose. |
The cache: when it really kicks in, and how much it saves
Instructions and tools being billed at 10% on re-reads comes with a condition that is not obvious: the marked block has to exceed about 1,024 tokens (roughly 4,000 characters). Below that the marker is ignored and you pay full price with nothing warning you.
Measured against the live gateway with a 4,500-character system prompt:
call 1: cache write=1836 cache read=0 → 35 credits
call 2: cache write=0 cache read=1836 → 4 credits
call 3: cache write=0 cache read=1836 → 4 credits
Same question, nearly nine times cheaper. The cache lasts about 5 minutes and every use resets that clock: as long as someone asks every few minutes it stays warm by itself. If your system goes quiet for half an hour, the next question pays the write again — it is not worth keeping it warm with fake calls, since every call is billed too.
The condition for it to work: instructions and tools must travel byte-identical on every call. A timestamp inside the instructions breaks the cache on every request and multiplies the bill.
The assistant can request SEVERAL functions at once
It does not always ask for one. In a real test, asked to "compare the profit of Caracas and Valencia on August 3rd", it requested four at once: sales and expenses for each branch. Your code must walk contenido, run EVERY herramienta block, and return all results together in a single message with role usuario. Sending them one at a time gives peticion_invalida.
When your function fails
Return the result anyway, with "error": true. The assistant reads it as a failed lookup and recovers on its own — tested: given "I could not find that customer", it asked to double-check the ID instead of inventing a purchase history.
The floating bubble: copy, paste, done
The most common way to add this to a system that already exists: a bubble in the corner. Nothing you already have changes.
How it must look — this is not a detail, it is the product
The business owner will use this chat every day. If it looks cheap, the assistant looks cheap no matter how good it is. The code below already solves all of this; if you write your own, meet the same bar:
- It looks like WhatsApp, because that is what everyone knows. Bubbles with a tail on their side, the user's on the right in the brand colour, the assistant's on the left in grey, with a timestamp on each message.
- No asterisks on screen. The assistant answers with formatting marks (
**bold**, dashed lists). Render them as plain text and the customer sees**2 orders**and the chat looks broken. Convert them — the example ships a twelve-line converter, no libraries. - With emojis, one or two per answer. That does not come from the code but from the instructions you send: see the
INSTRUCCIONESfield in the example. - With the three dots while it thinks. A query takes one to five seconds; with no sign of life the user thinks it froze and taps again.
- Big and comfortable: around 400 px wide and 620 tall on desktop, near full screen on mobile. A tiny chat feels like a toy.
- With sample questions on open. Nobody knows what to ask a blank assistant. Three buttons with the typical questions solve the first minute.
- In the customer's brand colours. Four values at the top of the example: change them and the chat is theirs.
This is the whole file. Paste it before </body> and change what is marked <-- CAMBIA. The tool loop, the answer formatting and the design are already solved.
<script>
(function () {
// ── 1. LO QUE TIENES QUE CAMBIAR ──────────────────────────────────────
const SITIO = "mi-sitio"; // <-- CAMBIA: el nombre en el panel
const CLAVE = "pk_tu_clave_publica"; // <-- CAMBIA: la llave del panel
// LOS COLORES DE TU MARCA. Cambia estos cuatro y el chat es tuyo.
const MARCA = {
color: "#2bc7e8", // el color principal
colorTexto: "#04222c", // texto sobre ese color
titulo: "Asistente de mi negocio",
saludo: "¡Hola! 👋 Pregúntame por tus ventas, tu agenda o tus clientes.",
};
const EJEMPLOS = ["¿Cómo vamos hoy?", "¿Qué hay agendado?", "¿Qué está pendiente?"];
const FUNCIONES = {
cierre_del_dia: {
descripcion: "Devuelve el cierre de caja de una sucursal en una fecha (AAAA-MM-DD).",
esquema: { type: "object", properties: { sucursal: { type: "string" }, fecha: { type: "string" } }, required: ["sucursal", "fecha"] },
ejecutar: async () => ({ total_usd: 4312.55, notas: 27 }),
},
};
const INSTRUCCIONES =
"Eres el asistente del sistema de una ferretería con dos sucursales, Caracas y Valencia. " +
"Hablas como en un chat de WhatsApp: cálido, directo y en español neutro. " +
"Frases cortas. Usa uno o dos emojis por respuesta, nunca más, y solo donde aporten. " +
"Usa **negrita** para los números y los nombres que importan, y listas con guiones cuando " +
"sean varios elementos: el chat las pinta bien. " +
"Nunca inventes cifras: si te falta un dato, pídelo. " +
"Cierra ofreciendo el siguiente paso cuando tenga sentido. " +
"Hoy es " + new Date().toISOString().slice(0, 10) + ". La moneda es el dólar. " +
"El cierre de caja se hace a las ocho de la noche y los precios ya llevan impuesto incluido. " +
"Si una consulta devuelve vacío, dilo tal cual en vez de suponer que fue cero. " +
"Cuando el dueño pregunte por un periodo, confirma las fechas exactas antes de responder. " +
"No hables de tus herramientas ni de cómo obtienes los datos: solo da la respuesta.";
// ── 2. DE AQUÍ PARA ABAJO NO HACE FALTA TOCAR NADA ────────────────────
const URL = "https://yapanel.yadominios.com/agente/v1/chat";
const historial = [];
const escapar = (t) =>
String(t).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
const css = `
#yad-b{position:fixed;right:20px;bottom:20px;width:58px;height:58px;border-radius:50%;
border:0;background:${MARCA.color};color:${MARCA.colorTexto};font-size:24px;cursor:pointer;
z-index:9998;box-shadow:0 8px 24px rgba(0,0,0,.3);display:grid;place-items:center;
transition:transform .15s}
#yad-b:hover{transform:scale(1.06)}
#yad-p{position:fixed;right:20px;bottom:88px;width:min(400px,calc(100vw - 32px));
height:min(620px,calc(100vh - 130px));display:none;flex-direction:column;
background:#0f1720;border:1px solid rgba(255,255,255,.08);border-radius:18px;overflow:hidden;
z-index:9999;box-shadow:0 24px 60px rgba(0,0,0,.45);
font:15px/1.55 -apple-system,system-ui,"Segoe UI",sans-serif}
#yad-p header{background:#151f2b;padding:14px 16px;display:flex;gap:12px;align-items:center;
border-bottom:1px solid rgba(255,255,255,.06)}
#yad-av{width:38px;height:38px;border-radius:50%;background:${MARCA.color};color:${MARCA.colorTexto};
display:grid;place-items:center;font-size:19px;flex:0 0 auto}
#yad-p header b{color:#e9eef4;font-size:15px;font-weight:600;display:block}
#yad-p header span{color:#7d8b9c;font-size:12.5px}
#yad-m{flex:1;overflow-y:auto;padding:16px 14px;display:flex;flex-direction:column;gap:10px;
background:#0b1219}
#yad-m::-webkit-scrollbar{width:6px}
#yad-m::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}
.yad-fila{display:flex;max-width:100%}
.yad-fila.u{justify-content:flex-end}
.yad-globo{max-width:82%;padding:9px 13px;border-radius:16px;position:relative;
word-wrap:break-word;overflow-wrap:anywhere;font-size:14.5px}
.yad-fila.u .yad-globo{background:${MARCA.color};color:${MARCA.colorTexto};border-bottom-right-radius:5px}
.yad-fila.a .yad-globo{background:#1b2733;color:#dfe7ef;border-bottom-left-radius:5px}
.yad-globo p{margin:0 0 8px}
.yad-globo p:last-child{margin:0}
.yad-globo strong{font-weight:650}
.yad-globo ul{margin:6px 0;padding-left:18px}
.yad-globo li{margin:3px 0}
.yad-globo code{background:rgba(255,255,255,.09);padding:1px 5px;border-radius:5px;font-size:13px}
.yad-hora{display:block;margin-top:4px;font-size:10.5px;opacity:.55;text-align:right}
.yad-esc{display:flex;gap:4px;padding:12px 14px;background:#1b2733;border-radius:16px;
border-bottom-left-radius:5px;width:fit-content}
.yad-esc i{width:7px;height:7px;border-radius:50%;background:#6d7d8f;display:block;
animation:yad-salta 1.2s infinite}
.yad-esc i:nth-child(2){animation-delay:.18s}
.yad-esc i:nth-child(3){animation-delay:.36s}
@keyframes yad-salta{0%,60%,100%{opacity:.35;transform:translateY(0)}30%{opacity:1;transform:translateY(-4px)}}
#yad-suge{display:flex;flex-wrap:wrap;gap:7px;padding:0 14px 12px;background:#0b1219}
#yad-suge button{background:transparent;border:1px solid rgba(255,255,255,.14);color:#aab6c4;
border-radius:999px;padding:7px 13px;font-size:13px;cursor:pointer;transition:all .15s}
#yad-suge button:hover{border-color:${MARCA.color};color:${MARCA.color}}
#yad-f{display:flex;gap:8px;padding:12px;background:#151f2b;border-top:1px solid rgba(255,255,255,.06)}
#yad-i{flex:1;min-width:0;border:1px solid rgba(255,255,255,.12);background:#0b1219;color:#e9eef4;
border-radius:22px;padding:11px 16px;font:inherit;font-size:14.5px;outline:none}
#yad-i:focus{border-color:${MARCA.color}}
#yad-i::placeholder{color:#5f6d7d}
#yad-s{border:0;background:${MARCA.color};color:${MARCA.colorTexto};border-radius:50%;
width:42px;height:42px;font-size:17px;cursor:pointer;flex:0 0 auto;display:grid;place-items:center}
#yad-s:disabled{opacity:.4;cursor:default}`;
document.head.appendChild(Object.assign(document.createElement("style"), { textContent: css }));
document.body.insertAdjacentHTML("beforeend", `
<button id="yad-b" aria-label="Abrir el asistente">💬</button>
<div id="yad-p" role="dialog" aria-label="${escapar(MARCA.titulo)}">
<header>
<div id="yad-av">🤖</div>
<div><b>${escapar(MARCA.titulo)}</b><span>En línea · responde al momento</span></div>
</header>
<div id="yad-m"></div>
<div id="yad-suge"></div>
<form id="yad-f">
<input id="yad-i" placeholder="Escribe tu pregunta…" autocomplete="off">
<button id="yad-s" type="submit" aria-label="Enviar">➤</button>
</form>
</div>`);
const panel = document.getElementById("yad-p");
const lista = document.getElementById("yad-m");
const sugerencias = document.getElementById("yad-suge");
const campo = document.getElementById("yad-i");
const enviar = document.getElementById("yad-s");
const hora = () => new Date().toLocaleTimeString("es-US", { hour: "numeric", minute: "2-digit" });
/**
* El texto del asistente viene con marcas de Markdown. Sin esto, el cliente
* ve "**2 órdenes**" con los asteriscos a la vista y el chat parece roto.
* Se ESCAPA primero y se aplican las marcas después: lo que llega es texto de
* otro sistema y nunca se pinta como HTML tal cual.
*/
function conFormato(texto) {
const seguro = escapar(texto);
const conMarcas = seguro
.replace(/**(.+?)**/g, "<strong>$1</strong>") // **negrita** primero
.replace(/(^|[^*])*([^*
]+?)*($|[^*])/g, "$1<em>$2</em>$3") // *cursiva* después
.replace(/`(.+?)`/g, "<code>$1</code>");
const lineas = conMarcas.split("
");
let html = "", enLista = false;
for (const linea of lineas) {
const item = linea.match(/^s*[-*•]s+(.*)$/);
if (item) {
if (!enLista) { html += "<ul>"; enLista = true; }
html += `<li>${item[1]}</li>`;
} else {
if (enLista) { html += "</ul>"; enLista = false; }
if (linea.trim()) html += `<p>${linea}</p>`;
}
}
if (enLista) html += "</ul>";
return html || `<p>${seguro}</p>`;
}
function globo(quien, texto) {
const fila = document.createElement("div");
fila.className = `yad-fila ${quien}`;
const g = document.createElement("div");
g.className = "yad-globo";
g.innerHTML = conFormato(texto) + `<span class="yad-hora">${hora()}</span>`;
fila.appendChild(g);
lista.appendChild(fila);
lista.scrollTop = lista.scrollHeight;
return fila;
}
function escribiendo(prender) {
const previo = document.getElementById("yad-escribiendo");
if (previo) previo.remove();
if (!prender) return;
const fila = document.createElement("div");
fila.className = "yad-fila a";
fila.id = "yad-escribiendo";
fila.innerHTML = `<div class="yad-esc"><i></i><i></i><i></i></div>`;
lista.appendChild(fila);
lista.scrollTop = lista.scrollHeight;
}
function pintarSugerencias() {
sugerencias.innerHTML = "";
for (const t of EJEMPLOS) {
const b = document.createElement("button");
b.type = "button";
b.textContent = t;
b.onclick = () => { sugerencias.innerHTML = ""; preguntarYPintar(t); };
sugerencias.appendChild(b);
}
}
async function preguntar(texto) {
historial.push({ rol: "usuario", contenido: [{ tipo: "texto", texto }] });
const herramientas = Object.entries(FUNCIONES).map(([nombre, f]) => ({
nombre, descripcion: f.descripcion, esquema: f.esquema,
}));
for (let vuelta = 0; vuelta < 5; vuelta++) {
const r = await fetch(URL, {
method: "POST",
headers: { "content-type": "application/json", "X-Sitio": SITIO, "X-Clave": CLAVE },
body: JSON.stringify({ modelo: "auto", sistema: INSTRUCCIONES, herramientas, mensajes: historial }),
});
const d = await r.json();
if (!d.ok) return "Uy, algo falló: " + d.mensaje;
historial.push({ rol: "asistente", contenido: d.contenido });
if (d.parar_por !== "herramienta") {
return d.contenido.filter((b) => b.tipo === "texto").map((b) => b.texto).join("
");
}
const resultados = [];
for (const b of d.contenido) {
if (b.tipo !== "herramienta") continue;
try {
const salida = await FUNCIONES[b.nombre].ejecutar(b.entrada);
resultados.push({ tipo: "resultado", id: b.id, contenido: JSON.stringify(salida) });
} catch (e) {
resultados.push({ tipo: "resultado", id: b.id, contenido: String(e), error: true });
}
}
historial.push({ rol: "usuario", contenido: resultados });
}
return "No pude terminar la consulta.";
}
async function preguntarYPintar(texto) {
globo("u", texto);
campo.value = "";
enviar.disabled = true;
escribiendo(true);
try {
const respuesta = await preguntar(texto);
escribiendo(false);
globo("a", respuesta);
} catch (e) {
escribiendo(false);
globo("a", "No pude conectarme. Inténtalo otra vez en un momento. 🙏");
}
enviar.disabled = false;
campo.focus();
}
document.getElementById("yad-b").onclick = () => {
const abierto = panel.style.display === "flex";
panel.style.display = abierto ? "none" : "flex";
document.getElementById("yad-b").textContent = abierto ? "💬" : "✕";
if (!abierto) {
if (!lista.children.length) { globo("a", MARCA.saludo); pintarSugerencias(); }
campo.focus();
}
};
document.getElementById("yad-f").onsubmit = (e) => {
e.preventDefault();
const texto = campo.value.trim();
if (!texto) return;
sugerencias.innerHTML = "";
void preguntarYPintar(texto);
};
})();
</script>
The only things you need to understand to adapt it
MARCAholds the four design values. Colour, text colour on that colour, title and greeting.FUNCIONESis the bridge to their system. Each entry has adescripcion(what the assistant reads to decide when to use it) andejecutar(what runs on their side).INSTRUCCIONESis where the tone comes from. That is where you tell it to speak like a chat, short, with one or two emojis and bold for the numbers. Without it, answers are correct but dull. And they must be LONG —over 4,000 characters— and identical between calls, or the cache never hits and the bill triples.- The assistant's text is never rendered as raw HTML. It is escaped first and the formatting marks applied after.
- With
pk_you must authorize the domain in the dashboard, or the call returnsorigen_no_autorizado.
If your system has its own server
Then use the secret key and put the loop on the server: the key never reaches the browser, and you can filter which questions are allowed. The loop is identical.
// Node 18+ / Cloudflare Workers - no dependencies
const URL = "https://yapanel.yadominios.com/agente/v1/chat";
const FUNCIONES = {
cierre_del_dia: {
descripcion: "Returns the daily sales close for a branch on a date (YYYY-MM-DD).",
esquema: { type: "object",
properties: { sucursal: { type: "string" }, fecha: { type: "string" } },
required: ["sucursal", "fecha"] },
ejecutar: async ({ sucursal, fecha }) => {
// YOUR real query, against YOUR database:
return await db.query(
"select sum(total) total_usd, count(*) notas from ventas where sucursal=$1 and fecha=$2",
[sucursal, fecha],
);
},
},
};
export async function preguntar(pregunta, historial = []) {
const herramientas = Object.entries(FUNCIONES).map(([nombre, f]) => ({
nombre, descripcion: f.descripcion, esquema: f.esquema,
}));
historial.push({ rol: "usuario", contenido: [{ tipo: "texto", texto: pregunta }] });
for (let vuelta = 0; vuelta < 5; vuelta++) {
const r = await fetch(URL, {
method: "POST",
headers: {
"content-type": "application/json",
"X-Sitio": "my-site",
"X-Clave": process.env.YADOMINIOS_CLAVE, // the sk_ key, from the environment
},
body: JSON.stringify({ modelo: "normal", herramientas, mensajes: historial }),
});
const d = await r.json();
if (!d.ok) throw new Error(d.error + ": " + d.mensaje);
if (d.parar_por !== "herramienta") {
historial.push({ rol: "asistente", contenido: d.contenido });
return d.contenido.filter((b) => b.tipo === "texto").map((b) => b.texto).join("\n");
}
historial.push({ rol: "asistente", contenido: d.contenido });
const resultados = [];
for (const b of d.contenido) {
if (b.tipo !== "herramienta") continue;
const salida = await FUNCIONES[b.nombre].ejecutar(b.entrada);
resultados.push({ tipo: "resultado", id: b.id, contenido: JSON.stringify(salida) });
}
historial.push({ rol: "usuario", contenido: resultados });
}
throw new Error("too many rounds");
}
Mistakes everyone makes the first time
| Symptom | What happened |
|---|---|
| It hangs forever | The loop was never written: the code only looks for parar_por: "fin". It has to run the function when it says "herramienta". |
403 origen_no_autorizado | A pk_ key with no authorized domain in the dashboard. |
| The assistant ignores a function that exists | The descripcion is too vague. Write what it returns and when it should be used. |
peticion_invalida mentioning an id | The resultado does not carry the same id as the herramienta that asked for it. |
| Credits drop faster than expected | Instructions or tools change between calls (a timestamp with the hour, say) and the cache never hits. Keep them byte-identical. |
If you are an AI integrating this gateway: read this first
- The loop is mandatory. After each response check
parar_por. On"herramienta", run the requested function, append TWO messages (theherramientablock verbatim with roleasistente, then aresultadoblock with roleusuario) and call again. Repeat until"fin". Cap the rounds (5 is fine). - The
idmust match exactly between the tool request and your result. - You keep the history. The gateway is stateless — send the whole conversation every call.
- Never put the secret key in a browser. Static apps use the public key plus authorized domains.
contenidois a string. Serialize objects to JSON.- If your function fails, still return the
resultadowith"error": trueand the message — the assistant explains it instead of hanging. - Keep instructions and tools byte-identical across a conversation so the cache hits and the bill drops.
Frequently asked questions
Does it work if my site is hosted elsewhere?
Yes. The gateway needs neither your code nor your repository: what your key authorizes is your domain. If you pay us for the service, your dashboard shows the same activation button as everyone else.
Do credits expire?
Yes: after 90 days WITHOUT use. Every question restarts the clock, so as long as you use the assistant you lose nothing. We email you 15, 7 and 1 day before they expire.
Do you see my business data?
It passes through so the assistant can answer, but it is not stored. Our log keeps only the site, date, model, token counts and credits charged — nothing of the content.
Does the gateway connect to my database?
Never. When the assistant needs data it asks you; the query runs on your system with your permissions and you return only the result.
What happens when I run out of credits?
The call returns 402 sin_creditos and the provider is never contacted, so nothing is spent. Top up in the dashboard and it keeps working.
Can I put the key inside my web page?
The public one (pk_) yes: it only works from domains you authorize and has a per-minute cap. The secret one (sk_) never — that is server-only.
Am I charged for the round that only asks to run a function?
Yes. That round consumed the full model. What is never charged is a provider failure or an invalid key.